commit
88cda043ac
13
.coveragerc
13
.coveragerc
|
@ -148,6 +148,9 @@ omit =
|
|||
homeassistant/components/hive.py
|
||||
homeassistant/components/*/hive.py
|
||||
|
||||
homeassistant/components/hlk_sw16.py
|
||||
homeassistant/components/*/hlk_sw16.py
|
||||
|
||||
homeassistant/components/homekit_controller/__init__.py
|
||||
homeassistant/components/*/homekit_controller.py
|
||||
|
||||
|
@ -203,6 +206,9 @@ omit =
|
|||
homeassistant/components/linode.py
|
||||
homeassistant/components/*/linode.py
|
||||
|
||||
homeassistant/components/lightwave.py
|
||||
homeassistant/components/*/lightwave.py
|
||||
|
||||
homeassistant/components/logi_circle.py
|
||||
homeassistant/components/*/logi_circle.py
|
||||
|
||||
|
@ -323,7 +329,8 @@ omit =
|
|||
homeassistant/components/tahoma.py
|
||||
homeassistant/components/*/tahoma.py
|
||||
|
||||
homeassistant/components/tellduslive.py
|
||||
homeassistant/components/tellduslive/__init__.py
|
||||
homeassistant/components/tellduslive/entry.py
|
||||
homeassistant/components/*/tellduslive.py
|
||||
|
||||
homeassistant/components/tellstick.py
|
||||
|
@ -400,6 +407,8 @@ omit =
|
|||
|
||||
homeassistant/components/zha/__init__.py
|
||||
homeassistant/components/zha/const.py
|
||||
homeassistant/components/zha/entities/*
|
||||
homeassistant/components/zha/helpers.py
|
||||
homeassistant/components/*/zha.py
|
||||
|
||||
homeassistant/components/zigbee.py
|
||||
|
@ -637,7 +646,6 @@ omit =
|
|||
homeassistant/components/notify/group.py
|
||||
homeassistant/components/notify/hipchat.py
|
||||
homeassistant/components/notify/homematic.py
|
||||
homeassistant/components/notify/instapush.py
|
||||
homeassistant/components/notify/kodi.py
|
||||
homeassistant/components/notify/lannouncer.py
|
||||
homeassistant/components/notify/llamalab_automate.py
|
||||
|
@ -780,6 +788,7 @@ omit =
|
|||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
homeassistant/components/sensor/qbittorrent.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/rainbird.py
|
||||
|
|
19
CODEOWNERS
19
CODEOWNERS
|
@ -51,6 +51,7 @@ homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
|||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/binary_sensor/threshold.py @fabaff
|
||||
homeassistant/components/binary_sensor/uptimerobot.py @ludeeus
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
homeassistant/components/climate/ephember.py @ttroy50
|
||||
homeassistant/components/climate/eq3btsmart.py @rytilahti
|
||||
|
@ -61,9 +62,11 @@ homeassistant/components/cover/group.py @cdce8p
|
|||
homeassistant/components/cover/template.py @PhracturedBlue
|
||||
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
|
||||
homeassistant/components/device_tracker/automatic.py @armills
|
||||
homeassistant/components/device_tracker/googlehome.py @ludeeus
|
||||
homeassistant/components/device_tracker/huawei_router.py @abmantis
|
||||
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
|
||||
homeassistant/components/device_tracker/tile.py @bachya
|
||||
homeassistant/components/device_tracker/traccar.py @ludeeus
|
||||
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
|
||||
homeassistant/components/history_graph.py @andrey-git
|
||||
homeassistant/components/influx.py @fabaff
|
||||
|
@ -109,6 +112,7 @@ homeassistant/components/sensor/glances.py @fabaff
|
|||
homeassistant/components/sensor/gpsd.py @fabaff
|
||||
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
|
||||
homeassistant/components/sensor/jewish_calendar.py @tsvi
|
||||
homeassistant/components/sensor/launch_library.py @ludeeus
|
||||
homeassistant/components/sensor/linux_battery.py @fabaff
|
||||
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
|
||||
homeassistant/components/sensor/min_max.py @fabaff
|
||||
|
@ -119,6 +123,7 @@ homeassistant/components/sensor/pi_hole.py @fabaff
|
|||
homeassistant/components/sensor/pollen.py @bachya
|
||||
homeassistant/components/sensor/pvoutput.py @fabaff
|
||||
homeassistant/components/sensor/qnap.py @colinodell
|
||||
homeassistant/components/sensor/ruter.py @ludeeus
|
||||
homeassistant/components/sensor/scrape.py @fabaff
|
||||
homeassistant/components/sensor/serial.py @fabaff
|
||||
homeassistant/components/sensor/seventeentrack.py @bachya
|
||||
|
@ -128,12 +133,15 @@ homeassistant/components/sensor/sql.py @dgomes
|
|||
homeassistant/components/sensor/statistics.py @fabaff
|
||||
homeassistant/components/sensor/swiss*.py @fabaff
|
||||
homeassistant/components/sensor/sytadin.py @gautric
|
||||
homeassistant/components/sensor/tautulli.py @ludeeus
|
||||
homeassistant/components/sensor/time_data.py @fabaff
|
||||
homeassistant/components/sensor/version.py @fabaff
|
||||
homeassistant/components/sensor/waqi.py @andrey-git
|
||||
homeassistant/components/sensor/worldclock.py @fabaff
|
||||
homeassistant/components/shiftr.py @fabaff
|
||||
homeassistant/components/spaceapi.py @fabaff
|
||||
homeassistant/components/switch/switchbot.py @danielhiversen
|
||||
homeassistant/components/switch/switchmate.py @danielhiversen
|
||||
homeassistant/components/switch/tplink.py @rytilahti
|
||||
homeassistant/components/vacuum/roomba.py @pschmitt
|
||||
homeassistant/components/weather/__init__.py @fabaff
|
||||
|
@ -157,9 +165,12 @@ homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
|||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
|
||||
# C
|
||||
homeassistant/components/cloudflare.py @ludeeus
|
||||
homeassistant/components/counter/* @fabaff
|
||||
|
||||
# D
|
||||
homeassistant/components/daikin.py @fredrike @rofrantz
|
||||
homeassistant/components/*/daikin.py @fredrike @rofrantz
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/digital_ocean.py @fabaff
|
||||
homeassistant/components/*/digital_ocean.py @fabaff
|
||||
|
@ -204,6 +215,10 @@ homeassistant/components/*/mystrom.py @fabaff
|
|||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
|
||||
# P
|
||||
homeassistant/components/point/* @fredrike
|
||||
homeassistant/components/*/point.py @fredrike
|
||||
|
||||
# Q
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
|
@ -221,8 +236,8 @@ homeassistant/components/*/simplisafe.py @bachya
|
|||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
homeassistant/components/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
|
||||
homeassistant/components/tellduslive/*.py @fredrike
|
||||
homeassistant/components/*/tellduslive.py @fredrike
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/thethingsnetwork.py @fabaff
|
||||
|
|
|
@ -78,11 +78,6 @@ class AuthManager:
|
|||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
"""Return if any auth providers are registered."""
|
||||
return bool(self._providers)
|
||||
|
||||
@property
|
||||
def support_legacy(self) -> bool:
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Storage for auth models."""
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
import hmac
|
||||
|
@ -11,7 +12,7 @@ from homeassistant.util import dt as dt_util
|
|||
|
||||
from . import models
|
||||
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY
|
||||
from .permissions import system_policies
|
||||
from .permissions import PermissionLookup, system_policies
|
||||
from .permissions.types import PolicyType # noqa: F401
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
|
@ -34,6 +35,7 @@ class AuthStore:
|
|||
self.hass = hass
|
||||
self._users = None # type: Optional[Dict[str, models.User]]
|
||||
self._groups = None # type: Optional[Dict[str, models.Group]]
|
||||
self._perm_lookup = None # type: Optional[PermissionLookup]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
|
||||
private=True)
|
||||
|
||||
|
@ -94,6 +96,7 @@ class AuthStore:
|
|||
# Until we get group management, we just put everyone in the
|
||||
# same group.
|
||||
'groups': groups,
|
||||
'perm_lookup': self._perm_lookup,
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
|
@ -269,13 +272,18 @@ class AuthStore:
|
|||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the users."""
|
||||
data = await self._store.async_load()
|
||||
[ent_reg, data] = await asyncio.gather(
|
||||
self.hass.helpers.entity_registry.async_get_registry(),
|
||||
self._store.async_load(),
|
||||
)
|
||||
|
||||
# Make sure that we're not overriding data if 2 loads happened at the
|
||||
# same time
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg)
|
||||
|
||||
if data is None:
|
||||
self._set_defaults()
|
||||
return
|
||||
|
@ -374,6 +382,7 @@ class AuthStore:
|
|||
is_owner=user_dict['is_owner'],
|
||||
is_active=user_dict['is_active'],
|
||||
system_generated=user_dict['system_generated'],
|
||||
perm_lookup=perm_lookup,
|
||||
)
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
|
|
|
@ -4,13 +4,14 @@ Sending HOTP through notify service
|
|||
"""
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
|
||||
from typing import Any, Dict, Optional, List
|
||||
|
||||
import attr
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceNotFound
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
|
@ -314,8 +315,11 @@ class NotifySetupFlow(SetupFlow):
|
|||
_generate_otp, self._secret, self._count)
|
||||
|
||||
assert self._notify_service
|
||||
await self._auth_module.async_notify(
|
||||
code, self._notify_service, self._target)
|
||||
try:
|
||||
await self._auth_module.async_notify(
|
||||
code, self._notify_service, self._target)
|
||||
except ServiceNotFound:
|
||||
return self.async_abort(reason='notify_service_not_exist')
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='setup',
|
||||
|
|
|
@ -31,6 +31,9 @@ class User:
|
|||
"""A user."""
|
||||
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
perm_lookup = attr.ib(
|
||||
type=perm_mdl.PermissionLookup, cmp=False,
|
||||
) # type: perm_mdl.PermissionLookup
|
||||
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
|
@ -66,7 +69,8 @@ class User:
|
|||
|
||||
self._permissions = perm_mdl.PolicyPermissions(
|
||||
perm_mdl.merge_policies([
|
||||
group.policy for group in self.groups]))
|
||||
group.policy for group in self.groups]),
|
||||
self.perm_lookup)
|
||||
|
||||
return self._permissions
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
"""Permissions for Home Assistant."""
|
||||
import logging
|
||||
from typing import ( # noqa: F401
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union)
|
||||
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union,
|
||||
TYPE_CHECKING)
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import CAT_ENTITIES
|
||||
from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .entities import ENTITY_POLICY_SCHEMA, compile_entities
|
||||
from .merge import merge_policies # noqa
|
||||
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
|
||||
})
|
||||
|
@ -39,13 +42,16 @@ class AbstractPermissions:
|
|||
class PolicyPermissions(AbstractPermissions):
|
||||
"""Handle permissions."""
|
||||
|
||||
def __init__(self, policy: PolicyType) -> None:
|
||||
def __init__(self, policy: PolicyType,
|
||||
perm_lookup: PermissionLookup) -> None:
|
||||
"""Initialize the permission class."""
|
||||
self._policy = policy
|
||||
self._perm_lookup = perm_lookup
|
||||
|
||||
def _entity_func(self) -> Callable[[str, str], bool]:
|
||||
"""Return a function that can test entity access."""
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES))
|
||||
return compile_entities(self._policy.get(CAT_ENTITIES),
|
||||
self._perm_lookup)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
"""Equals check."""
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"""Entity permissions."""
|
||||
from functools import wraps
|
||||
from typing import ( # noqa: F401
|
||||
Callable, Dict, List, Tuple, Union)
|
||||
from typing import Callable, List, Union # noqa: F401
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
|
||||
from .models import PermissionLookup
|
||||
from .types import CategoryType, ValueType
|
||||
|
||||
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
|
@ -15,6 +15,7 @@ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
|
|||
}))
|
||||
|
||||
ENTITY_DOMAINS = 'domains'
|
||||
ENTITY_DEVICE_IDS = 'device_ids'
|
||||
ENTITY_ENTITY_IDS = 'entity_ids'
|
||||
|
||||
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
||||
|
@ -23,6 +24,7 @@ ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
|
|||
|
||||
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
|
||||
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
|
||||
vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
|
||||
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
|
||||
}))
|
||||
|
@ -37,7 +39,7 @@ def _entity_allowed(schema: ValueType, key: str) \
|
|||
return schema.get(key)
|
||||
|
||||
|
||||
def compile_entities(policy: CategoryType) \
|
||||
def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \
|
||||
-> Callable[[str, str], bool]:
|
||||
"""Compile policy into a function that tests policy."""
|
||||
# None, Empty Dict, False
|
||||
|
@ -58,6 +60,7 @@ def compile_entities(policy: CategoryType) \
|
|||
assert isinstance(policy, dict)
|
||||
|
||||
domains = policy.get(ENTITY_DOMAINS)
|
||||
device_ids = policy.get(ENTITY_DEVICE_IDS)
|
||||
entity_ids = policy.get(ENTITY_ENTITY_IDS)
|
||||
all_entities = policy.get(SUBCAT_ALL)
|
||||
|
||||
|
@ -85,6 +88,29 @@ def compile_entities(policy: CategoryType) \
|
|||
|
||||
funcs.append(allowed_entity_id_dict)
|
||||
|
||||
if isinstance(device_ids, bool):
|
||||
def allowed_device_id_bool(entity_id: str, key: str) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed device_id."""
|
||||
return device_ids
|
||||
|
||||
funcs.append(allowed_device_id_bool)
|
||||
|
||||
elif device_ids is not None:
|
||||
def allowed_device_id_dict(entity_id: str, key: str) \
|
||||
-> Union[None, bool]:
|
||||
"""Test if allowed device_id."""
|
||||
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
|
||||
|
||||
if entity_entry is None or entity_entry.device_id is None:
|
||||
return None
|
||||
|
||||
return _entity_allowed(
|
||||
device_ids.get(entity_entry.device_id), key # type: ignore
|
||||
)
|
||||
|
||||
funcs.append(allowed_device_id_dict)
|
||||
|
||||
if isinstance(domains, bool):
|
||||
def allowed_domain_bool(entity_id: str, key: str) \
|
||||
-> Union[None, bool]:
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
"""Models for permissions."""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable=unused-import
|
||||
from homeassistant.helpers import ( # noqa
|
||||
entity_registry as ent_reg,
|
||||
)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class PermissionLookup:
|
||||
"""Class to hold data for permission lookups."""
|
||||
|
||||
entity_registry = attr.ib(type='ent_reg.EntityRegistry')
|
|
@ -1,6 +1,5 @@
|
|||
"""Common code for permissions."""
|
||||
from typing import ( # noqa: F401
|
||||
Mapping, Union, Any)
|
||||
from typing import Mapping, Union
|
||||
|
||||
# MyPy doesn't support recursion yet. So writing it out as far as we need.
|
||||
|
||||
|
|
|
@ -226,7 +226,11 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||
|
||||
if user_input is None and hasattr(auth_module,
|
||||
'async_initialize_login_mfa_step'):
|
||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
||||
try:
|
||||
await auth_module.async_initialize_login_mfa_step(self.user.id)
|
||||
except HomeAssistantError:
|
||||
_LOGGER.exception('Error initializing MFA step')
|
||||
return self.async_abort(reason='unknown_error')
|
||||
|
||||
if user_input is not None:
|
||||
expires = self.created_at + MFA_SESSION_EXPIRATION
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
"""Home Assistant auth provider."""
|
||||
import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
import bcrypt
|
||||
|
@ -11,12 +9,10 @@ import voluptuous as vol
|
|||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
|
||||
from ..models import Credentials, UserMeta
|
||||
from ..util import generate_secret
|
||||
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
|
@ -62,7 +58,6 @@ class Data:
|
|||
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
|
||||
|
@ -94,39 +89,11 @@ class Data:
|
|||
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# if the hash is not a bcrypt hash...
|
||||
# provide a transparant upgrade for old pbkdf2 hash format
|
||||
if not (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$')):
|
||||
# IMPORTANT! validate the login, bail if invalid
|
||||
hashed = self.legacy_hash_password(password)
|
||||
if not hmac.compare_digest(hashed, user_hash):
|
||||
raise InvalidAuth
|
||||
# then re-hash the valid password with bcrypt
|
||||
self.change_password(found['username'], password)
|
||||
run_coroutine_threadsafe(
|
||||
self.async_save(), self.hass.loop
|
||||
).result()
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# bcrypt.checkpw is timing-safe
|
||||
if not bcrypt.checkpw(password.encode(),
|
||||
user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
def legacy_hash_password(self, password: str,
|
||||
for_storage: bool = False) -> bytes:
|
||||
"""LEGACY password encoding."""
|
||||
# We're no longer storing salts in data, but if one exists we
|
||||
# should be able to retrieve it.
|
||||
salt = self._data['salt'].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
|
|
|
@ -115,11 +115,6 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||
conf_util.merge_packages_config(
|
||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
|
||||
# Ensure we have no None values after merge
|
||||
for key, value in config.items():
|
||||
if not value:
|
||||
config[key] = {}
|
||||
|
||||
hass.config_entries = config_entries.ConfigEntries(hass, config)
|
||||
await hass.config_entries.async_load()
|
||||
|
||||
|
|
|
@ -25,21 +25,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
return
|
||||
data = hass.data[BLINK_DATA]
|
||||
|
||||
# Current version of blinkpy API only supports one sync module. When
|
||||
# support for additional models is added, the sync module name should
|
||||
# come from the API.
|
||||
sync_modules = []
|
||||
sync_modules.append(BlinkSyncModule(data, 'sync'))
|
||||
for sync_name, sync_module in data.sync.items():
|
||||
sync_modules.append(BlinkSyncModule(data, sync_name, sync_module))
|
||||
add_entities(sync_modules, True)
|
||||
|
||||
|
||||
class BlinkSyncModule(AlarmControlPanel):
|
||||
"""Representation of a Blink Alarm Control Panel."""
|
||||
|
||||
def __init__(self, data, name):
|
||||
def __init__(self, data, name, sync):
|
||||
"""Initialize the alarm control panel."""
|
||||
self.data = data
|
||||
self.sync = data.sync
|
||||
self.sync = sync
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
|
@ -68,6 +66,7 @@ class BlinkSyncModule(AlarmControlPanel):
|
|||
"""Return the state attributes."""
|
||||
attr = self.sync.attributes
|
||||
attr['network_info'] = self.data.networks
|
||||
attr['associated_cameras'] = list(self.sync.cameras.keys())
|
||||
attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
|
||||
return attr
|
||||
|
||||
|
|
|
@ -12,7 +12,8 @@ from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN
|
|||
from homeassistant.components.lupusec import LupusecDevice
|
||||
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
|
||||
DEPENDENCIES = ['lupusec']
|
||||
|
||||
|
@ -50,6 +51,8 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanel):
|
|||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._device.is_home:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif self._device.is_alarm_triggered:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
else:
|
||||
state = None
|
||||
return state
|
||||
|
|
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -116,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
)])
|
||||
|
||||
|
||||
class ManualAlarm(alarm.AlarmControlPanel):
|
||||
class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
|
||||
"""
|
||||
Representation of an alarm status.
|
||||
|
||||
|
@ -310,7 +310,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
self._state = state.state
|
||||
self._state_ts = state.last_updated
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate)
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -51,7 +51,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
|||
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_entities, discovery_info=None):
|
||||
"""Set up MQTT alarm control panel through configuration.yaml."""
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
@ -59,7 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def async_discover(discovery_payload):
|
||||
"""Discover and add an MQTT alarm control panel."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(hass, config, async_add_entities,
|
||||
await _async_setup_entity(config, async_add_entities,
|
||||
discovery_payload[ATTR_DISCOVERY_HASH])
|
||||
|
||||
async_dispatcher_connect(
|
||||
|
@ -67,54 +67,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async_discover)
|
||||
|
||||
|
||||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
async def _async_setup_entity(config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
async_add_entities([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_PAYLOAD_DISARM),
|
||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
discovery_hash,)])
|
||||
async_add_entities([MqttAlarm(config, discovery_hash)])
|
||||
|
||||
|
||||
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
||||
alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available,
|
||||
discovery_hash):
|
||||
def __init__(self, config, discovery_hash):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._config = config
|
||||
self._sub_state = None
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
qos = config.get(CONF_QOS)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload_disarm = payload_disarm
|
||||
self._payload_arm_home = payload_arm_home
|
||||
self._payload_arm_away = payload_arm_away
|
||||
self._code = code
|
||||
self._discovery_hash = discovery_hash
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await super().async_added_to_hass()
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._config = config
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
|
@ -126,8 +119,16 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
self._state = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
{'state_topic': {'topic': self._config.get(CONF_STATE_TOPIC),
|
||||
'msg_callback': message_received,
|
||||
'qos': self._config.get(CONF_QOS)}})
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -137,7 +138,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -147,9 +148,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
code = self._config.get(CONF_CODE)
|
||||
if code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(code, str) and re.search('^\\d+$', code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
|
@ -161,8 +163,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
if not self._validate_code(code, 'disarming'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_DISARM),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command.
|
||||
|
@ -172,8 +176,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
if not self._validate_code(code, 'arming home'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command.
|
||||
|
@ -183,12 +189,15 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
|
|||
if not self._validate_code(code, 'arming away'):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
conf_code = self._config.get(CONF_CODE)
|
||||
check = conf_code is None or code == conf_code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
return check
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.5']
|
||||
|
||||
CONF_AREA_ID = 'area_id'
|
||||
|
||||
|
|
|
@ -504,6 +504,20 @@ class _AlexaColorTemperatureController(_AlexaInterface):
|
|||
def name(self):
|
||||
return 'Alexa.ColorTemperatureController'
|
||||
|
||||
def properties_supported(self):
|
||||
return [{'name': 'colorTemperatureInKelvin'}]
|
||||
|
||||
def properties_retrievable(self):
|
||||
return True
|
||||
|
||||
def get_property(self, name):
|
||||
if name != 'colorTemperatureInKelvin':
|
||||
raise _UnsupportedProperty(name)
|
||||
if 'color_temp' in self.entity.attributes:
|
||||
return color_util.color_temperature_mired_to_kelvin(
|
||||
self.entity.attributes['color_temp'])
|
||||
return 0
|
||||
|
||||
|
||||
class _AlexaPercentageController(_AlexaInterface):
|
||||
"""Implements Alexa.PercentageController.
|
||||
|
|
|
@ -9,7 +9,9 @@ import json
|
|||
import logging
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPBadRequest
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
@ -21,7 +23,8 @@ from homeassistant.const import (
|
|||
URL_API_TEMPLATE, __version__)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.auth.permissions.const import POLICY_READ
|
||||
from homeassistant.exceptions import TemplateError, Unauthorized
|
||||
from homeassistant.exceptions import (
|
||||
TemplateError, Unauthorized, ServiceNotFound)
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
|
@ -339,8 +342,11 @@ class APIDomainServicesView(HomeAssistantView):
|
|||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
await hass.services.async_call(
|
||||
domain, service, data, True, self.context(request))
|
||||
try:
|
||||
await hass.services.async_call(
|
||||
domain, service, data, True, self.context(request))
|
||||
except (vol.Invalid, ServiceNotFound):
|
||||
raise HTTPBadRequest()
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.10']
|
||||
REQUIREMENTS = ['pyatv==0.3.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import voluptuous as vol
|
|||
from requests import RequestException
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.helpers import discovery
|
||||
|
@ -141,11 +140,11 @@ def setup(hass, config):
|
|||
from requests import Session
|
||||
|
||||
conf = config[DOMAIN]
|
||||
api_http_session = None
|
||||
try:
|
||||
api_http_session = Session()
|
||||
except RequestException as ex:
|
||||
_LOGGER.warning("Creating HTTP session failed with: %s", str(ex))
|
||||
api_http_session = None
|
||||
|
||||
api = Api(timeout=conf.get(CONF_TIMEOUT), http_session=api_http_session)
|
||||
|
||||
|
@ -157,6 +156,20 @@ def setup(hass, config):
|
|||
install_id=conf.get(CONF_INSTALL_ID),
|
||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
|
||||
|
||||
def close_http_session(event):
|
||||
"""Close API sessions used to connect to August."""
|
||||
_LOGGER.debug("Closing August HTTP sessions")
|
||||
if api_http_session:
|
||||
try:
|
||||
api_http_session.close()
|
||||
except RequestException:
|
||||
pass
|
||||
|
||||
_LOGGER.debug("August HTTP session closed.")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session)
|
||||
_LOGGER.debug("Registered for HASS stop event")
|
||||
|
||||
return setup_august(hass, config, api, authenticator)
|
||||
|
||||
|
||||
|
@ -178,22 +191,6 @@ class AugustData:
|
|||
self._door_state_by_id = {}
|
||||
self._activities_by_id = {}
|
||||
|
||||
@callback
|
||||
def august_api_stop(event):
|
||||
"""Close the API HTTP session."""
|
||||
_LOGGER.debug("Closing August HTTP session")
|
||||
|
||||
try:
|
||||
self._api.http_session.close()
|
||||
self._api.http_session = None
|
||||
except RequestException:
|
||||
pass
|
||||
_LOGGER.debug("August HTTP session closed.")
|
||||
|
||||
self._hass.bus.listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, august_api_stop)
|
||||
_LOGGER.debug("Registered for HASS stop event")
|
||||
|
||||
@property
|
||||
def house_ids(self):
|
||||
"""Return a list of house_ids."""
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
|
||||
},
|
||||
"setup": {
|
||||
"description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
|
||||
"description": "S'ha enviat una contrasenya d'un sol \u00fas mitjan\u00e7ant **notify.{notify_service}**. Introdu\u00efu-la a continuaci\u00f3:",
|
||||
"title": "Verifiqueu la configuraci\u00f3"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Jednor\u00e1zov\u00e9 heslo bylo odesl\u00e1no prost\u0159ednictv\u00edm **notify.{notify_service}**. Zadejte jej n\u00ed\u017ee:",
|
||||
"title": "Ov\u011b\u0159en\u00ed nastaven\u00ed"
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +21,14 @@
|
|||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Neplatn\u00fd k\u00f3d, zkuste to znovu. Pokud se tato chyba opakuje, ujist\u011bte se, \u017ee hodiny syst\u00e9mu Home Assistant jsou spr\u00e1vn\u011b nastaveny."
|
||||
}
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Chcete-li aktivovat dvoufaktorovou autentizaci pomoc\u00ed jednor\u00e1zov\u00fdch hesel zalo\u017een\u00fdch na \u010dase, na\u010dt\u011bte k\u00f3d QR pomoc\u00ed va\u0161\u00ed autentiza\u010dn\u00ed aplikace. Pokud ji nem\u00e1te, doporu\u010dujeme bu\u010f [Google Authenticator](https://support.google.com/accounts/answer/1066447) nebo [Authy](https://authy.com/). \n\n {qr_code} \n \n Po skenov\u00e1n\u00ed k\u00f3du zadejte \u0161estcifern\u00fd k\u00f3d z aplikace a ov\u011b\u0159te nastaven\u00ed. Pokud m\u00e1te probl\u00e9my se skenov\u00e1n\u00edm k\u00f3du QR, prove\u010fte ru\u010dn\u00ed nastaven\u00ed s k\u00f3dem **`{code}`**.",
|
||||
"title": "Nastavte dvoufaktorovou autentizaci pomoc\u00ed TOTP"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,18 +2,18 @@
|
|||
"mfa_setup": {
|
||||
"notify": {
|
||||
"abort": {
|
||||
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
|
||||
"no_available_service": "Storitve obve\u0161\u010danja niso na voljo."
|
||||
},
|
||||
"error": {
|
||||
"invalid_code": "Neveljavna koda, poskusite znova."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
|
||||
"description": "Izberite eno od storitev obve\u0161\u010danja:",
|
||||
"title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
|
||||
},
|
||||
"setup": {
|
||||
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
|
||||
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Prosimo, vnesite ga spodaj:",
|
||||
"title": "Preverite nastavitev"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -16,12 +16,13 @@ from homeassistant.core import CoreState
|
|||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID,
|
||||
EVENT_AUTOMATION_TRIGGERED, ATTR_NAME)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
@ -182,7 +183,7 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
class AutomationEntity(ToggleEntity):
|
||||
class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
|
||||
|
@ -227,12 +228,13 @@ class AutomationEntity(ToggleEntity):
|
|||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Startup with initial state or previous state."""
|
||||
await super().async_added_to_hass()
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug("Automation %s initial state %s from config "
|
||||
"initial_state", self.entity_id, enable_automation)
|
||||
else:
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
state = await self.async_get_last_state()
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
|
@ -285,12 +287,17 @@ class AutomationEntity(ToggleEntity):
|
|||
"""
|
||||
if skip_condition or self._cond_func(variables):
|
||||
self.async_set_context(context)
|
||||
self.hass.bus.async_fire(EVENT_AUTOMATION_TRIGGERED, {
|
||||
ATTR_NAME: self._name,
|
||||
ATTR_ENTITY_ID: self.entity_id,
|
||||
}, context=context)
|
||||
await self._async_action(self.entity_id, variables, context)
|
||||
self._last_triggered = utcnow()
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Remove listeners when removing automation from HASS."""
|
||||
await super().async_will_remove_from_hass()
|
||||
await self.async_turn_off()
|
||||
|
||||
async def async_enable(self):
|
||||
|
@ -368,8 +375,6 @@ def _async_get_action(hass, config, name):
|
|||
async def action(entity_id, variables, context):
|
||||
"""Execute an action."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
hass.components.logbook.async_log_entry(
|
||||
name, 'has been triggered', DOMAIN, entity_id)
|
||||
await script_obj.async_run(variables, context)
|
||||
|
||||
return action
|
||||
|
|
|
@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
data = hass.data[BLINK_DATA]
|
||||
|
||||
devs = []
|
||||
for camera in data.sync.cameras:
|
||||
for camera in data.cameras:
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
devs.append(BlinkBinarySensor(data, camera, sensor_type))
|
||||
add_entities(devs, True)
|
||||
|
@ -34,7 +34,7 @@ class BlinkBinarySensor(BinarySensorDevice):
|
|||
name, icon = BINARY_SENSORS[sensor_type]
|
||||
self._name = "{} {} {}".format(BLINK_DATA, camera, name)
|
||||
self._icon = icon
|
||||
self._camera = data.sync.cameras[camera]
|
||||
self._camera = data.cameras[camera]
|
||||
self._state = None
|
||||
self._unique_id = "{}-{}".format(self._camera.serial, self._type)
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@ DEPENDENCIES = ['fibaro']
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'com.fibaro.floodSensor': ['Flood', 'mdi:water', 'flood'],
|
||||
'com.fibaro.motionSensor': ['Motion', 'mdi:run', 'motion'],
|
||||
'com.fibaro.doorSensor': ['Door', 'mdi:window-open', 'door'],
|
||||
'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'],
|
||||
'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'],
|
||||
|
|
|
@ -3,59 +3,39 @@
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ihc/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
BinarySensorDevice)
|
||||
from homeassistant.components.ihc import (
|
||||
validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO)
|
||||
from homeassistant.components.ihc.const import CONF_INVERTING
|
||||
IHC_DATA, IHC_CONTROLLER, IHC_INFO)
|
||||
from homeassistant.components.ihc.const import (
|
||||
CONF_INVERTING)
|
||||
from homeassistant.components.ihc.ihcdevice import IHCDevice
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_TYPE, CONF_ID, CONF_BINARY_SENSORS)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
CONF_TYPE)
|
||||
|
||||
DEPENDENCIES = ['ihc']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_BINARY_SENSORS, default=[]):
|
||||
vol.All(cv.ensure_list, [
|
||||
vol.All({
|
||||
vol.Required(CONF_ID): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
|
||||
}, validate_name)
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the IHC binary sensor platform."""
|
||||
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER]
|
||||
info = hass.data[IHC_DATA][IHC_INFO]
|
||||
if discovery_info is None:
|
||||
return
|
||||
devices = []
|
||||
if discovery_info:
|
||||
for name, device in discovery_info.items():
|
||||
ihc_id = device['ihc_id']
|
||||
product_cfg = device['product_cfg']
|
||||
product = device['product']
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
product_cfg.get(CONF_TYPE),
|
||||
product_cfg[CONF_INVERTING],
|
||||
product)
|
||||
devices.append(sensor)
|
||||
else:
|
||||
binary_sensors = config[CONF_BINARY_SENSORS]
|
||||
for sensor_cfg in binary_sensors:
|
||||
ihc_id = sensor_cfg[CONF_ID]
|
||||
name = sensor_cfg[CONF_NAME]
|
||||
sensor_type = sensor_cfg.get(CONF_TYPE)
|
||||
inverting = sensor_cfg[CONF_INVERTING]
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
sensor_type, inverting)
|
||||
devices.append(sensor)
|
||||
for name, device in discovery_info.items():
|
||||
ihc_id = device['ihc_id']
|
||||
product_cfg = device['product_cfg']
|
||||
product = device['product']
|
||||
# Find controller that corresponds with device id
|
||||
ctrl_id = device['ctrl_id']
|
||||
ihc_key = IHC_DATA.format(ctrl_id)
|
||||
info = hass.data[ihc_key][IHC_INFO]
|
||||
ihc_controller = hass.data[ihc_key][IHC_CONTROLLER]
|
||||
|
||||
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
|
||||
product_cfg.get(CONF_TYPE),
|
||||
product_cfg[CONF_INVERTING],
|
||||
product)
|
||||
devices.append(sensor)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_OFF_DELAY):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=0)),
|
||||
# Integrations shouldn't never expose unique_id through configuration
|
||||
# this here is an exception because MQTT is a msg transport, not a protocol
|
||||
# Integrations should never expose unique_id through configuration.
|
||||
# This is an exception because MQTT is a message transport, not a protocol
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
@ -55,7 +55,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
|||
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_entities, discovery_info=None):
|
||||
"""Set up MQTT binary sensor through configuration.yaml."""
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
@ -63,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def async_discover(discovery_payload):
|
||||
"""Discover and add a MQTT binary sensor."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(hass, config, async_add_entities,
|
||||
await _async_setup_entity(config, async_add_entities,
|
||||
discovery_payload[ATTR_DISCOVERY_HASH])
|
||||
|
||||
async_dispatcher_connect(
|
||||
|
@ -71,17 +71,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async_discover)
|
||||
|
||||
|
||||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
|
||||
"""Set up the MQTT binary sensor."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
async_add_entities([MqttBinarySensor(
|
||||
config,
|
||||
discovery_hash
|
||||
)])
|
||||
async_add_entities([MqttBinarySensor(config, discovery_hash)])
|
||||
|
||||
|
||||
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
||||
|
@ -91,30 +83,18 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
def __init__(self, config, discovery_hash):
|
||||
"""Initialize the MQTT binary sensor."""
|
||||
self._config = config
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = None
|
||||
self._sub_state = None
|
||||
self._delay_listener = None
|
||||
|
||||
self._name = None
|
||||
self._state_topic = None
|
||||
self._device_class = None
|
||||
self._payload_on = None
|
||||
self._payload_off = None
|
||||
self._qos = None
|
||||
self._force_update = None
|
||||
self._off_delay = None
|
||||
self._template = None
|
||||
self._unique_id = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
qos = config.get(CONF_QOS)
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, self._qos,
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
|
@ -122,37 +102,23 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe mqtt events."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await super().async_added_to_hass()
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._setup_from_config(config)
|
||||
self._config = config
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._name = config.get(CONF_NAME)
|
||||
self._state_topic = config.get(CONF_STATE_TOPIC)
|
||||
self._device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._qos = config.get(CONF_QOS)
|
||||
self._force_update = config.get(CONF_FORCE_UPDATE)
|
||||
self._off_delay = config.get(CONF_OFF_DELAY)
|
||||
self._payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
self._payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None and value_template.hass is None:
|
||||
value_template.hass = self.hass
|
||||
self._template = value_template
|
||||
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = self.hass
|
||||
|
||||
@callback
|
||||
def off_delay_listener(now):
|
||||
"""Switch device off after a delay."""
|
||||
|
@ -163,34 +129,37 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
@callback
|
||||
def state_message_received(_topic, payload, _qos):
|
||||
"""Handle a new received MQTT state message."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
value_template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
payload = value_template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
if payload == self._payload_on:
|
||||
if payload == self._config.get(CONF_PAYLOAD_ON):
|
||||
self._state = True
|
||||
elif payload == self._payload_off:
|
||||
elif payload == self._config.get(CONF_PAYLOAD_OFF):
|
||||
self._state = False
|
||||
else: # Payload is not for this entity
|
||||
_LOGGER.warning('No matching payload found'
|
||||
' for entity: %s with state_topic: %s',
|
||||
self._name, self._state_topic)
|
||||
self._config.get(CONF_NAME),
|
||||
self._config.get(CONF_STATE_TOPIC))
|
||||
return
|
||||
|
||||
if self._delay_listener is not None:
|
||||
self._delay_listener()
|
||||
self._delay_listener = None
|
||||
|
||||
if (self._state and self._off_delay is not None):
|
||||
off_delay = self._config.get(CONF_OFF_DELAY)
|
||||
if (self._state and off_delay is not None):
|
||||
self._delay_listener = evt.async_call_later(
|
||||
self.hass, self._off_delay, off_delay_listener)
|
||||
self.hass, off_delay, off_delay_listener)
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
{'state_topic': {'topic': self._state_topic,
|
||||
{'state_topic': {'topic': self._config.get(CONF_STATE_TOPIC),
|
||||
'msg_callback': state_message_received,
|
||||
'qos': self._qos}})
|
||||
'qos': self._config.get(CONF_QOS)}})
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
|
@ -205,7 +174,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -215,12 +184,12 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
|
|||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self._device_class
|
||||
return self._config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
@property
|
||||
def force_update(self):
|
||||
"""Force update."""
|
||||
return self._force_update
|
||||
return self._config.get(CONF_FORCE_UPDATE)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
|
|
@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.point/
|
|||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as PARENT_DOMAIN, BinarySensorDevice)
|
||||
from homeassistant.components.point import MinutPointEntity
|
||||
from homeassistant.components.point.const import (
|
||||
DOMAIN as POINT_DOMAIN, NEW_DEVICE, SIGNAL_WEBHOOK)
|
||||
DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
|
@ -40,10 +41,16 @@ EVENTS = {
|
|||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a Point's binary sensors based on a config entry."""
|
||||
device_id = config_entry.data[NEW_DEVICE]
|
||||
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
|
||||
async_add_entities((MinutPointBinarySensor(client, device_id, device_class)
|
||||
for device_class in EVENTS), True)
|
||||
async def async_discover_sensor(device_id):
|
||||
"""Discover and add a discovered sensor."""
|
||||
client = hass.data[POINT_DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
(MinutPointBinarySensor(client, device_id, device_class)
|
||||
for device_class in EVENTS), True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
hass, POINT_DISCOVERY_NEW.format(PARENT_DOMAIN, POINT_DOMAIN),
|
||||
async_discover_sensor)
|
||||
|
||||
|
||||
class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice):
|
||||
|
|
|
@ -14,46 +14,48 @@ DEPENDENCIES = ['sense']
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BIN_SENSOR_CLASS = 'power'
|
||||
MDI_ICONS = {'ac': 'air-conditioner',
|
||||
'aquarium': 'fish',
|
||||
'car': 'car-electric',
|
||||
'computer': 'desktop-classic',
|
||||
'cup': 'coffee',
|
||||
'dehumidifier': 'water-off',
|
||||
'dishes': 'dishwasher',
|
||||
'drill': 'toolbox',
|
||||
'fan': 'fan',
|
||||
'freezer': 'fridge-top',
|
||||
'fridge': 'fridge-bottom',
|
||||
'game': 'gamepad-variant',
|
||||
'garage': 'garage',
|
||||
'grill': 'stove',
|
||||
'heat': 'fire',
|
||||
'heater': 'radiatior',
|
||||
'humidifier': 'water',
|
||||
'kettle': 'kettle',
|
||||
'leafblower': 'leaf',
|
||||
'lightbulb': 'lightbulb',
|
||||
'media_console': 'set-top-box',
|
||||
'modem': 'router-wireless',
|
||||
'outlet': 'power-socket-us',
|
||||
'papershredder': 'shredder',
|
||||
'printer': 'printer',
|
||||
'pump': 'water-pump',
|
||||
'settings': 'settings',
|
||||
'skillet': 'pot',
|
||||
'smartcamera': 'webcam',
|
||||
'socket': 'power-plug',
|
||||
'sound': 'speaker',
|
||||
'stove': 'stove',
|
||||
'trash': 'trash-can',
|
||||
'tv': 'television',
|
||||
'vacuum': 'robot-vacuum',
|
||||
'washer': 'washing-machine'}
|
||||
MDI_ICONS = {
|
||||
'ac': 'air-conditioner',
|
||||
'aquarium': 'fish',
|
||||
'car': 'car-electric',
|
||||
'computer': 'desktop-classic',
|
||||
'cup': 'coffee',
|
||||
'dehumidifier': 'water-off',
|
||||
'dishes': 'dishwasher',
|
||||
'drill': 'toolbox',
|
||||
'fan': 'fan',
|
||||
'freezer': 'fridge-top',
|
||||
'fridge': 'fridge-bottom',
|
||||
'game': 'gamepad-variant',
|
||||
'garage': 'garage',
|
||||
'grill': 'stove',
|
||||
'heat': 'fire',
|
||||
'heater': 'radiatior',
|
||||
'humidifier': 'water',
|
||||
'kettle': 'kettle',
|
||||
'leafblower': 'leaf',
|
||||
'lightbulb': 'lightbulb',
|
||||
'media_console': 'set-top-box',
|
||||
'modem': 'router-wireless',
|
||||
'outlet': 'power-socket-us',
|
||||
'papershredder': 'shredder',
|
||||
'printer': 'printer',
|
||||
'pump': 'water-pump',
|
||||
'settings': 'settings',
|
||||
'skillet': 'pot',
|
||||
'smartcamera': 'webcam',
|
||||
'socket': 'power-plug',
|
||||
'sound': 'speaker',
|
||||
'stove': 'stove',
|
||||
'trash': 'trash-can',
|
||||
'tv': 'television',
|
||||
'vacuum': 'robot-vacuum',
|
||||
'washer': 'washing-machine',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Sense sensor."""
|
||||
"""Set up the Sense binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
|
@ -67,14 +69,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
def sense_to_mdi(sense_icon):
|
||||
"""Convert sense icon to mdi icon."""
|
||||
return 'mdi:' + MDI_ICONS.get(sense_icon, 'power-plug')
|
||||
return 'mdi:{}'.format(MDI_ICONS.get(sense_icon, 'power-plug'))
|
||||
|
||||
|
||||
class SenseDevice(BinarySensorDevice):
|
||||
"""Implementation of a Sense energy device binary sensor."""
|
||||
|
||||
def __init__(self, data, device):
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the Sense binary sensor."""
|
||||
self._name = device['name']
|
||||
self._id = device['id']
|
||||
self._icon = sense_to_mdi(device['icon'])
|
||||
|
|
|
@ -41,6 +41,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
|
|||
self._state = None
|
||||
self._icon = None
|
||||
self._battery = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
@ -71,6 +72,11 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
|
|||
attr[ATTR_BATTERY_LEVEL] = self._battery
|
||||
return attr
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self.controller.get_states([self.tahoma_device])
|
||||
|
@ -82,11 +88,13 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
|
|||
self._state = STATE_ON
|
||||
|
||||
if 'core:SensorDefectState' in self.tahoma_device.active_states:
|
||||
# Set to 'lowBattery' for low battery warning.
|
||||
# 'lowBattery' for low battery warning. 'dead' for not available.
|
||||
self._battery = self.tahoma_device.active_states[
|
||||
'core:SensorDefectState']
|
||||
self._available = bool(self._battery != 'dead')
|
||||
else:
|
||||
self._battery = None
|
||||
self._available = True
|
||||
|
||||
if self._state == STATE_ON:
|
||||
self._icon = "mdi:fire"
|
||||
|
|
|
@ -9,8 +9,9 @@ https://home-assistant.io/components/binary_sensor.tellduslive/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||
from homeassistant.components import tellduslive
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -19,8 +20,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
"""Set up Tellstick sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
client = hass.data[tellduslive.DOMAIN]
|
||||
add_entities(
|
||||
TelldusLiveSensor(hass, binary_sensor)
|
||||
TelldusLiveSensor(client, binary_sensor)
|
||||
for binary_sensor in discovery_info
|
||||
)
|
||||
|
||||
|
|
|
@ -6,17 +6,19 @@ https://home-assistant.io/components/binary_sensor.volvooncall/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.volvooncall import VolvoEntity
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASSES)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Volvo sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
add_entities([VolvoSensor(hass, *discovery_info)])
|
||||
async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)])
|
||||
|
||||
|
||||
class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
|
@ -25,14 +27,11 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
val = getattr(self.vehicle, self._attribute)
|
||||
if self._attribute == 'bulb_failures':
|
||||
return bool(val)
|
||||
if self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
return val != 'Normal'
|
||||
return self.instrument.is_on
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return 'safety'
|
||||
if self.instrument.device_class in DEVICE_CLASSES:
|
||||
return self.instrument.device_class
|
||||
return None
|
||||
|
|
|
@ -7,7 +7,11 @@ at https://home-assistant.io/components/binary_sensor.zha/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,23 +30,43 @@ CLASS_MAPPING = {
|
|||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation binary sensors."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||
await _async_setup_iaszone(hass, config, async_add_entities,
|
||||
discovery_info)
|
||||
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||
await _async_setup_remote(hass, config, async_add_entities,
|
||||
discovery_info)
|
||||
"""Old way of setting up Zigbee Home Automation binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def _async_setup_iaszone(hass, config, async_add_entities,
|
||||
discovery_info):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Zigbee Home Automation binary sensor from config entry."""
|
||||
async def async_discover(discovery_info):
|
||||
await _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
[discovery_info])
|
||||
|
||||
unsub = async_dispatcher_connect(
|
||||
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
|
||||
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
|
||||
|
||||
binary_sensors = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
|
||||
if binary_sensors is not None:
|
||||
await _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
binary_sensors.values())
|
||||
del hass.data[DATA_ZHA][DOMAIN]
|
||||
|
||||
|
||||
async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
discovery_infos):
|
||||
"""Set up the ZHA binary sensors."""
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
if IasZone.cluster_id in discovery_info['in_clusters']:
|
||||
entities.append(await _async_setup_iaszone(discovery_info))
|
||||
elif OnOff.cluster_id in discovery_info['out_clusters']:
|
||||
entities.append(await _async_setup_remote(discovery_info))
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
async def _async_setup_iaszone(discovery_info):
|
||||
device_class = None
|
||||
from zigpy.zcl.clusters.security import IasZone
|
||||
cluster = discovery_info['in_clusters'][IasZone.cluster_id]
|
||||
|
@ -58,13 +82,10 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
|
|||
# If we fail to read from the device, use a non-specific class
|
||||
pass
|
||||
|
||||
sensor = BinarySensor(device_class, **discovery_info)
|
||||
async_add_entities([sensor], update_before_add=True)
|
||||
return BinarySensor(device_class, **discovery_info)
|
||||
|
||||
|
||||
async def _async_setup_remote(hass, config, async_add_entities,
|
||||
discovery_info):
|
||||
|
||||
async def _async_setup_remote(discovery_info):
|
||||
remote = Remote(**discovery_info)
|
||||
|
||||
if discovery_info['new_join']:
|
||||
|
@ -72,21 +93,21 @@ async def _async_setup_remote(hass, config, async_add_entities,
|
|||
out_clusters = discovery_info['out_clusters']
|
||||
if OnOff.cluster_id in out_clusters:
|
||||
cluster = out_clusters[OnOff.cluster_id]
|
||||
await zha.configure_reporting(
|
||||
await helpers.configure_reporting(
|
||||
remote.entity_id, cluster, 0, min_report=0, max_report=600,
|
||||
reportable_change=1
|
||||
)
|
||||
if LevelControl.cluster_id in out_clusters:
|
||||
cluster = out_clusters[LevelControl.cluster_id]
|
||||
await zha.configure_reporting(
|
||||
await helpers.configure_reporting(
|
||||
remote.entity_id, cluster, 0, min_report=1, max_report=600,
|
||||
reportable_change=1
|
||||
)
|
||||
|
||||
async_add_entities([remote], update_before_add=True)
|
||||
return remote
|
||||
|
||||
|
||||
class BinarySensor(zha.Entity, BinarySensorDevice):
|
||||
class BinarySensor(ZhaEntity, BinarySensorDevice):
|
||||
"""The ZHA Binary Sensor."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
@ -130,16 +151,16 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
|
|||
"""Retrieve latest state."""
|
||||
from zigpy.types.basic import uint16_t
|
||||
|
||||
result = await zha.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
result = await helpers.safe_read(self._endpoint.ias_zone,
|
||||
['zone_status'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
state = result.get('zone_status', self._state)
|
||||
if isinstance(state, (int, uint16_t)):
|
||||
self._state = result.get('zone_status', self._state) & 3
|
||||
|
||||
|
||||
class Remote(zha.Entity, BinarySensorDevice):
|
||||
class Remote(ZhaEntity, BinarySensorDevice):
|
||||
"""ZHA switch/remote controller/button."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
@ -252,7 +273,7 @@ class Remote(zha.Entity, BinarySensorDevice):
|
|||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
result = await zha.safe_read(
|
||||
result = await helpers.safe_read(
|
||||
self._endpoint.out_clusters[OnOff.cluster_id],
|
||||
['on_off'],
|
||||
allow_cache=False,
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.const import (
|
|||
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
|
||||
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
|
||||
|
||||
REQUIREMENTS = ['blinkpy==0.10.3']
|
||||
REQUIREMENTS = ['blinkpy==0.11.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -111,7 +111,7 @@ def setup(hass, config):
|
|||
|
||||
def trigger_camera(call):
|
||||
"""Trigger a camera."""
|
||||
cameras = hass.data[BLINK_DATA].sync.cameras
|
||||
cameras = hass.data[BLINK_DATA].cameras
|
||||
name = call.data[CONF_NAME]
|
||||
if name in cameras:
|
||||
cameras[name].snap_picture()
|
||||
|
@ -148,7 +148,7 @@ async def async_handle_save_video_service(hass, call):
|
|||
|
||||
def _write_video(camera_name, video_path):
|
||||
"""Call video write."""
|
||||
all_cameras = hass.data[BLINK_DATA].sync.cameras
|
||||
all_cameras = hass.data[BLINK_DATA].cameras
|
||||
if camera_name in all_cameras:
|
||||
all_cameras[camera_name].video_to_file(video_path)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
return
|
||||
data = hass.data[BLINK_DATA]
|
||||
devs = []
|
||||
for name, camera in data.sync.cameras.items():
|
||||
for name, camera in data.cameras.items():
|
||||
devs.append(BlinkCamera(data, name, camera))
|
||||
|
||||
add_entities(devs)
|
||||
|
|
|
@ -60,13 +60,20 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
def extract_image_from_mjpeg(stream):
|
||||
"""Take in a MJPEG stream object, return the jpg from it."""
|
||||
data = b''
|
||||
|
||||
for chunk in stream:
|
||||
data += chunk
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
jpg_end = data.find(b'\xff\xd9')
|
||||
if jpg_start != -1 and jpg_end != -1:
|
||||
jpg = data[jpg_start:jpg_end + 2]
|
||||
return jpg
|
||||
|
||||
if jpg_end == -1:
|
||||
continue
|
||||
|
||||
jpg_start = data.find(b'\xff\xd8')
|
||||
|
||||
if jpg_start == -1:
|
||||
continue
|
||||
|
||||
return data[jpg_start:jpg_end + 2]
|
||||
|
||||
|
||||
class MjpegCamera(Camera):
|
||||
|
|
|
@ -10,14 +10,15 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE, \
|
||||
HTTP_HEADER_HA_AUTH
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
from . import async_get_still_stream
|
||||
from homeassistant.components.camera import async_get_still_stream
|
||||
|
||||
REQUIREMENTS = ['pillow==5.2.0']
|
||||
REQUIREMENTS = ['pillow==5.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -26,21 +27,34 @@ CONF_FORCE_RESIZE = 'force_resize'
|
|||
CONF_IMAGE_QUALITY = 'image_quality'
|
||||
CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
|
||||
CONF_MAX_IMAGE_WIDTH = 'max_image_width'
|
||||
CONF_MAX_IMAGE_HEIGHT = 'max_image_height'
|
||||
CONF_MAX_STREAM_WIDTH = 'max_stream_width'
|
||||
CONF_MAX_STREAM_HEIGHT = 'max_stream_height'
|
||||
CONF_IMAGE_TOP = 'image_top'
|
||||
CONF_IMAGE_LEFT = 'image_left'
|
||||
CONF_STREAM_QUALITY = 'stream_quality'
|
||||
|
||||
MODE_RESIZE = 'resize'
|
||||
MODE_CROP = 'crop'
|
||||
|
||||
DEFAULT_BASENAME = "Camera Proxy"
|
||||
DEFAULT_QUALITY = 75
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||
vol.Optional(CONF_MODE, default=MODE_RESIZE):
|
||||
vol.In([MODE_RESIZE, MODE_CROP]),
|
||||
vol.Optional(CONF_IMAGE_QUALITY): int,
|
||||
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||
vol.Optional(CONF_MAX_IMAGE_HEIGHT): int,
|
||||
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MAX_STREAM_HEIGHT): int,
|
||||
vol.Optional(CONF_IMAGE_LEFT): int,
|
||||
vol.Optional(CONF_IMAGE_TOP): int,
|
||||
vol.Optional(CONF_STREAM_QUALITY): int,
|
||||
})
|
||||
|
||||
|
@ -51,26 +65,37 @@ async def async_setup_platform(
|
|||
async_add_entities([ProxyCamera(hass, config)])
|
||||
|
||||
|
||||
def _precheck_image(image, opts):
|
||||
"""Perform some pre-checks on the given image."""
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
if not opts:
|
||||
raise ValueError()
|
||||
try:
|
||||
img = Image.open(io.BytesIO(image))
|
||||
except IOError:
|
||||
_LOGGER.warning("Failed to open image")
|
||||
raise ValueError()
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt not in ('PNG', 'JPEG'):
|
||||
_LOGGER.warning("Image is of unsupported type: %s", imgfmt)
|
||||
raise ValueError()
|
||||
return img
|
||||
|
||||
|
||||
def _resize_image(image, opts):
|
||||
"""Resize image."""
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
if not opts:
|
||||
try:
|
||||
img = _precheck_image(image, opts)
|
||||
except ValueError:
|
||||
return image
|
||||
|
||||
quality = opts.quality or DEFAULT_QUALITY
|
||||
new_width = opts.max_width
|
||||
|
||||
try:
|
||||
img = Image.open(io.BytesIO(image))
|
||||
except IOError:
|
||||
return image
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt not in ('PNG', 'JPEG'):
|
||||
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||
return image
|
||||
|
||||
(old_width, old_height) = img.size
|
||||
old_size = len(image)
|
||||
if old_width <= new_width:
|
||||
|
@ -87,7 +112,7 @@ def _resize_image(image, opts):
|
|||
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
|
||||
newimage = imgbuf.getvalue()
|
||||
if not opts.force_resize and len(newimage) >= old_size:
|
||||
_LOGGER.debug("Using original image(%d bytes) "
|
||||
_LOGGER.debug("Using original image (%d bytes) "
|
||||
"because resized image (%d bytes) is not smaller",
|
||||
old_size, len(newimage))
|
||||
return image
|
||||
|
@ -98,12 +123,50 @@ def _resize_image(image, opts):
|
|||
return newimage
|
||||
|
||||
|
||||
def _crop_image(image, opts):
|
||||
"""Crop image."""
|
||||
import io
|
||||
|
||||
try:
|
||||
img = _precheck_image(image, opts)
|
||||
except ValueError:
|
||||
return image
|
||||
|
||||
quality = opts.quality or DEFAULT_QUALITY
|
||||
(old_width, old_height) = img.size
|
||||
old_size = len(image)
|
||||
if opts.top is None:
|
||||
opts.top = 0
|
||||
if opts.left is None:
|
||||
opts.left = 0
|
||||
if opts.max_width is None or opts.max_width > old_width - opts.left:
|
||||
opts.max_width = old_width - opts.left
|
||||
if opts.max_height is None or opts.max_height > old_height - opts.top:
|
||||
opts.max_height = old_height - opts.top
|
||||
|
||||
img = img.crop((opts.left, opts.top,
|
||||
opts.left+opts.max_width, opts.top+opts.max_height))
|
||||
imgbuf = io.BytesIO()
|
||||
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
|
||||
newimage = imgbuf.getvalue()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Cropped image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
|
||||
old_width, old_height, old_size, opts.max_width, opts.max_height,
|
||||
len(newimage))
|
||||
return newimage
|
||||
|
||||
|
||||
class ImageOpts():
|
||||
"""The representation of image options."""
|
||||
|
||||
def __init__(self, max_width, quality, force_resize):
|
||||
def __init__(self, max_width, max_height, left, top,
|
||||
quality, force_resize):
|
||||
"""Initialize image options."""
|
||||
self.max_width = max_width
|
||||
self.max_height = max_height
|
||||
self.left = left
|
||||
self.top = top
|
||||
self.quality = quality
|
||||
self.force_resize = force_resize
|
||||
|
||||
|
@ -125,11 +188,18 @@ class ProxyCamera(Camera):
|
|||
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
|
||||
self._image_opts = ImageOpts(
|
||||
config.get(CONF_MAX_IMAGE_WIDTH),
|
||||
config.get(CONF_MAX_IMAGE_HEIGHT),
|
||||
config.get(CONF_IMAGE_LEFT),
|
||||
config.get(CONF_IMAGE_TOP),
|
||||
config.get(CONF_IMAGE_QUALITY),
|
||||
config.get(CONF_FORCE_RESIZE))
|
||||
|
||||
self._stream_opts = ImageOpts(
|
||||
config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY),
|
||||
config.get(CONF_MAX_STREAM_WIDTH),
|
||||
config.get(CONF_MAX_STREAM_HEIGHT),
|
||||
config.get(CONF_IMAGE_LEFT),
|
||||
config.get(CONF_IMAGE_TOP),
|
||||
config.get(CONF_STREAM_QUALITY),
|
||||
True)
|
||||
|
||||
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
|
@ -141,6 +211,7 @@ class ProxyCamera(Camera):
|
|||
self._headers = (
|
||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||
if self.hass.config.api.api_password is not None else None)
|
||||
self._mode = config.get(CONF_MODE)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return camera image."""
|
||||
|
@ -162,8 +233,12 @@ class ProxyCamera(Camera):
|
|||
_LOGGER.error("Error getting original camera image")
|
||||
return self._last_image
|
||||
|
||||
image = await self.hass.async_add_job(
|
||||
_resize_image, image.content, self._image_opts)
|
||||
if self._mode == MODE_RESIZE:
|
||||
job = _resize_image
|
||||
else:
|
||||
job = _crop_image
|
||||
image = await self.hass.async_add_executor_job(
|
||||
job, image.content, self._image_opts)
|
||||
|
||||
if self._cache_images:
|
||||
self._last_image = image
|
||||
|
@ -192,7 +267,11 @@ class ProxyCamera(Camera):
|
|||
if not image:
|
||||
return None
|
||||
except HomeAssistantError:
|
||||
raise asyncio.CancelledError
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
return await self.hass.async_add_job(
|
||||
_resize_image, image.content, self._stream_opts)
|
||||
if self._mode == MODE_RESIZE:
|
||||
job = _resize_image
|
||||
else:
|
||||
job = _crop_image
|
||||
return await self.hass.async_add_executor_job(
|
||||
job, image.content, self._stream_opts)
|
||||
|
|
|
@ -5,35 +5,33 @@ For more details about this platform, please refer to the documentation
|
|||
https://home-assistant.io/components/camera.push/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from collections import deque
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
|
||||
STATE_IDLE, STATE_RECORDING
|
||||
STATE_IDLE, STATE_RECORDING, DOMAIN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http.view import KEY_AUTHENTICATED,\
|
||||
HomeAssistantView
|
||||
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
|
||||
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
|
||||
from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['webhook']
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BUFFER_SIZE = 'buffer'
|
||||
CONF_IMAGE_FIELD = 'field'
|
||||
CONF_TOKEN = 'token'
|
||||
|
||||
DEFAULT_NAME = "Push Camera"
|
||||
|
||||
ATTR_FILENAME = 'filename'
|
||||
ATTR_LAST_TRIP = 'last_trip'
|
||||
ATTR_TOKEN = 'token'
|
||||
|
||||
PUSH_CAMERA_DATA = 'push_camera'
|
||||
|
||||
|
@ -43,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
|
||||
cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string,
|
||||
vol.Optional(CONF_TOKEN): vol.All(cv.string, vol.Length(min=8)),
|
||||
vol.Required(CONF_WEBHOOK_ID): cv.string,
|
||||
})
|
||||
|
||||
|
||||
|
@ -53,69 +51,43 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
if PUSH_CAMERA_DATA not in hass.data:
|
||||
hass.data[PUSH_CAMERA_DATA] = {}
|
||||
|
||||
cameras = [PushCamera(config[CONF_NAME],
|
||||
webhook_id = config.get(CONF_WEBHOOK_ID)
|
||||
|
||||
cameras = [PushCamera(hass,
|
||||
config[CONF_NAME],
|
||||
config[CONF_BUFFER_SIZE],
|
||||
config[CONF_TIMEOUT],
|
||||
config.get(CONF_TOKEN))]
|
||||
|
||||
hass.http.register_view(CameraPushReceiver(hass,
|
||||
config[CONF_IMAGE_FIELD]))
|
||||
config[CONF_IMAGE_FIELD],
|
||||
webhook_id)]
|
||||
|
||||
async_add_entities(cameras)
|
||||
|
||||
|
||||
class CameraPushReceiver(HomeAssistantView):
|
||||
"""Handle pushes from remote camera."""
|
||||
async def handle_webhook(hass, webhook_id, request):
|
||||
"""Handle incoming webhook POST with image files."""
|
||||
try:
|
||||
with async_timeout.timeout(5, loop=hass.loop):
|
||||
data = dict(await request.post())
|
||||
except (asyncio.TimeoutError, aiohttp.web.HTTPException) as error:
|
||||
_LOGGER.error("Could not get information from POST <%s>", error)
|
||||
return
|
||||
|
||||
url = "/api/camera_push/{entity_id}"
|
||||
name = 'api:camera_push:camera_entity'
|
||||
requires_auth = False
|
||||
camera = hass.data[PUSH_CAMERA_DATA][webhook_id]
|
||||
|
||||
def __init__(self, hass, image_field):
|
||||
"""Initialize CameraPushReceiver with camera entity."""
|
||||
self._cameras = hass.data[PUSH_CAMERA_DATA]
|
||||
self._image = image_field
|
||||
if camera.image_field not in data:
|
||||
_LOGGER.warning("Webhook call without POST parameter <%s>",
|
||||
camera.image_field)
|
||||
return
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Accept the POST from Camera."""
|
||||
_camera = self._cameras.get(entity_id)
|
||||
|
||||
if _camera is None:
|
||||
_LOGGER.error("Unknown %s", entity_id)
|
||||
status = HTTP_NOT_FOUND if request[KEY_AUTHENTICATED]\
|
||||
else HTTP_UNAUTHORIZED
|
||||
return self.json_message('Unknown {}'.format(entity_id),
|
||||
status)
|
||||
|
||||
# Supports HA authentication and token based
|
||||
# when token has been configured
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
(_camera.token is not None and
|
||||
request.query.get('token') == _camera.token))
|
||||
|
||||
if not authenticated:
|
||||
return self.json_message(
|
||||
'Invalid authorization credentials for {}'.format(entity_id),
|
||||
HTTP_UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
data = await request.post()
|
||||
_LOGGER.debug("Received Camera push: %s", data[self._image])
|
||||
await _camera.update_image(data[self._image].file.read(),
|
||||
data[self._image].filename)
|
||||
except ValueError as value_error:
|
||||
_LOGGER.error("Unknown value %s", value_error)
|
||||
return self.json_message('Invalid POST', HTTP_BAD_REQUEST)
|
||||
except KeyError as key_error:
|
||||
_LOGGER.error('In your POST message %s', key_error)
|
||||
return self.json_message('{} missing'.format(self._image),
|
||||
HTTP_BAD_REQUEST)
|
||||
await camera.update_image(data[camera.image_field].file.read(),
|
||||
data[camera.image_field].filename)
|
||||
|
||||
|
||||
class PushCamera(Camera):
|
||||
"""The representation of a Push camera."""
|
||||
|
||||
def __init__(self, name, buffer_size, timeout, token):
|
||||
def __init__(self, hass, name, buffer_size, timeout, image_field,
|
||||
webhook_id):
|
||||
"""Initialize push camera component."""
|
||||
super().__init__()
|
||||
self._name = name
|
||||
|
@ -126,11 +98,28 @@ class PushCamera(Camera):
|
|||
self._timeout = timeout
|
||||
self.queue = deque([], buffer_size)
|
||||
self._current_image = None
|
||||
self.token = token
|
||||
self._image_field = image_field
|
||||
self.webhook_id = webhook_id
|
||||
self.webhook_url = \
|
||||
hass.components.webhook.async_generate_url(webhook_id)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[PUSH_CAMERA_DATA][self.entity_id] = self
|
||||
self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self
|
||||
|
||||
try:
|
||||
self.hass.components.webhook.async_register(DOMAIN,
|
||||
self.name,
|
||||
self.webhook_id,
|
||||
handle_webhook)
|
||||
except ValueError:
|
||||
_LOGGER.error("In <%s>, webhook_id <%s> already used",
|
||||
self.name, self.webhook_id)
|
||||
|
||||
@property
|
||||
def image_field(self):
|
||||
"""HTTP field containing the image file."""
|
||||
return self._image_field
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
@ -189,6 +178,5 @@ class PushCamera(Camera):
|
|||
name: value for name, value in (
|
||||
(ATTR_LAST_TRIP, self._last_trip),
|
||||
(ATTR_FILENAME, self._filename),
|
||||
(ATTR_TOKEN, self.token),
|
||||
) if value is not None
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ import socket
|
|||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_PORT
|
||||
from homeassistant.const import CONF_PORT, CONF_SSL
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
REQUIREMENTS = ['uvcclient==0.10.1']
|
||||
REQUIREMENTS = ['uvcclient==0.11.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -25,12 +25,14 @@ CONF_PASSWORD = 'password'
|
|||
|
||||
DEFAULT_PASSWORD = 'ubnt'
|
||||
DEFAULT_PORT = 7080
|
||||
DEFAULT_SSL = False
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NVR): cv.string,
|
||||
vol.Required(CONF_KEY): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
|
@ -40,11 +42,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
key = config[CONF_KEY]
|
||||
password = config[CONF_PASSWORD]
|
||||
port = config[CONF_PORT]
|
||||
ssl = config[CONF_SSL]
|
||||
|
||||
from uvcclient import nvr
|
||||
try:
|
||||
# Exceptions may be raised in all method calls to the nvr library.
|
||||
nvrconn = nvr.UVCRemote(addr, port, key)
|
||||
nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl)
|
||||
cameras = nvrconn.index()
|
||||
|
||||
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'
|
||||
|
|
|
@ -192,6 +192,11 @@ class DaikinClimate(ClimateDevice):
|
|||
"""Return the name of the thermostat, if any."""
|
||||
return self._api.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._api.mac
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement which this thermostat uses."""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Support for Honeywell evohome (EMEA/EU-based systems only).
|
||||
"""Support for Climate devices of (EMEA/EU-based) Honeywell evohome systems.
|
||||
|
||||
Support for a temperature control system (TCS, controller) with 0+ heating
|
||||
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
|
||||
zones (e.g. TRVs, relays).
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.evohome/
|
||||
|
@ -13,29 +13,34 @@ import logging
|
|||
from requests.exceptions import HTTPError
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice,
|
||||
STATE_AUTO,
|
||||
STATE_ECO,
|
||||
STATE_OFF,
|
||||
SUPPORT_OPERATION_MODE,
|
||||
STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF,
|
||||
SUPPORT_AWAY_MODE,
|
||||
SUPPORT_ON_OFF,
|
||||
SUPPORT_OPERATION_MODE,
|
||||
SUPPORT_TARGET_TEMPERATURE,
|
||||
ClimateDevice
|
||||
)
|
||||
from homeassistant.components.evohome import (
|
||||
CONF_LOCATION_IDX,
|
||||
DATA_EVOHOME,
|
||||
MAX_TEMP,
|
||||
MIN_TEMP,
|
||||
SCAN_INTERVAL_MAX
|
||||
DATA_EVOHOME, DISPATCHER_EVOHOME,
|
||||
CONF_LOCATION_IDX, SCAN_INTERVAL_DEFAULT,
|
||||
EVO_PARENT, EVO_CHILD,
|
||||
GWS, TCS,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_SCAN_INTERVAL,
|
||||
PRECISION_TENTHS,
|
||||
TEMP_CELSIUS,
|
||||
HTTP_TOO_MANY_REQUESTS,
|
||||
PRECISION_HALVES,
|
||||
TEMP_CELSIUS
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
dispatcher_send,
|
||||
async_dispatcher_connect
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# these are for the controller's opmode/state and the zone's state
|
||||
# the Controller's opmode/state and the zone's (inherited) state
|
||||
EVO_RESET = 'AutoWithReset'
|
||||
EVO_AUTO = 'Auto'
|
||||
EVO_AUTOECO = 'AutoWithEco'
|
||||
|
@ -44,7 +49,14 @@ EVO_DAYOFF = 'DayOff'
|
|||
EVO_CUSTOM = 'Custom'
|
||||
EVO_HEATOFF = 'HeatingOff'
|
||||
|
||||
EVO_STATE_TO_HA = {
|
||||
# these are for Zones' opmode, and state
|
||||
EVO_FOLLOW = 'FollowSchedule'
|
||||
EVO_TEMPOVER = 'TemporaryOverride'
|
||||
EVO_PERMOVER = 'PermanentOverride'
|
||||
|
||||
# for the Controller. NB: evohome treats Away mode as a mode in/of itself,
|
||||
# where HA considers it to 'override' the exising operating mode
|
||||
TCS_STATE_TO_HA = {
|
||||
EVO_RESET: STATE_AUTO,
|
||||
EVO_AUTO: STATE_AUTO,
|
||||
EVO_AUTOECO: STATE_ECO,
|
||||
|
@ -53,171 +65,150 @@ EVO_STATE_TO_HA = {
|
|||
EVO_CUSTOM: STATE_AUTO,
|
||||
EVO_HEATOFF: STATE_OFF
|
||||
}
|
||||
|
||||
HA_STATE_TO_EVO = {
|
||||
HA_STATE_TO_TCS = {
|
||||
STATE_AUTO: EVO_AUTO,
|
||||
STATE_ECO: EVO_AUTOECO,
|
||||
STATE_OFF: EVO_HEATOFF
|
||||
}
|
||||
TCS_OP_LIST = list(HA_STATE_TO_TCS)
|
||||
|
||||
HA_OP_LIST = list(HA_STATE_TO_EVO)
|
||||
# the Zones' opmode; their state is usually 'inherited' from the TCS
|
||||
EVO_FOLLOW = 'FollowSchedule'
|
||||
EVO_TEMPOVER = 'TemporaryOverride'
|
||||
EVO_PERMOVER = 'PermanentOverride'
|
||||
|
||||
# these are used to help prevent E501 (line too long) violations
|
||||
GWS = 'gateways'
|
||||
TCS = 'temperatureControlSystems'
|
||||
|
||||
# debug codes - these happen occasionally, but the cause is unknown
|
||||
EVO_DEBUG_NO_RECENT_UPDATES = '0x01'
|
||||
EVO_DEBUG_NO_STATUS = '0x02'
|
||||
# for the Zones...
|
||||
ZONE_STATE_TO_HA = {
|
||||
EVO_FOLLOW: STATE_AUTO,
|
||||
EVO_TEMPOVER: STATE_MANUAL,
|
||||
EVO_PERMOVER: STATE_MANUAL
|
||||
}
|
||||
HA_STATE_TO_ZONE = {
|
||||
STATE_AUTO: EVO_FOLLOW,
|
||||
STATE_MANUAL: EVO_PERMOVER
|
||||
}
|
||||
ZONE_OP_LIST = list(HA_STATE_TO_ZONE)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system.
|
||||
|
||||
An evohome system consists of: a controller, with 0-12 heating zones (e.g.
|
||||
TRVs, relays) and, optionally, a DHW controller (a HW boiler).
|
||||
|
||||
Here, we add the controller only.
|
||||
"""
|
||||
async def async_setup_platform(hass, hass_config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Create the evohome Controller, and its Zones, if any."""
|
||||
evo_data = hass.data[DATA_EVOHOME]
|
||||
|
||||
client = evo_data['client']
|
||||
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
||||
|
||||
# evohomeclient has no defined way of accessing non-default location other
|
||||
# than using a protected member, such as below
|
||||
# evohomeclient has exposed no means of accessing non-default location
|
||||
# (i.e. loc_idx > 0) other than using a protected member, such as below
|
||||
tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access
|
||||
|
||||
_LOGGER.debug(
|
||||
"setup_platform(): Found Controller: id: %s [%s], type: %s",
|
||||
"setup_platform(): Found Controller, id=%s [%s], "
|
||||
"name=%s (location_idx=%s)",
|
||||
tcs_obj_ref.systemId,
|
||||
tcs_obj_ref.modelType,
|
||||
tcs_obj_ref.location.name,
|
||||
tcs_obj_ref.modelType
|
||||
loc_idx
|
||||
)
|
||||
parent = EvoController(evo_data, client, tcs_obj_ref)
|
||||
add_entities([parent], update_before_add=True)
|
||||
|
||||
controller = EvoController(evo_data, client, tcs_obj_ref)
|
||||
zones = []
|
||||
|
||||
for zone_idx in tcs_obj_ref.zones:
|
||||
zone_obj_ref = tcs_obj_ref.zones[zone_idx]
|
||||
_LOGGER.debug(
|
||||
"setup_platform(): Found Zone, id=%s [%s], "
|
||||
"name=%s",
|
||||
zone_obj_ref.zoneId,
|
||||
zone_obj_ref.zone_type,
|
||||
zone_obj_ref.name
|
||||
)
|
||||
zones.append(EvoZone(evo_data, client, zone_obj_ref))
|
||||
|
||||
entities = [controller] + zones
|
||||
|
||||
async_add_entities(entities, update_before_add=False)
|
||||
|
||||
|
||||
class EvoController(ClimateDevice):
|
||||
"""Base for a Honeywell evohome hub/Controller device.
|
||||
class EvoClimateDevice(ClimateDevice):
|
||||
"""Base for a Honeywell evohome Climate device."""
|
||||
|
||||
The Controller (aka TCS, temperature control system) is the parent of all
|
||||
the child (CH/DHW) devices.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
"""Initialize the evohome entity.
|
||||
|
||||
Most read-only properties are set here. So are pseudo read-only,
|
||||
for example name (which _could_ change between update()s).
|
||||
"""
|
||||
self.client = client
|
||||
"""Initialize the evohome entity."""
|
||||
self._client = client
|
||||
self._obj = obj_ref
|
||||
|
||||
self._id = obj_ref.systemId
|
||||
self._name = evo_data['config']['locationInfo']['name']
|
||||
|
||||
self._config = evo_data['config'][GWS][0][TCS][0]
|
||||
self._params = evo_data['params']
|
||||
self._timers = evo_data['timers']
|
||||
|
||||
self._timers['statusUpdated'] = datetime.min
|
||||
self._status = {}
|
||||
|
||||
self._available = False # should become True after first update()
|
||||
|
||||
def _handle_requests_exceptions(self, err):
|
||||
# evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.:
|
||||
# - HTTP_BAD_REQUEST, is usually Bad user credentials
|
||||
# - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded
|
||||
# - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect)
|
||||
|
||||
@callback
|
||||
def _connect(self, packet):
|
||||
if packet['to'] & self._type and packet['signal'] == 'refresh':
|
||||
self.async_schedule_update_ha_state(force_refresh=True)
|
||||
|
||||
def _handle_requests_exceptions(self, err):
|
||||
if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
|
||||
# execute a back off: pause, and reduce rate
|
||||
old_scan_interval = self._params[CONF_SCAN_INTERVAL]
|
||||
new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX)
|
||||
self._params[CONF_SCAN_INTERVAL] = new_scan_interval
|
||||
# execute a backoff: pause, and also reduce rate
|
||||
old_interval = self._params[CONF_SCAN_INTERVAL]
|
||||
new_interval = min(old_interval, SCAN_INTERVAL_DEFAULT) * 2
|
||||
self._params[CONF_SCAN_INTERVAL] = new_interval
|
||||
|
||||
_LOGGER.warning(
|
||||
"API rate limit has been exceeded: increasing '%s' from %s to "
|
||||
"%s seconds, and suspending polling for %s seconds.",
|
||||
"API rate limit has been exceeded. Suspending polling for %s "
|
||||
"seconds, and increasing '%s' from %s to %s seconds.",
|
||||
new_interval * 3,
|
||||
CONF_SCAN_INTERVAL,
|
||||
old_scan_interval,
|
||||
new_scan_interval,
|
||||
new_scan_interval * 3
|
||||
old_interval,
|
||||
new_interval,
|
||||
)
|
||||
|
||||
self._timers['statusUpdated'] = datetime.now() + \
|
||||
timedelta(seconds=new_scan_interval * 3)
|
||||
self._timers['statusUpdated'] = datetime.now() + new_interval * 3
|
||||
|
||||
else:
|
||||
raise err
|
||||
raise err # we dont handle any other HTTPErrors
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name to use in the frontend UI."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if the device is available.
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend UI."""
|
||||
return self._icon
|
||||
|
||||
All evohome entities are initially unavailable. Once HA has started,
|
||||
state data is then retrieved by the Controller, and then the children
|
||||
will get a state (e.g. operating_mode, current_temperature).
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes of the evohome Climate device.
|
||||
|
||||
However, evohome entities can become unavailable for other reasons.
|
||||
This is state data that is not available otherwise, due to the
|
||||
restrictions placed upon ClimateDevice properties, etc. by HA.
|
||||
"""
|
||||
return {'status': self._status}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if the device is currently available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Get the list of supported features of the Controller."""
|
||||
return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes of the controller.
|
||||
|
||||
This is operating mode state data that is not available otherwise, due
|
||||
to the restrictions placed upon ClimateDevice properties, etc by HA.
|
||||
"""
|
||||
data = {}
|
||||
data['systemMode'] = self._status['systemModeStatus']['mode']
|
||||
data['isPermanent'] = self._status['systemModeStatus']['isPermanent']
|
||||
if 'timeUntil' in self._status['systemModeStatus']:
|
||||
data['timeUntil'] = self._status['systemModeStatus']['timeUntil']
|
||||
data['activeFaults'] = self._status['activeFaults']
|
||||
return data
|
||||
"""Get the list of supported features of the device."""
|
||||
return self._supported_features
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operations."""
|
||||
return HA_OP_LIST
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the operation mode of the evohome entity."""
|
||||
return EVO_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the average target temperature of the Heating/DHW zones."""
|
||||
temps = [zone['setpointStatus']['targetHeatTemperature']
|
||||
for zone in self._status['zones']]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the average current temperature of the Heating/DHW zones."""
|
||||
tmp_list = [x for x in self._status['zones']
|
||||
if x['temperatureStatus']['isAvailable'] is True]
|
||||
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
return self._operation_list
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
|
@ -227,47 +218,313 @@ class EvoController(ClimateDevice):
|
|||
@property
|
||||
def precision(self):
|
||||
"""Return the temperature precision to use in the frontend UI."""
|
||||
return PRECISION_TENTHS
|
||||
return PRECISION_HALVES
|
||||
|
||||
|
||||
class EvoZone(EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome Zone device."""
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
"""Initialize the evohome Zone."""
|
||||
super().__init__(evo_data, client, obj_ref)
|
||||
|
||||
self._id = obj_ref.zoneId
|
||||
self._name = obj_ref.name
|
||||
self._icon = "mdi:radiator"
|
||||
self._type = EVO_CHILD
|
||||
|
||||
for _zone in evo_data['config'][GWS][0][TCS][0]['zones']:
|
||||
if _zone['zoneId'] == self._id:
|
||||
self._config = _zone
|
||||
break
|
||||
self._status = {}
|
||||
|
||||
self._operation_list = ZONE_OP_LIST
|
||||
self._supported_features = \
|
||||
SUPPORT_OPERATION_MODE | \
|
||||
SUPPORT_TARGET_TEMPERATURE | \
|
||||
SUPPORT_ON_OFF
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum target temp (setpoint) of a evohome entity."""
|
||||
return MIN_TEMP
|
||||
"""Return the minimum target temperature of a evohome Zone.
|
||||
|
||||
The default is 5 (in Celsius), but it is configurable within 5-35.
|
||||
"""
|
||||
return self._config['setpointCapabilities']['minHeatSetpoint']
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum target temp (setpoint) of a evohome entity."""
|
||||
return MAX_TEMP
|
||||
"""Return the minimum target temperature of a evohome Zone.
|
||||
|
||||
The default is 35 (in Celsius), but it is configurable within 5-35.
|
||||
"""
|
||||
return self._config['setpointCapabilities']['maxHeatSetpoint']
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true as evohome controllers are always on.
|
||||
def target_temperature(self):
|
||||
"""Return the target temperature of the evohome Zone."""
|
||||
return self._status['setpointStatus']['targetHeatTemperature']
|
||||
|
||||
Operating modes can include 'HeatingOff', but (for example) DHW would
|
||||
remain on.
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature of the evohome Zone."""
|
||||
return self._status['temperatureStatus']['temperature']
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operating mode of the evohome Zone.
|
||||
|
||||
The evohome Zones that are in 'FollowSchedule' mode inherit their
|
||||
actual operating mode from the Controller.
|
||||
"""
|
||||
evo_data = self.hass.data[DATA_EVOHOME]
|
||||
|
||||
system_mode = evo_data['status']['systemModeStatus']['mode']
|
||||
setpoint_mode = self._status['setpointStatus']['setpointMode']
|
||||
|
||||
if setpoint_mode == EVO_FOLLOW:
|
||||
# then inherit state from the controller
|
||||
if system_mode == EVO_RESET:
|
||||
current_operation = TCS_STATE_TO_HA.get(EVO_AUTO)
|
||||
else:
|
||||
current_operation = TCS_STATE_TO_HA.get(system_mode)
|
||||
else:
|
||||
current_operation = ZONE_STATE_TO_HA.get(setpoint_mode)
|
||||
|
||||
return current_operation
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the evohome Zone is off.
|
||||
|
||||
A Zone is considered off if its target temp is set to its minimum, and
|
||||
it is not following its schedule (i.e. not in 'FollowSchedule' mode).
|
||||
"""
|
||||
is_off = \
|
||||
self.target_temperature == self.min_temp and \
|
||||
self._status['setpointStatus']['setpointMode'] == EVO_PERMOVER
|
||||
return not is_off
|
||||
|
||||
def _set_temperature(self, temperature, until=None):
|
||||
"""Set the new target temperature of a Zone.
|
||||
|
||||
temperature is required, until can be:
|
||||
- strftime('%Y-%m-%dT%H:%M:%SZ') for TemporaryOverride, or
|
||||
- None for PermanentOverride (i.e. indefinitely)
|
||||
"""
|
||||
try:
|
||||
self._obj.set_temperature(temperature, until)
|
||||
except HTTPError as err:
|
||||
self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperature, indefinitely."""
|
||||
self._set_temperature(kwargs['temperature'], until=None)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn the evohome Zone on.
|
||||
|
||||
This is achieved by setting the Zone to its 'FollowSchedule' mode.
|
||||
"""
|
||||
self._set_operation_mode(EVO_FOLLOW)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn the evohome Zone off.
|
||||
|
||||
This is achieved by setting the Zone to its minimum temperature,
|
||||
indefinitely (i.e. 'PermanentOverride' mode).
|
||||
"""
|
||||
self._set_temperature(self.min_temp, until=None)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set an operating mode for a Zone.
|
||||
|
||||
Currently limited to 'Auto' & 'Manual'. If 'Off' is needed, it can be
|
||||
enabled via turn_off method.
|
||||
|
||||
NB: evohome Zones do not have an operating mode as understood by HA.
|
||||
Instead they usually 'inherit' an operating mode from their controller.
|
||||
|
||||
More correctly, these Zones are in a follow mode, 'FollowSchedule',
|
||||
where their setpoint temperatures are a function of their schedule, and
|
||||
the Controller's operating_mode, e.g. Economy mode is their scheduled
|
||||
setpoint less (usually) 3C.
|
||||
|
||||
Thus, you cannot set a Zone to Away mode, but the location (i.e. the
|
||||
Controller) is set to Away and each Zones's setpoints are adjusted
|
||||
accordingly to some lower temperature.
|
||||
|
||||
However, Zones can override these setpoints, either for a specified
|
||||
period of time, 'TemporaryOverride', after which they will revert back
|
||||
to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'.
|
||||
"""
|
||||
self._set_operation_mode(HA_STATE_TO_ZONE.get(operation_mode))
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
if operation_mode == EVO_FOLLOW:
|
||||
try:
|
||||
self._obj.cancel_temp_override(self._obj)
|
||||
except HTTPError as err:
|
||||
self._handle_exception("HTTPError", str(err)) # noqa: E501; pylint: disable=no-member
|
||||
|
||||
elif operation_mode == EVO_TEMPOVER:
|
||||
_LOGGER.error(
|
||||
"_set_operation_mode(op_mode=%s): mode not yet implemented",
|
||||
operation_mode
|
||||
)
|
||||
|
||||
elif operation_mode == EVO_PERMOVER:
|
||||
self._set_temperature(self.target_temperature, until=None)
|
||||
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"_set_operation_mode(op_mode=%s): mode not valid",
|
||||
operation_mode
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return False as evohome child devices should never be polled.
|
||||
|
||||
The evohome Controller will inform its children when to update().
|
||||
"""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
"""Process the evohome Zone's state data."""
|
||||
evo_data = self.hass.data[DATA_EVOHOME]
|
||||
|
||||
for _zone in evo_data['status']['zones']:
|
||||
if _zone['zoneId'] == self._id:
|
||||
self._status = _zone
|
||||
break
|
||||
|
||||
self._available = True
|
||||
|
||||
|
||||
class EvoController(EvoClimateDevice):
|
||||
"""Base for a Honeywell evohome hub/Controller device.
|
||||
|
||||
The Controller (aka TCS, temperature control system) is the parent of all
|
||||
the child (CH/DHW) devices. It is also a Climate device.
|
||||
"""
|
||||
|
||||
def __init__(self, evo_data, client, obj_ref):
|
||||
"""Initialize the evohome Controller (hub)."""
|
||||
super().__init__(evo_data, client, obj_ref)
|
||||
|
||||
self._id = obj_ref.systemId
|
||||
self._name = '_{}'.format(obj_ref.location.name)
|
||||
self._icon = "mdi:thermostat"
|
||||
self._type = EVO_PARENT
|
||||
|
||||
self._config = evo_data['config'][GWS][0][TCS][0]
|
||||
self._status = evo_data['status']
|
||||
self._timers['statusUpdated'] = datetime.min
|
||||
|
||||
self._operation_list = TCS_OP_LIST
|
||||
self._supported_features = \
|
||||
SUPPORT_OPERATION_MODE | \
|
||||
SUPPORT_AWAY_MODE
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes of the evohome Controller.
|
||||
|
||||
This is state data that is not available otherwise, due to the
|
||||
restrictions placed upon ClimateDevice properties, etc. by HA.
|
||||
"""
|
||||
status = dict(self._status)
|
||||
|
||||
if 'zones' in status:
|
||||
del status['zones']
|
||||
if 'dhw' in status:
|
||||
del status['dhw']
|
||||
|
||||
return {'status': status}
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return the current operating mode of the evohome Controller."""
|
||||
return TCS_STATE_TO_HA.get(self._status['systemModeStatus']['mode'])
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum target temperature of a evohome Controller.
|
||||
|
||||
Although evohome Controllers do not have a minimum target temp, one is
|
||||
expected by the HA schema; the default for an evohome HR92 is used.
|
||||
"""
|
||||
return 5
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the minimum target temperature of a evohome Controller.
|
||||
|
||||
Although evohome Controllers do not have a maximum target temp, one is
|
||||
expected by the HA schema; the default for an evohome HR92 is used.
|
||||
"""
|
||||
return 35
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the average target temperature of the Heating/DHW zones.
|
||||
|
||||
Although evohome Controllers do not have a target temp, one is
|
||||
expected by the HA schema.
|
||||
"""
|
||||
temps = [zone['setpointStatus']['targetHeatTemperature']
|
||||
for zone in self._status['zones']]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the average current temperature of the Heating/DHW zones.
|
||||
|
||||
Although evohome Controllers do not have a target temp, one is
|
||||
expected by the HA schema.
|
||||
"""
|
||||
tmp_list = [x for x in self._status['zones']
|
||||
if x['temperatureStatus']['isAvailable'] is True]
|
||||
temps = [zone['temperatureStatus']['temperature'] for zone in tmp_list]
|
||||
|
||||
avg_temp = round(sum(temps) / len(temps), 1) if temps else None
|
||||
return avg_temp
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True as evohome Controllers are always on.
|
||||
|
||||
For example, evohome Controllers have a 'HeatingOff' mode, but even
|
||||
then the DHW would remain on.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
"""Return true if away mode is on."""
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return True if away mode is on."""
|
||||
return self._status['systemModeStatus']['mode'] == EVO_AWAY
|
||||
|
||||
def turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
"""Turn away mode on.
|
||||
|
||||
The evohome Controller will not remember is previous operating mode.
|
||||
"""
|
||||
self._set_operation_mode(EVO_AWAY)
|
||||
|
||||
def turn_away_mode_off(self):
|
||||
"""Turn away mode off."""
|
||||
"""Turn away mode off.
|
||||
|
||||
The evohome Controller can not recall its previous operating mode (as
|
||||
intimated by the HA schema), so this method is achieved by setting the
|
||||
Controller's mode back to Auto.
|
||||
"""
|
||||
self._set_operation_mode(EVO_AUTO)
|
||||
|
||||
def _set_operation_mode(self, operation_mode):
|
||||
# Set new target operation mode for the TCS.
|
||||
_LOGGER.debug(
|
||||
"_set_operation_mode(): API call [1 request(s)]: "
|
||||
"tcs._set_status(%s)...",
|
||||
operation_mode
|
||||
)
|
||||
try:
|
||||
self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access
|
||||
except HTTPError as err:
|
||||
|
@ -279,93 +536,45 @@ class EvoController(ClimateDevice):
|
|||
Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away'
|
||||
mode is needed, it can be enabled via turn_away_mode_on method.
|
||||
"""
|
||||
self._set_operation_mode(HA_STATE_TO_EVO.get(operation_mode))
|
||||
self._set_operation_mode(HA_STATE_TO_TCS.get(operation_mode))
|
||||
|
||||
def _update_state_data(self, evo_data):
|
||||
client = evo_data['client']
|
||||
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
||||
|
||||
_LOGGER.debug(
|
||||
"_update_state_data(): API call [1 request(s)]: "
|
||||
"client.locations[loc_idx].status()..."
|
||||
)
|
||||
|
||||
try:
|
||||
evo_data['status'].update(
|
||||
client.locations[loc_idx].status()[GWS][0][TCS][0])
|
||||
except HTTPError as err: # check if we've exceeded the api rate limit
|
||||
self._handle_requests_exceptions(err)
|
||||
else:
|
||||
evo_data['timers']['statusUpdated'] = datetime.now()
|
||||
|
||||
_LOGGER.debug(
|
||||
"_update_state_data(): evo_data['status'] = %s",
|
||||
evo_data['status']
|
||||
)
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True as the evohome Controller should always be polled."""
|
||||
return True
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state data of the installation.
|
||||
"""Get the latest state data of the entire evohome Location.
|
||||
|
||||
This includes state data for the Controller and its child devices, such
|
||||
as the operating_mode of the Controller and the current_temperature
|
||||
of its children.
|
||||
|
||||
This is not asyncio-friendly due to the underlying client api.
|
||||
This includes state data for the Controller and all its child devices,
|
||||
such as the operating mode of the Controller and the current temp of
|
||||
its children (e.g. Zones, DHW controller).
|
||||
"""
|
||||
evo_data = self.hass.data[DATA_EVOHOME]
|
||||
|
||||
# should the latest evohome state data be retreived this cycle?
|
||||
timeout = datetime.now() + timedelta(seconds=55)
|
||||
expired = timeout > self._timers['statusUpdated'] + \
|
||||
timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL])
|
||||
self._params[CONF_SCAN_INTERVAL]
|
||||
|
||||
if not expired:
|
||||
return
|
||||
|
||||
was_available = self._available or \
|
||||
self._timers['statusUpdated'] == datetime.min
|
||||
|
||||
self._update_state_data(evo_data)
|
||||
self._status = evo_data['status']
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
tmp_dict = dict(self._status)
|
||||
if 'zones' in tmp_dict:
|
||||
tmp_dict['zones'] = '...'
|
||||
if 'dhw' in tmp_dict:
|
||||
tmp_dict['dhw'] = '...'
|
||||
|
||||
_LOGGER.debug(
|
||||
"update(%s), self._status = %s",
|
||||
self._id + " [" + self._name + "]",
|
||||
tmp_dict
|
||||
)
|
||||
|
||||
no_recent_updates = self._timers['statusUpdated'] < datetime.now() - \
|
||||
timedelta(seconds=self._params[CONF_SCAN_INTERVAL] * 3.1)
|
||||
|
||||
if no_recent_updates:
|
||||
self._available = False
|
||||
debug_code = EVO_DEBUG_NO_RECENT_UPDATES
|
||||
|
||||
elif not self._status:
|
||||
# unavailable because no status (but how? other than at startup?)
|
||||
self._available = False
|
||||
debug_code = EVO_DEBUG_NO_STATUS
|
||||
# Retreive the latest state data via the client api
|
||||
loc_idx = self._params[CONF_LOCATION_IDX]
|
||||
|
||||
try:
|
||||
self._status.update(
|
||||
self._client.locations[loc_idx].status()[GWS][0][TCS][0])
|
||||
except HTTPError as err: # check if we've exceeded the api rate limit
|
||||
self._handle_requests_exceptions(err)
|
||||
else:
|
||||
self._timers['statusUpdated'] = datetime.now()
|
||||
self._available = True
|
||||
|
||||
if not self._available and was_available:
|
||||
# only warn if available went from True to False
|
||||
_LOGGER.warning(
|
||||
"The entity, %s, has become unavailable, debug code is: %s",
|
||||
self._id + " [" + self._name + "]",
|
||||
debug_code
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"_update_state_data(): self._status = %s",
|
||||
self._status
|
||||
)
|
||||
|
||||
elif self._available and not was_available:
|
||||
# this isn't the first re-available (e.g. _after_ STARTUP)
|
||||
_LOGGER.debug(
|
||||
"The entity, %s, has become available",
|
||||
self._id + " [" + self._name + "]"
|
||||
)
|
||||
# inform the child devices that state data has been updated
|
||||
pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD}
|
||||
dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt)
|
||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.helpers import condition
|
|||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_time_interval)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities,
|
|||
precision)])
|
||||
|
||||
|
||||
class GenericThermostat(ClimateDevice):
|
||||
class GenericThermostat(ClimateDevice, RestoreEntity):
|
||||
"""Representation of a Generic Thermostat device."""
|
||||
|
||||
def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
|
||||
|
@ -155,8 +155,9 @@ class GenericThermostat(ClimateDevice):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when entity about to be added."""
|
||||
await super().async_added_to_hass()
|
||||
# Check If we have an old state
|
||||
old_state = await async_get_last_state(self.hass, self.entity_id)
|
||||
old_state = await self.async_get_last_state()
|
||||
if old_state is not None:
|
||||
# If we have no initial temperature, restore
|
||||
if self._target_temp is None:
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
|||
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
|
||||
ATTR_TEMPERATURE, CONF_REGION)
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.7', 'somecomfort==0.5.2']
|
||||
REQUIREMENTS = ['evohomeclient==0.2.8', 'somecomfort==0.5.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
|||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
REQUIREMENTS = ['millheater==0.2.8']
|
||||
REQUIREMENTS = ['millheater==0.2.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -22,7 +22,8 @@ from homeassistant.const import (
|
|||
from homeassistant.components.mqtt import (
|
||||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate)
|
||||
MQTT_BASE_PLATFORM_SCHEMA, MqttAvailability, MqttDiscoveryUpdate,
|
||||
subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -77,6 +78,18 @@ CONF_MIN_TEMP = 'min_temp'
|
|||
CONF_MAX_TEMP = 'max_temp'
|
||||
CONF_TEMP_STEP = 'temp_step'
|
||||
|
||||
TEMPLATE_KEYS = (
|
||||
CONF_POWER_STATE_TEMPLATE,
|
||||
CONF_MODE_STATE_TEMPLATE,
|
||||
CONF_TEMPERATURE_STATE_TEMPLATE,
|
||||
CONF_FAN_MODE_STATE_TEMPLATE,
|
||||
CONF_SWING_MODE_STATE_TEMPLATE,
|
||||
CONF_AWAY_MODE_STATE_TEMPLATE,
|
||||
CONF_HOLD_STATE_TEMPLATE,
|
||||
CONF_AUX_STATE_TEMPLATE,
|
||||
CONF_CURRENT_TEMPERATURE_TEMPLATE
|
||||
)
|
||||
|
||||
SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
|
||||
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
|
||||
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
|
@ -153,69 +166,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
"""Set up the MQTT climate devices."""
|
||||
template_keys = (
|
||||
CONF_POWER_STATE_TEMPLATE,
|
||||
CONF_MODE_STATE_TEMPLATE,
|
||||
CONF_TEMPERATURE_STATE_TEMPLATE,
|
||||
CONF_FAN_MODE_STATE_TEMPLATE,
|
||||
CONF_SWING_MODE_STATE_TEMPLATE,
|
||||
CONF_AWAY_MODE_STATE_TEMPLATE,
|
||||
CONF_HOLD_STATE_TEMPLATE,
|
||||
CONF_AUX_STATE_TEMPLATE,
|
||||
CONF_CURRENT_TEMPERATURE_TEMPLATE
|
||||
)
|
||||
value_templates = {}
|
||||
if CONF_VALUE_TEMPLATE in config:
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = hass
|
||||
value_templates = {key: value_template for key in template_keys}
|
||||
for key in template_keys & config.keys():
|
||||
value_templates[key] = config.get(key)
|
||||
value_templates[key].hass = hass
|
||||
|
||||
async_add_entities([
|
||||
MqttClimate(
|
||||
hass,
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_SWING_MODE_COMMAND_TOPIC,
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC,
|
||||
CONF_HOLD_COMMAND_TOPIC,
|
||||
CONF_AUX_COMMAND_TOPIC,
|
||||
CONF_POWER_STATE_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
CONF_AWAY_MODE_STATE_TOPIC,
|
||||
CONF_HOLD_STATE_TOPIC,
|
||||
CONF_AUX_STATE_TOPIC,
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC
|
||||
)
|
||||
},
|
||||
value_templates,
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_MODE_LIST),
|
||||
config.get(CONF_FAN_MODE_LIST),
|
||||
config.get(CONF_SWING_MODE_LIST),
|
||||
config.get(CONF_INITIAL),
|
||||
False, None, SPEED_LOW,
|
||||
STATE_OFF, STATE_OFF, False,
|
||||
config.get(CONF_SEND_IF_OFF),
|
||||
config.get(CONF_PAYLOAD_ON),
|
||||
config.get(CONF_PAYLOAD_OFF),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_MIN_TEMP),
|
||||
config.get(CONF_MAX_TEMP),
|
||||
config.get(CONF_TEMP_STEP),
|
||||
config,
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
|
@ -223,54 +177,103 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
|||
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
||||
"""Representation of an MQTT climate device."""
|
||||
|
||||
def __init__(self, hass, name, topic, value_templates, qos, retain,
|
||||
mode_list, fan_mode_list, swing_mode_list,
|
||||
target_temperature, away, hold, current_fan_mode,
|
||||
current_swing_mode, current_operation, aux, send_if_off,
|
||||
payload_on, payload_off, availability_topic,
|
||||
payload_available, payload_not_available,
|
||||
min_temp, max_temp, temp_step, discovery_hash):
|
||||
def __init__(self, hass, config, discovery_hash):
|
||||
"""Initialize the climate device."""
|
||||
self._config = config
|
||||
self._sub_state = None
|
||||
|
||||
self.hass = hass
|
||||
self._topic = None
|
||||
self._value_templates = None
|
||||
self._target_temperature = None
|
||||
self._current_fan_mode = None
|
||||
self._current_operation = None
|
||||
self._current_swing_mode = None
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = False
|
||||
self._hold = None
|
||||
self._current_temperature = None
|
||||
self._aux = False
|
||||
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
qos = config.get(CONF_QOS)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._value_templates = value_templates
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
await super().async_added_to_hass()
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._config = config
|
||||
self._setup_from_config(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._topic = {
|
||||
key: config.get(key) for key in (
|
||||
CONF_POWER_COMMAND_TOPIC,
|
||||
CONF_MODE_COMMAND_TOPIC,
|
||||
CONF_TEMPERATURE_COMMAND_TOPIC,
|
||||
CONF_FAN_MODE_COMMAND_TOPIC,
|
||||
CONF_SWING_MODE_COMMAND_TOPIC,
|
||||
CONF_AWAY_MODE_COMMAND_TOPIC,
|
||||
CONF_HOLD_COMMAND_TOPIC,
|
||||
CONF_AUX_COMMAND_TOPIC,
|
||||
CONF_POWER_STATE_TOPIC,
|
||||
CONF_MODE_STATE_TOPIC,
|
||||
CONF_TEMPERATURE_STATE_TOPIC,
|
||||
CONF_FAN_MODE_STATE_TOPIC,
|
||||
CONF_SWING_MODE_STATE_TOPIC,
|
||||
CONF_AWAY_MODE_STATE_TOPIC,
|
||||
CONF_HOLD_STATE_TOPIC,
|
||||
CONF_AUX_STATE_TOPIC,
|
||||
CONF_CURRENT_TEMPERATURE_TOPIC
|
||||
)
|
||||
}
|
||||
|
||||
# set to None in non-optimistic mode
|
||||
self._target_temperature = self._current_fan_mode = \
|
||||
self._current_operation = self._current_swing_mode = None
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
|
||||
self._target_temperature = target_temperature
|
||||
self._unit_of_measurement = hass.config.units.temperature_unit
|
||||
self._away = away
|
||||
self._hold = hold
|
||||
self._current_temperature = None
|
||||
self._target_temperature = config.get(CONF_INITIAL)
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = current_fan_mode
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = current_operation
|
||||
self._aux = aux
|
||||
self._current_fan_mode = SPEED_LOW
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
self._current_swing_mode = current_swing_mode
|
||||
self._fan_list = fan_mode_list
|
||||
self._operation_list = mode_list
|
||||
self._swing_list = swing_mode_list
|
||||
self._target_temperature_step = temp_step
|
||||
self._send_if_off = send_if_off
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._min_temp = min_temp
|
||||
self._max_temp = max_temp
|
||||
self._discovery_hash = discovery_hash
|
||||
self._current_swing_mode = STATE_OFF
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = STATE_OFF
|
||||
self._away = False
|
||||
self._hold = None
|
||||
self._aux = False
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle being added to home assistant."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
value_templates = {}
|
||||
if CONF_VALUE_TEMPLATE in config:
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template.hass = self.hass
|
||||
value_templates = {key: value_template for key in TEMPLATE_KEYS}
|
||||
for key in TEMPLATE_KEYS & config.keys():
|
||||
value_templates[key] = config.get(key)
|
||||
value_templates[key].hass = self.hass
|
||||
self._value_templates = value_templates
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
qos = self._config.get(CONF_QOS)
|
||||
|
||||
@callback
|
||||
def handle_current_temp_received(topic, payload, qos):
|
||||
|
@ -287,9 +290,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
handle_current_temp_received, self._qos)
|
||||
topics[CONF_CURRENT_TEMPERATURE_TOPIC] = {
|
||||
'topic': self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
|
||||
'msg_callback': handle_current_temp_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_mode_received(topic, payload, qos):
|
||||
|
@ -298,16 +302,17 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\
|
||||
async_render_with_possible_json_value(payload)
|
||||
|
||||
if payload not in self._operation_list:
|
||||
if payload not in self._config.get(CONF_MODE_LIST):
|
||||
_LOGGER.error("Invalid mode: %s", payload)
|
||||
else:
|
||||
self._current_operation = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_MODE_STATE_TOPIC],
|
||||
handle_mode_received, self._qos)
|
||||
topics[CONF_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_temperature_received(topic, payload, qos):
|
||||
|
@ -324,9 +329,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
_LOGGER.error("Could not parse temperature from %s", payload)
|
||||
|
||||
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
handle_temperature_received, self._qos)
|
||||
topics[CONF_TEMPERATURE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_TEMPERATURE_STATE_TOPIC],
|
||||
'msg_callback': handle_temperature_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_fan_mode_received(topic, payload, qos):
|
||||
|
@ -336,16 +342,17 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self._value_templates[CONF_FAN_MODE_STATE_TEMPLATE].\
|
||||
async_render_with_possible_json_value(payload)
|
||||
|
||||
if payload not in self._fan_list:
|
||||
if payload not in self._config.get(CONF_FAN_MODE_LIST):
|
||||
_LOGGER.error("Invalid fan mode: %s", payload)
|
||||
else:
|
||||
self._current_fan_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
handle_fan_mode_received, self._qos)
|
||||
topics[CONF_FAN_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_FAN_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_fan_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_swing_mode_received(topic, payload, qos):
|
||||
|
@ -355,32 +362,35 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self._value_templates[CONF_SWING_MODE_STATE_TEMPLATE].\
|
||||
async_render_with_possible_json_value(payload)
|
||||
|
||||
if payload not in self._swing_list:
|
||||
if payload not in self._config.get(CONF_SWING_MODE_LIST):
|
||||
_LOGGER.error("Invalid swing mode: %s", payload)
|
||||
else:
|
||||
self._current_swing_mode = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
handle_swing_mode_received, self._qos)
|
||||
topics[CONF_SWING_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_SWING_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_swing_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_away_mode_received(topic, payload, qos):
|
||||
"""Handle receiving away mode via MQTT."""
|
||||
payload_on = self._config.get(CONF_PAYLOAD_ON)
|
||||
payload_off = self._config.get(CONF_PAYLOAD_OFF)
|
||||
if CONF_AWAY_MODE_STATE_TEMPLATE in self._value_templates:
|
||||
payload = \
|
||||
self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\
|
||||
async_render_with_possible_json_value(payload)
|
||||
if payload == "True":
|
||||
payload = self._payload_on
|
||||
payload = payload_on
|
||||
elif payload == "False":
|
||||
payload = self._payload_off
|
||||
payload = payload_off
|
||||
|
||||
if payload == self._payload_on:
|
||||
if payload == payload_on:
|
||||
self._away = True
|
||||
elif payload == self._payload_off:
|
||||
elif payload == payload_off:
|
||||
self._away = False
|
||||
else:
|
||||
_LOGGER.error("Invalid away mode: %s", payload)
|
||||
|
@ -388,24 +398,27 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
handle_away_mode_received, self._qos)
|
||||
topics[CONF_AWAY_MODE_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_AWAY_MODE_STATE_TOPIC],
|
||||
'msg_callback': handle_away_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_aux_mode_received(topic, payload, qos):
|
||||
"""Handle receiving aux mode via MQTT."""
|
||||
payload_on = self._config.get(CONF_PAYLOAD_ON)
|
||||
payload_off = self._config.get(CONF_PAYLOAD_OFF)
|
||||
if CONF_AUX_STATE_TEMPLATE in self._value_templates:
|
||||
payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\
|
||||
async_render_with_possible_json_value(payload)
|
||||
if payload == "True":
|
||||
payload = self._payload_on
|
||||
payload = payload_on
|
||||
elif payload == "False":
|
||||
payload = self._payload_off
|
||||
payload = payload_off
|
||||
|
||||
if payload == self._payload_on:
|
||||
if payload == payload_on:
|
||||
self._aux = True
|
||||
elif payload == self._payload_off:
|
||||
elif payload == payload_off:
|
||||
self._aux = False
|
||||
else:
|
||||
_LOGGER.error("Invalid aux mode: %s", payload)
|
||||
|
@ -413,9 +426,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_AUX_STATE_TOPIC],
|
||||
handle_aux_mode_received, self._qos)
|
||||
topics[CONF_AUX_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_AUX_STATE_TOPIC],
|
||||
'msg_callback': handle_aux_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
@callback
|
||||
def handle_hold_mode_received(topic, payload, qos):
|
||||
|
@ -428,9 +442,19 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
handle_hold_mode_received, self._qos)
|
||||
topics[CONF_HOLD_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_HOLD_STATE_TOPIC],
|
||||
'msg_callback': handle_hold_mode_received,
|
||||
'qos': qos}
|
||||
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
topics)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -440,7 +464,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
|
@ -465,12 +489,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_list
|
||||
return self._config.get(CONF_MODE_LIST)
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._target_temperature_step
|
||||
return self._config.get(CONF_TEMP_STEP)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self):
|
||||
|
@ -495,7 +519,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
@property
|
||||
def fan_list(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_list
|
||||
return self._config.get(CONF_FAN_MODE_LIST)
|
||||
|
||||
async def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
|
@ -508,19 +532,23 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
# optimistic mode
|
||||
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
if (self._config.get(CONF_SEND_IF_OFF) or
|
||||
self._current_operation != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC],
|
||||
kwargs.get(ATTR_TEMPERATURE), self._qos, self._retain)
|
||||
kwargs.get(ATTR_TEMPERATURE), self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set new swing mode."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
if (self._config.get(CONF_SEND_IF_OFF) or
|
||||
self._current_operation != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC],
|
||||
swing_mode, self._qos, self._retain)
|
||||
swing_mode, self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
|
||||
self._current_swing_mode = swing_mode
|
||||
|
@ -528,10 +556,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set new target temperature."""
|
||||
if self._send_if_off or self._current_operation != STATE_OFF:
|
||||
if (self._config.get(CONF_SEND_IF_OFF) or
|
||||
self._current_operation != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
|
||||
fan_mode, self._qos, self._retain)
|
||||
fan_mode, self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
|
||||
self._current_fan_mode = fan_mode
|
||||
|
@ -539,22 +569,24 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
|
||||
async def async_set_operation_mode(self, operation_mode) -> None:
|
||||
"""Set new operation mode."""
|
||||
qos = self._config.get(CONF_QOS)
|
||||
retain = self._config.get(CONF_RETAIN)
|
||||
if self._topic[CONF_POWER_COMMAND_TOPIC] is not None:
|
||||
if (self._current_operation == STATE_OFF and
|
||||
operation_mode != STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_ON), qos, retain)
|
||||
elif (self._current_operation != STATE_OFF and
|
||||
operation_mode == STATE_OFF):
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_OFF), qos, retain)
|
||||
|
||||
if self._topic[CONF_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_MODE_COMMAND_TOPIC],
|
||||
operation_mode, self._qos, self._retain)
|
||||
operation_mode, qos, retain)
|
||||
|
||||
if self._topic[CONF_MODE_STATE_TOPIC] is None:
|
||||
self._current_operation = operation_mode
|
||||
|
@ -568,14 +600,16 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
@property
|
||||
def swing_list(self):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_list
|
||||
return self._config.get(CONF_SWING_MODE_LIST)
|
||||
|
||||
async def async_turn_away_mode_on(self):
|
||||
"""Turn away mode on."""
|
||||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_ON),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = True
|
||||
|
@ -586,7 +620,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_OFF),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
|
||||
self._away = False
|
||||
|
@ -597,7 +633,8 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass,
|
||||
self._topic[CONF_HOLD_COMMAND_TOPIC],
|
||||
hold_mode, self._qos, self._retain)
|
||||
hold_mode, self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
|
||||
self._hold = hold_mode
|
||||
|
@ -607,7 +644,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
"""Turn auxiliary heater on."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_on, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_ON),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = True
|
||||
|
@ -617,7 +656,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
"""Turn auxiliary heater off."""
|
||||
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
|
||||
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
|
||||
self._payload_off, self._qos, self._retain)
|
||||
self._config.get(CONF_PAYLOAD_OFF),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._topic[CONF_AUX_STATE_TOPIC] is None:
|
||||
self._aux = False
|
||||
|
@ -661,9 +702,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
|
|||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self._min_temp
|
||||
return self._config.get(CONF_MIN_TEMP)
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self._max_temp
|
||||
return self._config.get(CONF_MAX_TEMP)
|
||||
|
|
|
@ -15,6 +15,14 @@ from homeassistant.const import TEMP_CELSIUS
|
|||
|
||||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
|
||||
|
||||
HA_TOON = {
|
||||
STATE_AUTO: 'Comfort',
|
||||
STATE_HEAT: 'Home',
|
||||
STATE_ECO: 'Away',
|
||||
STATE_COOL: 'Sleep',
|
||||
}
|
||||
TOON_HA = {value: key for key, value in HA_TOON.items()}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Toon climate device."""
|
||||
|
@ -58,8 +66,7 @@ class ThermostatDevice(ClimateDevice):
|
|||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation i.e. comfort, home, away."""
|
||||
state = self.thermos.get_data('state')
|
||||
return state
|
||||
return TOON_HA.get(self.thermos.get_data('state'))
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
|
@ -83,14 +90,7 @@ class ThermostatDevice(ClimateDevice):
|
|||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set new operation mode."""
|
||||
toonlib_values = {
|
||||
STATE_AUTO: 'Comfort',
|
||||
STATE_HEAT: 'Home',
|
||||
STATE_ECO: 'Away',
|
||||
STATE_COOL: 'Sleep',
|
||||
}
|
||||
|
||||
self.thermos.set_state(toonlib_values[operation_mode])
|
||||
self.thermos.set_state(HA_TOON[operation_mode])
|
||||
|
||||
def update(self):
|
||||
"""Update local state."""
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.components.alexa import smart_home as alexa_sh
|
|||
from homeassistant.components.google_assistant import helpers as ga_h
|
||||
from homeassistant.components.google_assistant import const as ga_c
|
||||
|
||||
from . import http_api, iot, auth_api, prefs
|
||||
from . import http_api, iot, auth_api, prefs, cloudhooks
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
REQUIREMENTS = ['warrant==0.6.1']
|
||||
|
@ -37,6 +37,7 @@ CONF_RELAYER = 'relayer'
|
|||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
|
||||
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
|
||||
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'
|
||||
|
||||
DEFAULT_MODE = 'production'
|
||||
DEPENDENCIES = ['http']
|
||||
|
@ -78,6 +79,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
|
||||
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
|
||||
vol.Optional(CONF_CLOUDHOOK_CREATE_URL): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
}),
|
||||
|
@ -113,7 +115,7 @@ class Cloud:
|
|||
def __init__(self, hass, mode, alexa, google_actions,
|
||||
cognito_client_id=None, user_pool_id=None, region=None,
|
||||
relayer=None, google_actions_sync_url=None,
|
||||
subscription_info_url=None):
|
||||
subscription_info_url=None, cloudhook_create_url=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
|
@ -125,6 +127,7 @@ class Cloud:
|
|||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
self.iot = iot.CloudIoT(self)
|
||||
self.cloudhooks = cloudhooks.Cloudhooks(self)
|
||||
|
||||
if mode == MODE_DEV:
|
||||
self.cognito_client_id = cognito_client_id
|
||||
|
@ -133,6 +136,7 @@ class Cloud:
|
|||
self.relayer = relayer
|
||||
self.google_actions_sync_url = google_actions_sync_url
|
||||
self.subscription_info_url = subscription_info_url
|
||||
self.cloudhook_create_url = cloudhook_create_url
|
||||
|
||||
else:
|
||||
info = SERVERS[mode]
|
||||
|
@ -143,6 +147,7 @@ class Cloud:
|
|||
self.relayer = info['relayer']
|
||||
self.google_actions_sync_url = info['google_actions_sync_url']
|
||||
self.subscription_info_url = info['subscription_info_url']
|
||||
self.cloudhook_create_url = info['cloudhook_create_url']
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
|
@ -247,8 +252,7 @@ class Cloud:
|
|||
return json.loads(file.read())
|
||||
|
||||
info = await self.hass.async_add_job(load_config)
|
||||
|
||||
await self.prefs.async_initialize(bool(info))
|
||||
await self.prefs.async_initialize()
|
||||
|
||||
if info is None:
|
||||
return
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
"""Cloud APIs."""
|
||||
from functools import wraps
|
||||
import logging
|
||||
|
||||
from . import auth_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_token(func):
|
||||
"""Decorate a function to verify valid token."""
|
||||
@wraps(func)
|
||||
async def check_token(cloud, *args):
|
||||
"""Validate token, then call func."""
|
||||
await cloud.hass.async_add_executor_job(auth_api.check_token, cloud)
|
||||
return await func(cloud, *args)
|
||||
|
||||
return check_token
|
||||
|
||||
|
||||
def _log_response(func):
|
||||
"""Decorate a function to log bad responses."""
|
||||
@wraps(func)
|
||||
async def log_response(*args):
|
||||
"""Log response if it's bad."""
|
||||
resp = await func(*args)
|
||||
meth = _LOGGER.debug if resp.status < 400 else _LOGGER.warning
|
||||
meth('Fetched %s (%s)', resp.url, resp.status)
|
||||
return resp
|
||||
|
||||
return log_response
|
||||
|
||||
|
||||
@_check_token
|
||||
@_log_response
|
||||
async def async_create_cloudhook(cloud):
|
||||
"""Create a cloudhook."""
|
||||
websession = cloud.hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
return await websession.post(
|
||||
cloud.cloudhook_create_url, headers={
|
||||
'authorization': cloud.id_token
|
||||
})
|
|
@ -0,0 +1,66 @@
|
|||
"""Manage cloud cloudhooks."""
|
||||
import async_timeout
|
||||
|
||||
from . import cloud_api
|
||||
|
||||
|
||||
class Cloudhooks:
|
||||
"""Class to help manage cloudhooks."""
|
||||
|
||||
def __init__(self, cloud):
|
||||
"""Initialize cloudhooks."""
|
||||
self.cloud = cloud
|
||||
self.cloud.iot.register_on_connect(self.async_publish_cloudhooks)
|
||||
|
||||
async def async_publish_cloudhooks(self):
|
||||
"""Inform the Relayer of the cloudhooks that we support."""
|
||||
cloudhooks = self.cloud.prefs.cloudhooks
|
||||
await self.cloud.iot.async_send_message('webhook-register', {
|
||||
'cloudhook_ids': [info['cloudhook_id'] for info
|
||||
in cloudhooks.values()]
|
||||
}, expect_answer=False)
|
||||
|
||||
async def async_create(self, webhook_id):
|
||||
"""Create a cloud webhook."""
|
||||
cloudhooks = self.cloud.prefs.cloudhooks
|
||||
|
||||
if webhook_id in cloudhooks:
|
||||
raise ValueError('Hook is already enabled for the cloud.')
|
||||
|
||||
if not self.cloud.iot.connected:
|
||||
raise ValueError("Cloud is not connected")
|
||||
|
||||
# Create cloud hook
|
||||
with async_timeout.timeout(10):
|
||||
resp = await cloud_api.async_create_cloudhook(self.cloud)
|
||||
|
||||
data = await resp.json()
|
||||
cloudhook_id = data['cloudhook_id']
|
||||
cloudhook_url = data['url']
|
||||
|
||||
# Store hook
|
||||
cloudhooks = dict(cloudhooks)
|
||||
hook = cloudhooks[webhook_id] = {
|
||||
'webhook_id': webhook_id,
|
||||
'cloudhook_id': cloudhook_id,
|
||||
'cloudhook_url': cloudhook_url
|
||||
}
|
||||
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
|
||||
|
||||
await self.async_publish_cloudhooks()
|
||||
|
||||
return hook
|
||||
|
||||
async def async_delete(self, webhook_id):
|
||||
"""Delete a cloud webhook."""
|
||||
cloudhooks = self.cloud.prefs.cloudhooks
|
||||
|
||||
if webhook_id not in cloudhooks:
|
||||
raise ValueError('Hook is not enabled for the cloud.')
|
||||
|
||||
# Remove hook
|
||||
cloudhooks = dict(cloudhooks)
|
||||
cloudhooks.pop(webhook_id)
|
||||
await self.cloud.prefs.async_update(cloudhooks=cloudhooks)
|
||||
|
||||
await self.async_publish_cloudhooks()
|
|
@ -6,6 +6,7 @@ REQUEST_TIMEOUT = 10
|
|||
PREF_ENABLE_ALEXA = 'alexa_enabled'
|
||||
PREF_ENABLE_GOOGLE = 'google_enabled'
|
||||
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
|
||||
PREF_CLOUDHOOKS = 'cloudhooks'
|
||||
|
||||
SERVERS = {
|
||||
'production': {
|
||||
|
@ -16,7 +17,8 @@ SERVERS = {
|
|||
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
|
||||
'amazonaws.com/prod/smart_home_sync'),
|
||||
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
|
||||
'subscription_info')
|
||||
'subscription_info'),
|
||||
'cloudhook_create_url': 'https://webhooks-api.nabucasa.com/generate'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import asyncio
|
|||
from functools import wraps
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -44,6 +45,20 @@ SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
WS_TYPE_HOOK_CREATE = 'cloud/cloudhook/create'
|
||||
SCHEMA_WS_HOOK_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_HOOK_CREATE,
|
||||
vol.Required('webhook_id'): str
|
||||
})
|
||||
|
||||
|
||||
WS_TYPE_HOOK_DELETE = 'cloud/cloudhook/delete'
|
||||
SCHEMA_WS_HOOK_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_HOOK_DELETE,
|
||||
vol.Required('webhook_id'): str
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Initialize the HTTP API."""
|
||||
hass.components.websocket_api.async_register_command(
|
||||
|
@ -58,6 +73,14 @@ async def async_setup(hass):
|
|||
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
|
||||
SCHEMA_WS_UPDATE_PREFS
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_HOOK_CREATE, websocket_hook_create,
|
||||
SCHEMA_WS_HOOK_CREATE
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_HOOK_DELETE, websocket_hook_delete,
|
||||
SCHEMA_WS_HOOK_DELETE
|
||||
)
|
||||
hass.http.register_view(GoogleActionsSyncView)
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
|
@ -76,7 +99,7 @@ _CLOUD_ERRORS = {
|
|||
|
||||
|
||||
def _handle_cloud_errors(handler):
|
||||
"""Handle auth errors."""
|
||||
"""Webview decorator to handle auth errors."""
|
||||
@wraps(handler)
|
||||
async def error_handler(view, request, *args, **kwargs):
|
||||
"""Handle exceptions that raise from the wrapped request handler."""
|
||||
|
@ -240,17 +263,49 @@ def websocket_cloud_status(hass, connection, msg):
|
|||
websocket_api.result_message(msg['id'], _account_data(cloud)))
|
||||
|
||||
|
||||
def _require_cloud_login(handler):
|
||||
"""Websocket decorator that requires cloud to be logged in."""
|
||||
@wraps(handler)
|
||||
def with_cloud_auth(hass, connection, msg):
|
||||
"""Require to be logged into the cloud."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
if not cloud.is_logged_in:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'not_logged_in',
|
||||
'You need to be logged in to the cloud.'))
|
||||
return
|
||||
|
||||
handler(hass, connection, msg)
|
||||
|
||||
return with_cloud_auth
|
||||
|
||||
|
||||
def _handle_aiohttp_errors(handler):
|
||||
"""Websocket decorator that handlers aiohttp errors.
|
||||
|
||||
Can only wrap async handlers.
|
||||
"""
|
||||
@wraps(handler)
|
||||
async def with_error_handling(hass, connection, msg):
|
||||
"""Handle aiohttp errors."""
|
||||
try:
|
||||
await handler(hass, connection, msg)
|
||||
except asyncio.TimeoutError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'timeout', 'Command timed out.'))
|
||||
except aiohttp.ClientError:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'unknown', 'Error making request.'))
|
||||
|
||||
return with_error_handling
|
||||
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
async def websocket_subscription(hass, connection, msg):
|
||||
"""Handle request for account info."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
if not cloud.is_logged_in:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'not_logged_in',
|
||||
'You need to be logged in to the cloud.'))
|
||||
return
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
response = await cloud.fetch_subscription_info()
|
||||
|
||||
|
@ -277,24 +332,37 @@ async def websocket_subscription(hass, connection, msg):
|
|||
connection.send_message(websocket_api.result_message(msg['id'], data))
|
||||
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
async def websocket_update_prefs(hass, connection, msg):
|
||||
"""Handle request for account info."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
if not cloud.is_logged_in:
|
||||
connection.send_message(websocket_api.error_message(
|
||||
msg['id'], 'not_logged_in',
|
||||
'You need to be logged in to the cloud.'))
|
||||
return
|
||||
|
||||
changes = dict(msg)
|
||||
changes.pop('id')
|
||||
changes.pop('type')
|
||||
await cloud.prefs.async_update(**changes)
|
||||
|
||||
connection.send_message(websocket_api.result_message(
|
||||
msg['id'], {'success': True}))
|
||||
connection.send_message(websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
@_handle_aiohttp_errors
|
||||
async def websocket_hook_create(hass, connection, msg):
|
||||
"""Handle request for account info."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
hook = await cloud.cloudhooks.async_create(msg['webhook_id'])
|
||||
connection.send_message(websocket_api.result_message(msg['id'], hook))
|
||||
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
async def websocket_hook_delete(hass, connection, msg):
|
||||
"""Handle request for account info."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
await cloud.cloudhooks.async_delete(msg['webhook_id'])
|
||||
connection.send_message(websocket_api.result_message(msg['id']))
|
||||
|
||||
|
||||
def _account_data(cloud):
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import pprint
|
||||
import uuid
|
||||
|
||||
from aiohttp import hdrs, client_exceptions, WSMsgType
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.components.alexa import smart_home as alexa
|
||||
from homeassistant.components.google_assistant import smart_home as ga
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.util.aiohttp import MockRequest, serialize_response
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from . import auth_api
|
||||
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
|
||||
|
@ -25,6 +28,19 @@ class UnknownHandler(Exception):
|
|||
"""Exception raised when trying to handle unknown handler."""
|
||||
|
||||
|
||||
class NotConnected(Exception):
|
||||
"""Exception raised when trying to handle unknown handler."""
|
||||
|
||||
|
||||
class ErrorMessage(Exception):
|
||||
"""Exception raised when there was error handling message in the cloud."""
|
||||
|
||||
def __init__(self, error):
|
||||
"""Initialize Error Message."""
|
||||
super().__init__(self, "Error in Cloud")
|
||||
self.error = error
|
||||
|
||||
|
||||
class CloudIoT:
|
||||
"""Class to manage the IoT connection."""
|
||||
|
||||
|
@ -41,6 +57,19 @@ class CloudIoT:
|
|||
self.tries = 0
|
||||
# Current state of the connection
|
||||
self.state = STATE_DISCONNECTED
|
||||
# Local code waiting for a response
|
||||
self._response_handler = {}
|
||||
self._on_connect = []
|
||||
|
||||
@callback
|
||||
def register_on_connect(self, on_connect_cb):
|
||||
"""Register an async on_connect callback."""
|
||||
self._on_connect.append(on_connect_cb)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Return if we're currently connected."""
|
||||
return self.state == STATE_CONNECTED
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self):
|
||||
|
@ -91,6 +120,30 @@ class CloudIoT:
|
|||
if remove_hass_stop_listener is not None:
|
||||
remove_hass_stop_listener()
|
||||
|
||||
async def async_send_message(self, handler, payload,
|
||||
expect_answer=True):
|
||||
"""Send a message."""
|
||||
if self.state != STATE_CONNECTED:
|
||||
raise NotConnected
|
||||
|
||||
msgid = uuid.uuid4().hex
|
||||
|
||||
if expect_answer:
|
||||
fut = self._response_handler[msgid] = asyncio.Future()
|
||||
|
||||
message = {
|
||||
'msgid': msgid,
|
||||
'handler': handler,
|
||||
'payload': payload,
|
||||
}
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
_LOGGER.debug("Publishing message:\n%s\n",
|
||||
pprint.pformat(message))
|
||||
await self.client.send_json(message)
|
||||
|
||||
if expect_answer:
|
||||
return await fut
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_connection(self):
|
||||
"""Connect to the IoT broker."""
|
||||
|
@ -134,6 +187,9 @@ class CloudIoT:
|
|||
_LOGGER.info("Connected")
|
||||
self.state = STATE_CONNECTED
|
||||
|
||||
if self._on_connect:
|
||||
yield from asyncio.wait([cb() for cb in self._on_connect])
|
||||
|
||||
while not client.closed:
|
||||
msg = yield from client.receive()
|
||||
|
||||
|
@ -159,6 +215,17 @@ class CloudIoT:
|
|||
_LOGGER.debug("Received message:\n%s\n",
|
||||
pprint.pformat(msg))
|
||||
|
||||
response_handler = self._response_handler.pop(msg['msgid'],
|
||||
None)
|
||||
|
||||
if response_handler is not None:
|
||||
if 'payload' in msg:
|
||||
response_handler.set_result(msg["payload"])
|
||||
else:
|
||||
response_handler.set_exception(
|
||||
ErrorMessage(msg['error']))
|
||||
continue
|
||||
|
||||
response = {
|
||||
'msgid': msg['msgid'],
|
||||
}
|
||||
|
@ -257,3 +324,43 @@ def async_handle_cloud(hass, cloud, payload):
|
|||
payload['reason'])
|
||||
else:
|
||||
_LOGGER.warning("Received unknown cloud action: %s", action)
|
||||
|
||||
|
||||
@HANDLERS.register('webhook')
|
||||
async def async_handle_webhook(hass, cloud, payload):
|
||||
"""Handle an incoming IoT message for cloud webhooks."""
|
||||
cloudhook_id = payload['cloudhook_id']
|
||||
|
||||
found = None
|
||||
for cloudhook in cloud.prefs.cloudhooks.values():
|
||||
if cloudhook['cloudhook_id'] == cloudhook_id:
|
||||
found = cloudhook
|
||||
break
|
||||
|
||||
if found is None:
|
||||
return {
|
||||
'status': 200
|
||||
}
|
||||
|
||||
request = MockRequest(
|
||||
content=payload['body'].encode('utf-8'),
|
||||
headers=payload['headers'],
|
||||
method=payload['method'],
|
||||
query_string=payload['query'],
|
||||
)
|
||||
|
||||
response = await hass.components.webhook.async_handle_webhook(
|
||||
found['webhook_id'], request)
|
||||
|
||||
response_dict = serialize_response(response)
|
||||
body = response_dict.get('body')
|
||||
if body:
|
||||
body = body.decode('utf-8')
|
||||
|
||||
return {
|
||||
'body': body,
|
||||
'status': response_dict['status'],
|
||||
'headers': {
|
||||
'Content-Type': response.content_type
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Preference management for cloud."""
|
||||
from .const import (
|
||||
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK)
|
||||
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
|
@ -16,28 +16,29 @@ class CloudPreferences:
|
|||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._prefs = None
|
||||
|
||||
async def async_initialize(self, logged_in):
|
||||
async def async_initialize(self):
|
||||
"""Finish initializing the preferences."""
|
||||
prefs = await self._store.async_load()
|
||||
|
||||
if prefs is None:
|
||||
# Backwards compat: we enable alexa/google if already logged in
|
||||
prefs = {
|
||||
PREF_ENABLE_ALEXA: logged_in,
|
||||
PREF_ENABLE_GOOGLE: logged_in,
|
||||
PREF_ENABLE_ALEXA: True,
|
||||
PREF_ENABLE_GOOGLE: True,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK: False,
|
||||
PREF_CLOUDHOOKS: {}
|
||||
}
|
||||
await self._store.async_save(prefs)
|
||||
|
||||
self._prefs = prefs
|
||||
|
||||
async def async_update(self, *, google_enabled=_UNDEF,
|
||||
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF):
|
||||
alexa_enabled=_UNDEF, google_allow_unlock=_UNDEF,
|
||||
cloudhooks=_UNDEF):
|
||||
"""Update user preferences."""
|
||||
for key, value in (
|
||||
(PREF_ENABLE_GOOGLE, google_enabled),
|
||||
(PREF_ENABLE_ALEXA, alexa_enabled),
|
||||
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
|
||||
(PREF_CLOUDHOOKS, cloudhooks),
|
||||
):
|
||||
if value is not _UNDEF:
|
||||
self._prefs[key] = value
|
||||
|
@ -62,3 +63,8 @@ class CloudPreferences:
|
|||
def google_allow_unlock(self):
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)
|
||||
|
||||
@property
|
||||
def cloudhooks(self):
|
||||
"""Return the published cloud webhooks."""
|
||||
return self._prefs.get(PREF_CLOUDHOOKS, {})
|
||||
|
|
|
@ -14,6 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
|
|||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = (
|
||||
'auth',
|
||||
'auth_provider_homeassistant',
|
||||
'automation',
|
||||
'config_entries',
|
||||
'core',
|
||||
|
@ -58,10 +60,6 @@ async def async_setup(hass, config):
|
|||
|
||||
tasks = [setup_panel(panel_name) for panel_name in SECTIONS]
|
||||
|
||||
if hass.auth.active:
|
||||
tasks.append(setup_panel('auth'))
|
||||
tasks.append(setup_panel('auth_provider_homeassistant'))
|
||||
|
||||
for panel_name in ON_DEMAND:
|
||||
if panel_name in hass.config.components:
|
||||
tasks.append(setup_panel(panel_name))
|
||||
|
|
|
@ -10,9 +10,8 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -86,7 +85,7 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
class Counter(Entity):
|
||||
class Counter(RestoreEntity):
|
||||
"""Representation of a counter."""
|
||||
|
||||
def __init__(self, object_id, name, initial, restore, step, icon):
|
||||
|
@ -128,10 +127,11 @@ class Counter(Entity):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
# __init__ will set self._state to self._initial, only override
|
||||
# if needed.
|
||||
if self._restore:
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
state = await self.async_get_last_state()
|
||||
if state is not None:
|
||||
self._state = int(state.state)
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/cover.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -24,7 +23,7 @@ from homeassistant.components.mqtt import (
|
|||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic,
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo)
|
||||
MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription)
|
||||
from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -130,7 +129,7 @@ PLATFORM_SCHEMA = vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
|||
async def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
|
||||
async_add_entities, discovery_info=None):
|
||||
"""Set up MQTT cover through configuration.yaml."""
|
||||
await _async_setup_entity(hass, config, async_add_entities)
|
||||
await _async_setup_entity(config, async_add_entities)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
|
@ -138,7 +137,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def async_discover(discovery_payload):
|
||||
"""Discover and add an MQTT cover."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
await _async_setup_entity(hass, config, async_add_entities,
|
||||
await _async_setup_entity(config, async_add_entities,
|
||||
discovery_payload[ATTR_DISCOVERY_HASH])
|
||||
|
||||
async_dispatcher_connect(
|
||||
|
@ -146,112 +145,78 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async_discover)
|
||||
|
||||
|
||||
async def _async_setup_entity(hass, config, async_add_entities,
|
||||
discovery_hash=None):
|
||||
async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
|
||||
"""Set up the MQTT Cover."""
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
set_position_template = config.get(CONF_SET_POSITION_TEMPLATE)
|
||||
if set_position_template is not None:
|
||||
set_position_template.hass = hass
|
||||
|
||||
async_add_entities([MqttCover(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_GET_POSITION_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_TILT_COMMAND_TOPIC),
|
||||
config.get(CONF_TILT_STATUS_TOPIC),
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
config.get(CONF_STATE_OPEN),
|
||||
config.get(CONF_STATE_CLOSED),
|
||||
config.get(CONF_POSITION_OPEN),
|
||||
config.get(CONF_POSITION_CLOSED),
|
||||
config.get(CONF_PAYLOAD_OPEN),
|
||||
config.get(CONF_PAYLOAD_CLOSE),
|
||||
config.get(CONF_PAYLOAD_STOP),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
value_template,
|
||||
config.get(CONF_TILT_OPEN_POSITION),
|
||||
config.get(CONF_TILT_CLOSED_POSITION),
|
||||
config.get(CONF_TILT_MIN),
|
||||
config.get(CONF_TILT_MAX),
|
||||
config.get(CONF_TILT_STATE_OPTIMISTIC),
|
||||
config.get(CONF_TILT_INVERT_STATE),
|
||||
config.get(CONF_SET_POSITION_TOPIC),
|
||||
set_position_template,
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
discovery_hash
|
||||
)])
|
||||
async_add_entities([MqttCover(config, discovery_hash)])
|
||||
|
||||
|
||||
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
||||
CoverDevice):
|
||||
"""Representation of a cover that can be controlled using MQTT."""
|
||||
|
||||
def __init__(self, name, state_topic, get_position_topic,
|
||||
command_topic, availability_topic,
|
||||
tilt_command_topic, tilt_status_topic, qos, retain,
|
||||
state_open, state_closed, position_open, position_closed,
|
||||
payload_open, payload_close, payload_stop, payload_available,
|
||||
payload_not_available, optimistic, value_template,
|
||||
tilt_open_position, tilt_closed_position, tilt_min, tilt_max,
|
||||
tilt_optimistic, tilt_invert, set_position_topic,
|
||||
set_position_template, unique_id: Optional[str],
|
||||
device_config: Optional[ConfigType], discovery_hash):
|
||||
def __init__(self, config, discovery_hash):
|
||||
"""Initialize the cover."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._position = None
|
||||
self._state = None
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
self._get_position_topic = get_position_topic
|
||||
self._command_topic = command_topic
|
||||
self._tilt_command_topic = tilt_command_topic
|
||||
self._tilt_status_topic = tilt_status_topic
|
||||
self._qos = qos
|
||||
self._payload_open = payload_open
|
||||
self._payload_close = payload_close
|
||||
self._payload_stop = payload_stop
|
||||
self._state_open = state_open
|
||||
self._state_closed = state_closed
|
||||
self._position_open = position_open
|
||||
self._position_closed = position_closed
|
||||
self._retain = retain
|
||||
self._tilt_open_position = tilt_open_position
|
||||
self._tilt_closed_position = tilt_closed_position
|
||||
self._optimistic = (optimistic or (state_topic is None and
|
||||
get_position_topic is None))
|
||||
self._template = value_template
|
||||
self._sub_state = None
|
||||
|
||||
self._optimistic = None
|
||||
self._tilt_value = None
|
||||
self._tilt_min = tilt_min
|
||||
self._tilt_max = tilt_max
|
||||
self._tilt_optimistic = tilt_optimistic
|
||||
self._tilt_invert = tilt_invert
|
||||
self._set_position_topic = set_position_topic
|
||||
self._set_position_template = set_position_template
|
||||
self._unique_id = unique_id
|
||||
self._discovery_hash = discovery_hash
|
||||
self._tilt_optimistic = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
qos = config.get(CONF_QOS)
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe MQTT events."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await super().async_added_to_hass()
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._setup_from_config(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
self._config = config
|
||||
self._optimistic = (config.get(CONF_OPTIMISTIC) or
|
||||
(config.get(CONF_STATE_TOPIC) is None and
|
||||
config.get(CONF_GET_POSITION_TOPIC) is None))
|
||||
self._tilt_optimistic = config.get(CONF_TILT_STATE_OPTIMISTIC)
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||
if template is not None:
|
||||
template.hass = self.hass
|
||||
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
|
||||
if set_position_template is not None:
|
||||
set_position_template.hass = self.hass
|
||||
|
||||
topics = {}
|
||||
|
||||
@callback
|
||||
def tilt_updated(topic, payload, qos):
|
||||
"""Handle tilt updates."""
|
||||
if (payload.isnumeric() and
|
||||
self._tilt_min <= int(payload) <= self._tilt_max):
|
||||
(self._config.get(CONF_TILT_MIN) <= int(payload) <=
|
||||
self._config.get(CONF_TILT_MAX))):
|
||||
|
||||
level = self.find_percentage_in_range(float(payload))
|
||||
self._tilt_value = level
|
||||
|
@ -260,13 +225,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
@callback
|
||||
def state_message_received(topic, payload, qos):
|
||||
"""Handle new MQTT state messages."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
|
||||
if payload == self._state_open:
|
||||
if payload == self._config.get(CONF_STATE_OPEN):
|
||||
self._state = False
|
||||
elif payload == self._state_closed:
|
||||
elif payload == self._config.get(CONF_STATE_CLOSED):
|
||||
self._state = True
|
||||
else:
|
||||
_LOGGER.warning("Payload is not True or False: %s", payload)
|
||||
|
@ -276,8 +241,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
@callback
|
||||
def position_message_received(topic, payload, qos):
|
||||
"""Handle new MQTT state messages."""
|
||||
if self._template is not None:
|
||||
payload = self._template.async_render_with_possible_json_value(
|
||||
if template is not None:
|
||||
payload = template.async_render_with_possible_json_value(
|
||||
payload)
|
||||
|
||||
if payload.isnumeric():
|
||||
|
@ -292,25 +257,38 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
return
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._get_position_topic:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._get_position_topic,
|
||||
position_message_received, self._qos)
|
||||
elif self._state_topic:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._state_topic,
|
||||
state_message_received, self._qos)
|
||||
if self._config.get(CONF_GET_POSITION_TOPIC):
|
||||
topics['get_position_topic'] = {
|
||||
'topic': self._config.get(CONF_GET_POSITION_TOPIC),
|
||||
'msg_callback': position_message_received,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
elif self._config.get(CONF_STATE_TOPIC):
|
||||
topics['state_topic'] = {
|
||||
'topic': self._config.get(CONF_STATE_TOPIC),
|
||||
'msg_callback': state_message_received,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
else:
|
||||
# Force into optimistic mode.
|
||||
self._optimistic = True
|
||||
|
||||
if self._tilt_status_topic is None:
|
||||
if self._config.get(CONF_TILT_STATUS_TOPIC) is None:
|
||||
self._tilt_optimistic = True
|
||||
else:
|
||||
self._tilt_optimistic = False
|
||||
self._tilt_value = STATE_UNKNOWN
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._tilt_status_topic, tilt_updated, self._qos)
|
||||
topics['tilt_status_topic'] = {
|
||||
'topic': self._config.get(CONF_TILT_STATUS_TOPIC),
|
||||
'msg_callback': tilt_updated,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
topics)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
@ -325,7 +303,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the cover."""
|
||||
return self._name
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
|
@ -349,13 +327,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = 0
|
||||
if self._command_topic is not None:
|
||||
if self._config.get(CONF_COMMAND_TOPIC) is not None:
|
||||
supported_features = OPEN_CLOSE_FEATURES
|
||||
|
||||
if self._set_position_topic is not None:
|
||||
if self._config.get(CONF_SET_POSITION_TOPIC) is not None:
|
||||
supported_features |= SUPPORT_SET_POSITION
|
||||
|
||||
if self._tilt_command_topic is not None:
|
||||
if self._config.get(CONF_TILT_COMMAND_TOPIC) is not None:
|
||||
supported_features |= TILT_FEATURES
|
||||
|
||||
return supported_features
|
||||
|
@ -366,14 +344,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_open, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_OPEN), self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = False
|
||||
if self._get_position_topic:
|
||||
if self._config.get(CONF_GET_POSITION_TOPIC):
|
||||
self._position = self.find_percentage_in_range(
|
||||
self._position_open, COVER_PAYLOAD)
|
||||
self._config.get(CONF_POSITION_OPEN), COVER_PAYLOAD)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
|
@ -382,14 +361,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_close, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_CLOSE), self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if self._optimistic:
|
||||
# Optimistically assume that cover has changed state.
|
||||
self._state = True
|
||||
if self._get_position_topic:
|
||||
if self._config.get(CONF_GET_POSITION_TOPIC):
|
||||
self._position = self.find_percentage_in_range(
|
||||
self._position_closed, COVER_PAYLOAD)
|
||||
self._config.get(CONF_POSITION_CLOSED), COVER_PAYLOAD)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
|
@ -398,25 +378,30 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
This method is a coroutine.
|
||||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_stop, self._qos,
|
||||
self._retain)
|
||||
self.hass, self._config.get(CONF_COMMAND_TOPIC),
|
||||
self._config.get(CONF_PAYLOAD_STOP), self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
async def async_open_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover open."""
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
self._tilt_open_position, self._qos,
|
||||
self._retain)
|
||||
mqtt.async_publish(self.hass,
|
||||
self._config.get(CONF_TILT_COMMAND_TOPIC),
|
||||
self._config.get(CONF_TILT_OPEN_POSITION),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if self._tilt_optimistic:
|
||||
self._tilt_value = self._tilt_open_position
|
||||
self._tilt_value = self._config.get(CONF_TILT_OPEN_POSITION)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_close_cover_tilt(self, **kwargs):
|
||||
"""Tilt the cover closed."""
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
self._tilt_closed_position, self._qos,
|
||||
self._retain)
|
||||
mqtt.async_publish(self.hass,
|
||||
self._config.get(CONF_TILT_COMMAND_TOPIC),
|
||||
self._config.get(CONF_TILT_CLOSED_POSITION),
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if self._tilt_optimistic:
|
||||
self._tilt_value = self._tilt_closed_position
|
||||
self._tilt_value = self._config.get(CONF_TILT_CLOSED_POSITION)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs):
|
||||
|
@ -429,29 +414,38 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
# The position needs to be between min and max
|
||||
level = self.find_in_range_from_percent(position)
|
||||
|
||||
mqtt.async_publish(self.hass, self._tilt_command_topic,
|
||||
level, self._qos, self._retain)
|
||||
mqtt.async_publish(self.hass,
|
||||
self._config.get(CONF_TILT_COMMAND_TOPIC),
|
||||
level,
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
async def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
percentage_position = position
|
||||
if self._set_position_template is not None:
|
||||
if set_position_template is not None:
|
||||
try:
|
||||
position = self._set_position_template.async_render(
|
||||
position = set_position_template.async_render(
|
||||
**kwargs)
|
||||
except TemplateError as ex:
|
||||
_LOGGER.error(ex)
|
||||
self._state = None
|
||||
elif self._position_open != 100 and self._position_closed != 0:
|
||||
elif (self._config.get(CONF_POSITION_OPEN) != 100 and
|
||||
self._config.get(CONF_POSITION_CLOSED) != 0):
|
||||
position = self.find_in_range_from_percent(
|
||||
position, COVER_PAYLOAD)
|
||||
|
||||
mqtt.async_publish(self.hass, self._set_position_topic,
|
||||
position, self._qos, self._retain)
|
||||
mqtt.async_publish(self.hass,
|
||||
self._config.get(CONF_SET_POSITION_TOPIC),
|
||||
position,
|
||||
self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if self._optimistic:
|
||||
self._state = percentage_position == self._position_closed
|
||||
self._state = percentage_position == \
|
||||
self._config.get(CONF_POSITION_CLOSED)
|
||||
self._position = percentage_position
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
|
@ -459,11 +453,11 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
"""Find the 0-100% value within the specified range."""
|
||||
# the range of motion as defined by the min max values
|
||||
if range_type == COVER_PAYLOAD:
|
||||
max_range = self._position_open
|
||||
min_range = self._position_closed
|
||||
max_range = self._config.get(CONF_POSITION_OPEN)
|
||||
min_range = self._config.get(CONF_POSITION_CLOSED)
|
||||
else:
|
||||
max_range = self._tilt_max
|
||||
min_range = self._tilt_min
|
||||
max_range = self._config.get(CONF_TILT_MAX)
|
||||
min_range = self._config.get(CONF_TILT_MIN)
|
||||
current_range = max_range - min_range
|
||||
# offset to be zero based
|
||||
offset_position = position - min_range
|
||||
|
@ -474,7 +468,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
min_percent = 0
|
||||
position_percentage = min(max(position_percentage, min_percent),
|
||||
max_percent)
|
||||
if range_type == TILT_PAYLOAD and self._tilt_invert:
|
||||
if range_type == TILT_PAYLOAD and \
|
||||
self._config.get(CONF_TILT_INVERT_STATE):
|
||||
return 100 - position_percentage
|
||||
return position_percentage
|
||||
|
||||
|
@ -488,17 +483,18 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
returning the offset
|
||||
"""
|
||||
if range_type == COVER_PAYLOAD:
|
||||
max_range = self._position_open
|
||||
min_range = self._position_closed
|
||||
max_range = self._config.get(CONF_POSITION_OPEN)
|
||||
min_range = self._config.get(CONF_POSITION_CLOSED)
|
||||
else:
|
||||
max_range = self._tilt_max
|
||||
min_range = self._tilt_min
|
||||
max_range = self._config.get(CONF_TILT_MAX)
|
||||
min_range = self._config.get(CONF_TILT_MIN)
|
||||
offset = min_range
|
||||
current_range = max_range - min_range
|
||||
position = round(current_range * (percentage / 100.0))
|
||||
position += offset
|
||||
|
||||
if range_type == TILT_PAYLOAD and self._tilt_invert:
|
||||
if range_type == TILT_PAYLOAD and \
|
||||
self._config.get(CONF_TILT_INVERT_STATE):
|
||||
position = max_range - position + offset
|
||||
return position
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ https://home-assistant.io/components/cover.tellduslive/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components import tellduslive
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
from homeassistant.components.tellduslive import TelldusLiveEntity
|
||||
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -19,7 +20,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
|
||||
add_entities(TelldusLiveCover(hass, cover) for cover in discovery_info)
|
||||
client = hass.data[tellduslive.DOMAIN]
|
||||
add_entities(TelldusLiveCover(client, cover) for cover in discovery_info)
|
||||
|
||||
|
||||
class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
|
||||
|
@ -33,14 +35,11 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
|
|||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self.device.down()
|
||||
self.changed()
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self.device.up()
|
||||
self.changed()
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self.device.stop()
|
||||
self.changed()
|
||||
|
|
|
@ -132,3 +132,8 @@ class DaikinApi:
|
|||
_LOGGER.warning(
|
||||
"Connection failed for %s", self.ip_address
|
||||
)
|
||||
|
||||
@property
|
||||
def mac(self):
|
||||
"""Return mac-address of device."""
|
||||
return self.device.values.get('mac')
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"host": "Amfitri\u00f3",
|
||||
"port": "Port (predeterminat: '80')"
|
||||
"port": "Port"
|
||||
},
|
||||
"title": "Definiu la passarel\u00b7la deCONZ"
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"title": "deCONZ gateway d\u00e9fin\u00e9ieren"
|
||||
},
|
||||
"link": {
|
||||
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
|
||||
"description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen",
|
||||
"title": "Link mat deCONZ"
|
||||
},
|
||||
"options": {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"init": {
|
||||
"data": {
|
||||
"host": "\u0425\u043e\u0441\u0442",
|
||||
"port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')"
|
||||
"port": "\u041f\u043e\u0440\u0442"
|
||||
},
|
||||
"title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
|
||||
},
|
||||
|
|
|
@ -22,9 +22,8 @@ from homeassistant.components.zone.zone import async_active_zone
|
|||
from homeassistant.config import load_yaml_config_file, async_log_exception
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant import util
|
||||
|
@ -384,7 +383,6 @@ class DeviceTracker:
|
|||
for device in self.devices.values():
|
||||
if (device.track and device.last_update_home) and \
|
||||
device.stale(now):
|
||||
device.mark_stale()
|
||||
self.hass.async_create_task(device.async_update_ha_state(True))
|
||||
|
||||
async def async_setup_tracked_device(self):
|
||||
|
@ -407,7 +405,7 @@ class DeviceTracker:
|
|||
await asyncio.wait(tasks, loop=self.hass.loop)
|
||||
|
||||
|
||||
class Device(Entity):
|
||||
class Device(RestoreEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
host_name = None # type: str
|
||||
|
@ -575,7 +573,8 @@ class Device(Entity):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Add an entity."""
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
await super().async_added_to_hass()
|
||||
state = await self.async_get_last_state()
|
||||
if not state:
|
||||
return
|
||||
self._state = state.state
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
|
|||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
REQUIREMENTS = ['btsmarthub_devicelist==0.1.1']
|
||||
REQUIREMENTS = ['btsmarthub_devicelist==0.1.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
|
|||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify, dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.8']
|
||||
REQUIREMENTS = ['locationsharinglib==3.0.9']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import (
|
|||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_METHOD)
|
||||
|
||||
REQUIREMENTS = ['librouteros==2.1.1']
|
||||
REQUIREMENTS = ['librouteros==2.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -128,7 +128,8 @@ class MikrotikScanner(DeviceScanner):
|
|||
librouteros.exceptions.ConnectionError):
|
||||
self.wireless_exist = False
|
||||
|
||||
if not self.wireless_exist or self.method == 'ip':
|
||||
if not self.wireless_exist and not self.capsman_exist \
|
||||
or self.method == 'ip':
|
||||
_LOGGER.info(
|
||||
"Mikrotik %s: Wireless adapters not found. Try to "
|
||||
"use DHCP lease table as presence tracker source. "
|
||||
|
@ -143,12 +144,18 @@ class MikrotikScanner(DeviceScanner):
|
|||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError) as api_error:
|
||||
_LOGGER.error("Connection error: %s", api_error)
|
||||
|
||||
return self.connected
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device MACs."""
|
||||
self._update_info()
|
||||
import librouteros
|
||||
try:
|
||||
self._update_info()
|
||||
except (librouteros.exceptions.TrapError,
|
||||
librouteros.exceptions.MultiTrapError,
|
||||
librouteros.exceptions.ConnectionError) as api_error:
|
||||
_LOGGER.error("Connection error: %s", api_error)
|
||||
self.connect_to_device()
|
||||
return [device for device in self.last_results]
|
||||
|
||||
def get_device_name(self, device):
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
|||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.5']
|
||||
REQUIREMENTS = ['pysnmp==4.4.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -7,33 +7,32 @@ https://home-assistant.io/components/device_tracker.volvooncall/
|
|||
import logging
|
||||
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_STATE_UPDATED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
"""Set up the Volvo tracker."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
vin, _ = discovery_info
|
||||
voc = hass.data[DATA_KEY]
|
||||
vehicle = voc.vehicles[vin]
|
||||
vin, component, attr = discovery_info
|
||||
data = hass.data[DATA_KEY]
|
||||
instrument = data.instrument(vin, component, attr)
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
async def see_vehicle():
|
||||
"""Handle the reporting of the vehicle position."""
|
||||
host_name = voc.vehicle_name(vehicle)
|
||||
host_name = instrument.vehicle_name
|
||||
dev_id = 'volvo_{}'.format(slugify(host_name))
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
gps=(vehicle.position['latitude'],
|
||||
vehicle.position['longitude']),
|
||||
icon='mdi:car')
|
||||
await async_see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
source_type=SOURCE_TYPE_GPS,
|
||||
gps=instrument.state,
|
||||
icon='mdi:car')
|
||||
|
||||
dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle)
|
||||
dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle)
|
||||
async_dispatcher_connect(hass, SIGNAL_STATE_UPDATED, see_vehicle)
|
||||
|
||||
return True
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
|
|||
from homeassistant.const import CONF_HOST, CONF_TOKEN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.3', 'construct==2.9.45']
|
||||
REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nConsulteu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
|
||||
"default": "Per enviar esdeveniments a Home Assistant, haureu de configurar [integraci\u00f3 webhook de Dialogflow]({dialogflow_url}). \n\n Ompliu la informaci\u00f3 seg\u00fcent: \n\n - URL: `{webhook_url}` \n - M\u00e8tode: POST \n - Tipus de contingut: application/json\n\nVegeu [la documentaci\u00f3]({docs_url}) per a m\u00e9s detalls."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa"
|
||||
}
|
||||
},
|
||||
"title": "Dialogflow"
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
"one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n \uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
|
||||
"default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 [Dialogflow Webhook]({dialogflow_url}) \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c \uc815\ubcf4\ub97c \uc785\ub825\ud574 \uc8fc\uc138\uc694. \n\n - URL: `{webhook_url}`\n - Method: POST\n - Content Type: application/json\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574 \uc8fc\uc138\uc694."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"step": {
|
||||
"user": {
|
||||
"description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Dialogflow?",
|
||||
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Dialogflow Webhook"
|
||||
"title": "Dialogflow Webhook"
|
||||
}
|
||||
},
|
||||
"title": "Dialogflow"
|
||||
|
|
|
@ -134,6 +134,7 @@ async def async_setup(hass, config):
|
|||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
if discovery_hash in already_discovered:
|
||||
logger.debug("Already discoverd service %s %s.", service, info)
|
||||
return
|
||||
|
||||
already_discovered.add(discovery_hash)
|
||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType # noqa
|
|||
|
||||
DOMAIN = "elkm1"
|
||||
|
||||
REQUIREMENTS = ['elkm1-lib==0.7.12']
|
||||
REQUIREMENTS = ['elkm1-lib==0.7.13']
|
||||
|
||||
CONF_AREA = 'area'
|
||||
CONF_COUNTER = 'counter'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""Support for Honeywell evohome (EMEA/EU-based systems only).
|
||||
"""Support for (EMEA/EU-based) Honeywell evohome systems.
|
||||
|
||||
Support for a temperature control system (TCS, controller) with 0+ heating
|
||||
zones (e.g. TRVs, relays) and, optionally, a DHW controller.
|
||||
|
@ -8,46 +8,48 @@ https://home-assistant.io/components/evohome/
|
|||
"""
|
||||
|
||||
# Glossary:
|
||||
# TCS - temperature control system (a.k.a. Controller, Parent), which can
|
||||
# have up to 13 Children:
|
||||
# 0-12 Heating zones (a.k.a. Zone), and
|
||||
# 0-1 DHW controller, (a.k.a. Boiler)
|
||||
# TCS - temperature control system (a.k.a. Controller, Parent), which can
|
||||
# have up to 13 Children:
|
||||
# 0-12 Heating zones (a.k.a. Zone), and
|
||||
# 0-1 DHW controller, (a.k.a. Boiler)
|
||||
# The TCS & Zones are implemented as Climate devices, Boiler as a WaterHeater
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
HTTP_BAD_REQUEST
|
||||
CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS
|
||||
)
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['evohomeclient==0.2.7']
|
||||
# If ever > 0.2.7, re-check the work-around wrapper is still required when
|
||||
# instantiating the client, below.
|
||||
REQUIREMENTS = ['evohomeclient==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'evohome'
|
||||
DATA_EVOHOME = 'data_' + DOMAIN
|
||||
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
|
||||
|
||||
CONF_LOCATION_IDX = 'location_idx'
|
||||
MAX_TEMP = 28
|
||||
MIN_TEMP = 5
|
||||
SCAN_INTERVAL_DEFAULT = 180
|
||||
SCAN_INTERVAL_MAX = 300
|
||||
SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_LOCATION_IDX, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_LOCATION_IDX, default=0):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL_DEFAULT):
|
||||
vol.All(cv.time_period, vol.Range(min=SCAN_INTERVAL_MINIMUM)),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -55,91 +57,107 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
GWS = 'gateways'
|
||||
TCS = 'temperatureControlSystems'
|
||||
|
||||
# bit masks for dispatcher packets
|
||||
EVO_PARENT = 0x01
|
||||
EVO_CHILD = 0x02
|
||||
|
||||
def setup(hass, config):
|
||||
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system.
|
||||
|
||||
One controller with 0+ heating zones (e.g. TRVs, relays) and, optionally, a
|
||||
DHW controller. Does not work for US-based systems.
|
||||
def setup(hass, hass_config):
|
||||
"""Create a (EMEA/EU-based) Honeywell evohome system.
|
||||
|
||||
Currently, only the Controller and the Zones are implemented here.
|
||||
"""
|
||||
evo_data = hass.data[DATA_EVOHOME] = {}
|
||||
evo_data['timers'] = {}
|
||||
|
||||
evo_data['params'] = dict(config[DOMAIN])
|
||||
evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT
|
||||
# use a copy, since scan_interval is rounded up to nearest 60s
|
||||
evo_data['params'] = dict(hass_config[DOMAIN])
|
||||
scan_interval = evo_data['params'][CONF_SCAN_INTERVAL]
|
||||
scan_interval = timedelta(
|
||||
minutes=(scan_interval.total_seconds() + 59) // 60)
|
||||
|
||||
from evohomeclient2 import EvohomeClient
|
||||
|
||||
_LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...")
|
||||
|
||||
try:
|
||||
# There's a bug in evohomeclient2 v0.2.7: the client.__init__() sets
|
||||
# the root loglevel when EvohomeClient(debug=?), so remember it now...
|
||||
log_level = logging.getLogger().getEffectiveLevel()
|
||||
|
||||
client = EvohomeClient(
|
||||
evo_data['params'][CONF_USERNAME],
|
||||
evo_data['params'][CONF_PASSWORD],
|
||||
debug=False
|
||||
)
|
||||
# ...then restore it to what it was before instantiating the client
|
||||
logging.getLogger().setLevel(log_level)
|
||||
|
||||
except HTTPError as err:
|
||||
if err.response.status_code == HTTP_BAD_REQUEST:
|
||||
_LOGGER.error(
|
||||
"Failed to establish a connection with evohome web servers, "
|
||||
"setup(): Failed to connect with the vendor's web servers. "
|
||||
"Check your username (%s), and password are correct."
|
||||
"Unable to continue. Resolve any errors and restart HA.",
|
||||
evo_data['params'][CONF_USERNAME]
|
||||
)
|
||||
return False # unable to continue
|
||||
|
||||
raise # we dont handle any other HTTPErrors
|
||||
elif err.response.status_code == HTTP_SERVICE_UNAVAILABLE:
|
||||
_LOGGER.error(
|
||||
"setup(): Failed to connect with the vendor's web servers. "
|
||||
"The server is not contactable. Unable to continue. "
|
||||
"Resolve any errors and restart HA."
|
||||
)
|
||||
|
||||
finally: # Redact username, password as no longer needed.
|
||||
elif err.response.status_code == HTTP_TOO_MANY_REQUESTS:
|
||||
_LOGGER.error(
|
||||
"setup(): Failed to connect with the vendor's web servers. "
|
||||
"You have exceeded the api rate limit. Unable to continue. "
|
||||
"Wait a while (say 10 minutes) and restart HA."
|
||||
)
|
||||
|
||||
else:
|
||||
raise # we dont expect/handle any other HTTPErrors
|
||||
|
||||
return False # unable to continue
|
||||
|
||||
finally: # Redact username, password as no longer needed
|
||||
evo_data['params'][CONF_USERNAME] = 'REDACTED'
|
||||
evo_data['params'][CONF_PASSWORD] = 'REDACTED'
|
||||
|
||||
evo_data['client'] = client
|
||||
evo_data['status'] = {}
|
||||
|
||||
# Redact any installation data we'll never need.
|
||||
if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED':
|
||||
for loc in client.installation_info:
|
||||
loc['locationInfo']['streetAddress'] = 'REDACTED'
|
||||
loc['locationInfo']['city'] = 'REDACTED'
|
||||
loc['locationInfo']['locationOwner'] = 'REDACTED'
|
||||
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
|
||||
# Redact any installation data we'll never need
|
||||
for loc in client.installation_info:
|
||||
loc['locationInfo']['locationId'] = 'REDACTED'
|
||||
loc['locationInfo']['locationOwner'] = 'REDACTED'
|
||||
loc['locationInfo']['streetAddress'] = 'REDACTED'
|
||||
loc['locationInfo']['city'] = 'REDACTED'
|
||||
loc[GWS][0]['gatewayInfo'] = 'REDACTED'
|
||||
|
||||
# Pull down the installation configuration.
|
||||
# Pull down the installation configuration
|
||||
loc_idx = evo_data['params'][CONF_LOCATION_IDX]
|
||||
|
||||
try:
|
||||
evo_data['config'] = client.installation_info[loc_idx]
|
||||
|
||||
except IndexError:
|
||||
_LOGGER.warning(
|
||||
"setup(): Parameter '%s' = %s , is outside its range (0-%s)",
|
||||
"setup(): Parameter '%s'=%s, is outside its range (0-%s)",
|
||||
CONF_LOCATION_IDX,
|
||||
loc_idx,
|
||||
len(client.installation_info) - 1
|
||||
)
|
||||
|
||||
return False # unable to continue
|
||||
|
||||
evo_data['status'] = {}
|
||||
|
||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
tmp_loc = dict(evo_data['config'])
|
||||
tmp_loc['locationInfo']['postcode'] = 'REDACTED'
|
||||
tmp_tcs = tmp_loc[GWS][0][TCS][0]
|
||||
if 'zones' in tmp_tcs:
|
||||
tmp_tcs['zones'] = '...'
|
||||
if 'dhw' in tmp_tcs:
|
||||
tmp_tcs['dhw'] = '...'
|
||||
if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW...
|
||||
tmp_loc[GWS][0][TCS][0]['dhw'] = '...'
|
||||
|
||||
_LOGGER.debug("setup(), location = %s", tmp_loc)
|
||||
_LOGGER.debug("setup(): evo_data['config']=%s", tmp_loc)
|
||||
|
||||
load_platform(hass, 'climate', DOMAIN, {}, config)
|
||||
load_platform(hass, 'climate', DOMAIN, {}, hass_config)
|
||||
|
||||
@callback
|
||||
def _first_update(event):
|
||||
# When HA has started, the hub knows to retreive it's first update
|
||||
pkt = {'sender': 'setup()', 'signal': 'refresh', 'to': EVO_PARENT}
|
||||
async_dispatcher_send(hass, DISPATCHER_EVOHOME, pkt)
|
||||
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_START, _first_update)
|
||||
|
||||
return True
|
||||
|
|
|
@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation
|
|||
https://home-assistant.io/components/fan.mqtt/
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -18,7 +17,7 @@ from homeassistant.components.mqtt import (
|
|||
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate,
|
||||
MqttEntityDeviceInfo)
|
||||
MqttEntityDeviceInfo, subscription)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
|
||||
|
@ -107,40 +106,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
|
|||
discovery_hash=None):
|
||||
"""Set up the MQTT fan."""
|
||||
async_add_entities([MqttFan(
|
||||
config.get(CONF_NAME),
|
||||
{
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_SPEED_STATE_TOPIC,
|
||||
CONF_SPEED_COMMAND_TOPIC,
|
||||
CONF_OSCILLATION_STATE_TOPIC,
|
||||
CONF_OSCILLATION_COMMAND_TOPIC,
|
||||
)
|
||||
},
|
||||
{
|
||||
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
||||
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
|
||||
OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE)
|
||||
},
|
||||
config.get(CONF_QOS),
|
||||
config.get(CONF_RETAIN),
|
||||
{
|
||||
STATE_ON: config.get(CONF_PAYLOAD_ON),
|
||||
STATE_OFF: config.get(CONF_PAYLOAD_OFF),
|
||||
OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON),
|
||||
OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF),
|
||||
SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED),
|
||||
SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED),
|
||||
SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED),
|
||||
},
|
||||
config.get(CONF_SPEED_LIST),
|
||||
config.get(CONF_OPTIMISTIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config.get(CONF_DEVICE),
|
||||
config,
|
||||
discovery_hash,
|
||||
)])
|
||||
|
||||
|
@ -149,43 +115,95 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
FanEntity):
|
||||
"""A MQTT fan component."""
|
||||
|
||||
def __init__(self, name, topic, templates, qos, retain, payload,
|
||||
speed_list, optimistic, availability_topic, payload_available,
|
||||
payload_not_available, unique_id: Optional[str],
|
||||
device_config: Optional[ConfigType], discovery_hash):
|
||||
def __init__(self, config, discovery_hash):
|
||||
"""Initialize the MQTT fan."""
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
self._name = name
|
||||
self._topic = topic
|
||||
self._qos = qos
|
||||
self._retain = retain
|
||||
self._payload = payload
|
||||
self._templates = templates
|
||||
self._speed_list = speed_list
|
||||
self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None
|
||||
self._optimistic_oscillation = (
|
||||
optimistic or topic[CONF_OSCILLATION_STATE_TOPIC] is None)
|
||||
self._optimistic_speed = (
|
||||
optimistic or topic[CONF_SPEED_STATE_TOPIC] is None)
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
self._state = False
|
||||
self._speed = None
|
||||
self._oscillation = None
|
||||
self._supported_features = 0
|
||||
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC]
|
||||
is not None and SUPPORT_OSCILLATE)
|
||||
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC]
|
||||
is not None and SUPPORT_SET_SPEED)
|
||||
self._unique_id = unique_id
|
||||
self._discovery_hash = discovery_hash
|
||||
self._sub_state = None
|
||||
|
||||
self._topic = None
|
||||
self._payload = None
|
||||
self._templates = None
|
||||
self._optimistic = None
|
||||
self._optimistic_oscillation = None
|
||||
self._optimistic_speed = None
|
||||
|
||||
# Load config
|
||||
self._setup_from_config(config)
|
||||
|
||||
availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
|
||||
payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
|
||||
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
|
||||
qos = config.get(CONF_QOS)
|
||||
device_config = config.get(CONF_DEVICE)
|
||||
|
||||
MqttAvailability.__init__(self, availability_topic, qos,
|
||||
payload_available, payload_not_available)
|
||||
MqttDiscoveryUpdate.__init__(self, discovery_hash,
|
||||
self.discovery_update)
|
||||
MqttEntityDeviceInfo.__init__(self, device_config)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT events."""
|
||||
await MqttAvailability.async_added_to_hass(self)
|
||||
await MqttDiscoveryUpdate.async_added_to_hass(self)
|
||||
await super().async_added_to_hass()
|
||||
await self._subscribe_topics()
|
||||
|
||||
async def discovery_update(self, discovery_payload):
|
||||
"""Handle updated discovery message."""
|
||||
config = PLATFORM_SCHEMA(discovery_payload)
|
||||
self._setup_from_config(config)
|
||||
await self.availability_discovery_update(config)
|
||||
await self._subscribe_topics()
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def _setup_from_config(self, config):
|
||||
"""(Re)Setup the entity."""
|
||||
self._config = config
|
||||
self._topic = {
|
||||
key: config.get(key) for key in (
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_SPEED_STATE_TOPIC,
|
||||
CONF_SPEED_COMMAND_TOPIC,
|
||||
CONF_OSCILLATION_STATE_TOPIC,
|
||||
CONF_OSCILLATION_COMMAND_TOPIC,
|
||||
)
|
||||
}
|
||||
self._templates = {
|
||||
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
||||
ATTR_SPEED: config.get(CONF_SPEED_VALUE_TEMPLATE),
|
||||
OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE)
|
||||
}
|
||||
self._payload = {
|
||||
STATE_ON: config.get(CONF_PAYLOAD_ON),
|
||||
STATE_OFF: config.get(CONF_PAYLOAD_OFF),
|
||||
OSCILLATE_ON_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_ON),
|
||||
OSCILLATE_OFF_PAYLOAD: config.get(CONF_PAYLOAD_OSCILLATION_OFF),
|
||||
SPEED_LOW: config.get(CONF_PAYLOAD_LOW_SPEED),
|
||||
SPEED_MEDIUM: config.get(CONF_PAYLOAD_MEDIUM_SPEED),
|
||||
SPEED_HIGH: config.get(CONF_PAYLOAD_HIGH_SPEED),
|
||||
}
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None
|
||||
self._optimistic_oscillation = (
|
||||
optimistic or self._topic[CONF_OSCILLATION_STATE_TOPIC] is None)
|
||||
self._optimistic_speed = (
|
||||
optimistic or self._topic[CONF_SPEED_STATE_TOPIC] is None)
|
||||
|
||||
self._supported_features = 0
|
||||
self._supported_features |= (self._topic[CONF_OSCILLATION_STATE_TOPIC]
|
||||
is not None and SUPPORT_OSCILLATE)
|
||||
self._supported_features |= (self._topic[CONF_SPEED_STATE_TOPIC]
|
||||
is not None and SUPPORT_SET_SPEED)
|
||||
|
||||
self._unique_id = config.get(CONF_UNIQUE_ID)
|
||||
|
||||
async def _subscribe_topics(self):
|
||||
"""(Re)Subscribe to topics."""
|
||||
topics = {}
|
||||
templates = {}
|
||||
for key, tpl in list(self._templates.items()):
|
||||
if tpl is None:
|
||||
|
@ -205,9 +223,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_STATE_TOPIC], state_received,
|
||||
self._qos)
|
||||
topics[CONF_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_STATE_TOPIC],
|
||||
'msg_callback': state_received,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
|
||||
@callback
|
||||
def speed_received(topic, payload, qos):
|
||||
|
@ -222,9 +241,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received,
|
||||
self._qos)
|
||||
topics[CONF_SPEED_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_SPEED_STATE_TOPIC],
|
||||
'msg_callback': speed_received,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
self._speed = SPEED_OFF
|
||||
|
||||
@callback
|
||||
|
@ -238,11 +258,21 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
self.async_schedule_update_ha_state()
|
||||
|
||||
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
|
||||
await mqtt.async_subscribe(
|
||||
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
||||
oscillation_received, self._qos)
|
||||
topics[CONF_OSCILLATION_STATE_TOPIC] = {
|
||||
'topic': self._topic[CONF_OSCILLATION_STATE_TOPIC],
|
||||
'msg_callback': oscillation_received,
|
||||
'qos': self._config.get(CONF_QOS)}
|
||||
self._oscillation = False
|
||||
|
||||
self._sub_state = await subscription.async_subscribe_topics(
|
||||
self.hass, self._sub_state,
|
||||
topics)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Unsubscribe when removed."""
|
||||
await subscription.async_unsubscribe_topics(self.hass, self._sub_state)
|
||||
await MqttAvailability.async_will_remove_from_hass(self)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for a MQTT fan."""
|
||||
|
@ -261,12 +291,12 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
@property
|
||||
def name(self) -> str:
|
||||
"""Get entity name."""
|
||||
return self._name
|
||||
return self._config.get(CONF_NAME)
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
"""Get the list of available speeds."""
|
||||
return self._speed_list
|
||||
return self._config.get(CONF_SPEED_LIST)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
|
@ -290,7 +320,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_ON], self._qos, self._retain)
|
||||
self._payload[STATE_ON], self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
if speed:
|
||||
await self.async_set_speed(speed)
|
||||
|
||||
|
@ -301,7 +332,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
"""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_COMMAND_TOPIC],
|
||||
self._payload[STATE_OFF], self._qos, self._retain)
|
||||
self._payload[STATE_OFF], self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
async def async_set_speed(self, speed: str) -> None:
|
||||
"""Set the speed of the fan.
|
||||
|
@ -322,7 +354,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_SPEED_COMMAND_TOPIC],
|
||||
mqtt_payload, self._qos, self._retain)
|
||||
mqtt_payload, self._config.get(CONF_QOS),
|
||||
self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._optimistic_speed:
|
||||
self._speed = speed
|
||||
|
@ -343,7 +376,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
|
|||
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC],
|
||||
payload, self._qos, self._retain)
|
||||
payload, self._config.get(CONF_QOS), self._config.get(CONF_RETAIN))
|
||||
|
||||
if self._optimistic_oscillation:
|
||||
self._oscillation = oscillating
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
|
|||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.4.3', 'construct==2.9.45']
|
||||
REQUIREMENTS = ['python-miio==0.4.4', 'construct==2.9.45']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -755,12 +755,13 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
|
|||
if self._model == MODEL_AIRHUMIDIFIER_CA:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA
|
||||
self._speed_list = [mode.name for mode in OperationMode]
|
||||
self._speed_list = [mode.name for mode in OperationMode if
|
||||
mode is not OperationMode.Strong]
|
||||
else:
|
||||
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
|
||||
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
|
||||
self._speed_list = [mode.name for mode in OperationMode if
|
||||
mode.name != 'Auto']
|
||||
mode is not OperationMode.Auto]
|
||||
|
||||
self._state_attrs.update(
|
||||
{attribute: None for attribute in self._available_attributes})
|
||||
|
|
|
@ -5,10 +5,15 @@ For more details on this platform, please refer to the documentation
|
|||
at https://home-assistant.io/components/fan.zha/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.components import zha
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
|
||||
SUPPORT_SET_SPEED)
|
||||
DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
|
||||
FanEntity)
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['zha']
|
||||
|
||||
|
@ -39,15 +44,38 @@ SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
|
|||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Zigbee Home Automation fans."""
|
||||
discovery_info = zha.get_discovery_info(hass, discovery_info)
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
async_add_entities([ZhaFan(**discovery_info)], update_before_add=True)
|
||||
"""Old way of setting up Zigbee Home Automation fans."""
|
||||
pass
|
||||
|
||||
|
||||
class ZhaFan(zha.Entity, FanEntity):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Zigbee Home Automation fan from config entry."""
|
||||
async def async_discover(discovery_info):
|
||||
await _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
[discovery_info])
|
||||
|
||||
unsub = async_dispatcher_connect(
|
||||
hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
|
||||
hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
|
||||
|
||||
fans = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
|
||||
if fans is not None:
|
||||
await _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
fans.values())
|
||||
del hass.data[DATA_ZHA][DOMAIN]
|
||||
|
||||
|
||||
async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
discovery_infos):
|
||||
"""Set up the ZHA fans."""
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
entities.append(ZhaFan(**discovery_info))
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
||||
class ZhaFan(ZhaEntity, FanEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
_domain = DOMAIN
|
||||
|
@ -101,9 +129,9 @@ class ZhaFan(zha.Entity, FanEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await zha.safe_read(self._endpoint.fan, ['fan_mode'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
result = await helpers.safe_read(self._endpoint.fan, ['fan_mode'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
new_value = result.get('fan_mode', None)
|
||||
self._state = VALUE_TO_SPEED.get(new_value, None)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ https://home-assistant.io/components/fibaro/
|
|||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import Optional
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (ATTR_ARMED, ATTR_BATTERY_LEVEL,
|
||||
|
@ -27,7 +28,8 @@ ATTR_CURRENT_POWER_W = "current_power_w"
|
|||
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
|
||||
CONF_PLUGINS = "plugins"
|
||||
|
||||
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', 'sensor', 'switch']
|
||||
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light',
|
||||
'scene', 'sensor', 'switch']
|
||||
|
||||
FIBARO_TYPEMAP = {
|
||||
'com.fibaro.multilevelSensor': "sensor",
|
||||
|
@ -43,7 +45,8 @@ FIBARO_TYPEMAP = {
|
|||
'com.fibaro.smokeSensor': 'binary_sensor',
|
||||
'com.fibaro.remoteSwitch': 'switch',
|
||||
'com.fibaro.sensor': 'sensor',
|
||||
'com.fibaro.colorController': 'light'
|
||||
'com.fibaro.colorController': 'light',
|
||||
'com.fibaro.securitySensor': 'binary_sensor'
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
@ -63,19 +66,23 @@ class FibaroController():
|
|||
_device_map = None # Dict for mapping deviceId to device object
|
||||
fibaro_devices = None # List of devices by type
|
||||
_callbacks = {} # Dict of update value callbacks by deviceId
|
||||
_client = None # Fiblary's Client object for communication
|
||||
_state_handler = None # Fiblary's StateHandler object
|
||||
_client = None # Fiblary's Client object for communication
|
||||
_state_handler = None # Fiblary's StateHandler object
|
||||
_import_plugins = None # Whether to import devices from plugins
|
||||
|
||||
def __init__(self, username, password, url, import_plugins):
|
||||
"""Initialize the Fibaro controller."""
|
||||
from fiblary3.client.v4.client import Client as FibaroClient
|
||||
self._client = FibaroClient(url, username, password)
|
||||
self._scene_map = None
|
||||
self.hub_serial = None # Unique serial number of the hub
|
||||
|
||||
def connect(self):
|
||||
"""Start the communication with the Fibaro controller."""
|
||||
try:
|
||||
login = self._client.login.get()
|
||||
info = self._client.info.get()
|
||||
self.hub_serial = slugify(info.serialNumber)
|
||||
except AssertionError:
|
||||
_LOGGER.error("Can't connect to Fibaro HC. "
|
||||
"Please check URL.")
|
||||
|
@ -87,6 +94,7 @@ class FibaroController():
|
|||
|
||||
self._room_map = {room.id: room for room in self._client.rooms.list()}
|
||||
self._read_devices()
|
||||
self._read_scenes()
|
||||
return True
|
||||
|
||||
def enable_state_handler(self):
|
||||
|
@ -166,6 +174,25 @@ class FibaroController():
|
|||
device_type = 'light'
|
||||
return device_type
|
||||
|
||||
def _read_scenes(self):
|
||||
scenes = self._client.scenes.list()
|
||||
self._scene_map = {}
|
||||
for device in scenes:
|
||||
if not device.visible:
|
||||
continue
|
||||
if device.roomID == 0:
|
||||
room_name = 'Unknown'
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
device.room_name = room_name
|
||||
device.friendly_name = '{} {}'.format(room_name, device.name)
|
||||
device.ha_id = '{}_{}_{}'.format(
|
||||
slugify(room_name), slugify(device.name), device.id)
|
||||
device.unique_id_str = "{}.{}".format(
|
||||
self.hub_serial, device.id)
|
||||
self._scene_map[device.id] = device
|
||||
self.fibaro_devices['scene'].append(device)
|
||||
|
||||
def _read_devices(self):
|
||||
"""Read and process the device list."""
|
||||
devices = self._client.devices.list()
|
||||
|
@ -177,6 +204,7 @@ class FibaroController():
|
|||
room_name = 'Unknown'
|
||||
else:
|
||||
room_name = self._room_map[device.roomID].name
|
||||
device.room_name = room_name
|
||||
device.friendly_name = room_name + ' ' + device.name
|
||||
device.ha_id = '{}_{}_{}'.format(
|
||||
slugify(room_name), slugify(device.name), device.id)
|
||||
|
@ -187,6 +215,8 @@ class FibaroController():
|
|||
else:
|
||||
device.mapped_type = None
|
||||
if device.mapped_type:
|
||||
device.unique_id_str = "{}.{}".format(
|
||||
self.hub_serial, device.id)
|
||||
self._device_map[device.id] = device
|
||||
self.fibaro_devices[device.mapped_type].append(device)
|
||||
else:
|
||||
|
@ -283,11 +313,14 @@ class FibaroDevice(Entity):
|
|||
|
||||
def call_set_color(self, red, green, blue, white):
|
||||
"""Set the color of Fibaro device."""
|
||||
color_str = "{},{},{},{}".format(int(red), int(green),
|
||||
int(blue), int(white))
|
||||
red = int(max(0, min(255, red)))
|
||||
green = int(max(0, min(255, green)))
|
||||
blue = int(max(0, min(255, blue)))
|
||||
white = int(max(0, min(255, white)))
|
||||
color_str = "{},{},{},{}".format(red, green, blue, white)
|
||||
self.fibaro_device.properties.color = color_str
|
||||
self.action("setColor", str(int(red)), str(int(green)),
|
||||
str(int(blue)), str(int(white)))
|
||||
self.action("setColor", str(red), str(green),
|
||||
str(blue), str(white))
|
||||
|
||||
def action(self, cmd, *args):
|
||||
"""Perform an action on the Fibaro HC."""
|
||||
|
@ -324,7 +357,12 @@ class FibaroDevice(Entity):
|
|||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
return self.fibaro_device.unique_id_str
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
|
@ -357,5 +395,5 @@ class FibaroDevice(Entity):
|
|||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
attr['id'] = self.ha_id
|
||||
attr['fibaro_id'] = self.fibaro_device.id
|
||||
return attr
|
||||
|
|
|
@ -24,7 +24,7 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20181121.1']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20181211.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
|
||||
|
@ -238,7 +238,7 @@ async def async_setup(hass, config):
|
|||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(repo_path, js_version, hass.auth.active)
|
||||
index_view = IndexView(repo_path, js_version)
|
||||
hass.http.register_view(index_view)
|
||||
hass.http.register_view(AuthorizeView(repo_path, js_version))
|
||||
|
||||
|
@ -250,7 +250,7 @@ async def async_setup(hass, config):
|
|||
await asyncio.wait(
|
||||
[async_register_built_in_panel(hass, panel) for panel in (
|
||||
'dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt', 'kiosk', 'lovelace', 'profile')],
|
||||
'dev-template', 'dev-mqtt', 'kiosk', 'states', 'profile')],
|
||||
loop=hass.loop)
|
||||
|
||||
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
|
||||
|
@ -362,13 +362,11 @@ class IndexView(HomeAssistantView):
|
|||
url = '/'
|
||||
name = 'frontend:index'
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self, repo_path, js_option, auth_active):
|
||||
def __init__(self, repo_path, js_option):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self.auth_active = auth_active
|
||||
self._template_cache = {}
|
||||
|
||||
def get_template(self, latest):
|
||||
|
@ -415,8 +413,6 @@ class IndexView(HomeAssistantView):
|
|||
# do not try to auto connect on load
|
||||
no_auth = '0'
|
||||
|
||||
use_oauth = '1' if self.auth_active else '0'
|
||||
|
||||
template = await hass.async_add_job(self.get_template, latest)
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
|
@ -425,7 +421,7 @@ class IndexView(HomeAssistantView):
|
|||
no_auth=no_auth,
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[extra_key],
|
||||
use_oauth=use_oauth
|
||||
use_oauth='1'
|
||||
)
|
||||
|
||||
return web.Response(text=template.render(**template_params),
|
||||
|
|
|
@ -13,7 +13,8 @@ import voluptuous as vol
|
|||
from homeassistant.components.geo_location import (
|
||||
PLATFORM_SCHEMA, GeoLocationEvent)
|
||||
from homeassistant.const import (
|
||||
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START)
|
||||
CONF_RADIUS, CONF_SCAN_INTERVAL, CONF_URL, EVENT_HOMEASSISTANT_START,
|
||||
CONF_LATITUDE, CONF_LONGITUDE)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
|
@ -38,6 +39,8 @@ SOURCE = 'geo_json_events'
|
|||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_URL): cv.string,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
@ -46,10 +49,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
"""Set up the GeoJSON Events platform."""
|
||||
url = config[CONF_URL]
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
config.get(CONF_LONGITUDE, hass.config.longitude))
|
||||
radius_in_km = config[CONF_RADIUS]
|
||||
# Initialize the entity manager.
|
||||
feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url,
|
||||
radius_in_km)
|
||||
feed = GeoJsonFeedEntityManager(
|
||||
hass, add_entities, scan_interval, coordinates, url, radius_in_km)
|
||||
|
||||
def start_feed_manager(event):
|
||||
"""Start feed manager."""
|
||||
|
@ -58,87 +63,49 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
|
||||
|
||||
class GeoJsonFeedManager:
|
||||
"""Feed Manager for GeoJSON feeds."""
|
||||
class GeoJsonFeedEntityManager:
|
||||
"""Feed Entity Manager for GeoJSON feeds."""
|
||||
|
||||
def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
|
||||
def __init__(self, hass, add_entities, scan_interval, coordinates, url,
|
||||
radius_in_km):
|
||||
"""Initialize the GeoJSON Feed Manager."""
|
||||
from geojson_client.generic_feed import GenericFeed
|
||||
from geojson_client.generic_feed import GenericFeedManager
|
||||
|
||||
self._hass = hass
|
||||
self._feed = GenericFeed(
|
||||
(hass.config.latitude, hass.config.longitude),
|
||||
filter_radius=radius_in_km, url=url)
|
||||
self._feed_manager = GenericFeedManager(
|
||||
self._generate_entity, self._update_entity, self._remove_entity,
|
||||
coordinates, url, filter_radius=radius_in_km)
|
||||
self._add_entities = add_entities
|
||||
self._scan_interval = scan_interval
|
||||
self.feed_entries = {}
|
||||
self._managed_external_ids = set()
|
||||
|
||||
def startup(self):
|
||||
"""Start up this manager."""
|
||||
self._update()
|
||||
self._feed_manager.update()
|
||||
self._init_regular_updates()
|
||||
|
||||
def _init_regular_updates(self):
|
||||
"""Schedule regular updates at the specified interval."""
|
||||
track_time_interval(
|
||||
self._hass, lambda now: self._update(), self._scan_interval)
|
||||
self._hass, lambda now: self._feed_manager.update(),
|
||||
self._scan_interval)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and then update connected entities."""
|
||||
import geojson_client
|
||||
def get_entry(self, external_id):
|
||||
"""Get feed entry by external id."""
|
||||
return self._feed_manager.feed_entries.get(external_id)
|
||||
|
||||
status, feed_entries = self._feed.update()
|
||||
if status == geojson_client.UPDATE_OK:
|
||||
_LOGGER.debug("Data retrieved %s", feed_entries)
|
||||
# Keep a copy of all feed entries for future lookups by entities.
|
||||
self.feed_entries = {entry.external_id: entry
|
||||
for entry in feed_entries}
|
||||
# For entity management the external ids from the feed are used.
|
||||
feed_external_ids = set(self.feed_entries)
|
||||
remove_external_ids = self._managed_external_ids.difference(
|
||||
feed_external_ids)
|
||||
self._remove_entities(remove_external_ids)
|
||||
update_external_ids = self._managed_external_ids.intersection(
|
||||
feed_external_ids)
|
||||
self._update_entities(update_external_ids)
|
||||
create_external_ids = feed_external_ids.difference(
|
||||
self._managed_external_ids)
|
||||
self._generate_new_entities(create_external_ids)
|
||||
elif status == geojson_client.UPDATE_OK_NO_DATA:
|
||||
_LOGGER.debug(
|
||||
"Update successful, but no data received from %s", self._feed)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Update not successful, no data received from %s", self._feed)
|
||||
# Remove all entities.
|
||||
self._remove_entities(self._managed_external_ids.copy())
|
||||
|
||||
def _generate_new_entities(self, external_ids):
|
||||
"""Generate new entities for events."""
|
||||
new_entities = []
|
||||
for external_id in external_ids:
|
||||
new_entity = GeoJsonLocationEvent(self, external_id)
|
||||
_LOGGER.debug("New entity added %s", external_id)
|
||||
new_entities.append(new_entity)
|
||||
self._managed_external_ids.add(external_id)
|
||||
def _generate_entity(self, external_id):
|
||||
"""Generate new entity."""
|
||||
new_entity = GeoJsonLocationEvent(self, external_id)
|
||||
# Add new entities to HA.
|
||||
self._add_entities(new_entities, True)
|
||||
self._add_entities([new_entity], True)
|
||||
|
||||
def _update_entities(self, external_ids):
|
||||
"""Update entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Existing entity found %s", external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
def _update_entity(self, external_id):
|
||||
"""Update entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
|
||||
def _remove_entities(self, external_ids):
|
||||
"""Remove entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Entity not current anymore %s", external_id)
|
||||
self._managed_external_ids.remove(external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
def _remove_entity(self, external_id):
|
||||
"""Remove entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
|
||||
|
||||
class GeoJsonLocationEvent(GeoLocationEvent):
|
||||
|
@ -184,7 +151,7 @@ class GeoJsonLocationEvent(GeoLocationEvent):
|
|||
async def async_update(self):
|
||||
"""Update this entity from the data held in the feed manager."""
|
||||
_LOGGER.debug("Updating %s", self._external_id)
|
||||
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
|
||||
feed_entry = self._feed_manager.get_entry(self._external_id)
|
||||
if feed_entry:
|
||||
self._update_from_feed(feed_entry)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.geo_location import (
|
|||
PLATFORM_SCHEMA, GeoLocationEvent)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
|
@ -57,18 +57,23 @@ VALID_CATEGORIES = [
|
|||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CATEGORIES, default=[]):
|
||||
vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]),
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the GeoJSON Events platform."""
|
||||
"""Set up the NSW Rural Fire Service Feed platform."""
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
config.get(CONF_LONGITUDE, hass.config.longitude))
|
||||
radius_in_km = config[CONF_RADIUS]
|
||||
categories = config.get(CONF_CATEGORIES)
|
||||
# Initialize the entity manager.
|
||||
feed = NswRuralFireServiceFeedManager(
|
||||
hass, add_entities, scan_interval, radius_in_km, categories)
|
||||
feed = NswRuralFireServiceFeedEntityManager(
|
||||
hass, add_entities, scan_interval, coordinates, radius_in_km,
|
||||
categories)
|
||||
|
||||
def start_feed_manager(event):
|
||||
"""Start feed manager."""
|
||||
|
@ -77,93 +82,55 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
|
||||
|
||||
class NswRuralFireServiceFeedManager:
|
||||
"""Feed Manager for NSW Rural Fire Service GeoJSON feed."""
|
||||
class NswRuralFireServiceFeedEntityManager:
|
||||
"""Feed Entity Manager for NSW Rural Fire Service GeoJSON feed."""
|
||||
|
||||
def __init__(self, hass, add_entities, scan_interval, radius_in_km,
|
||||
categories):
|
||||
"""Initialize the GeoJSON Feed Manager."""
|
||||
def __init__(self, hass, add_entities, scan_interval, coordinates,
|
||||
radius_in_km, categories):
|
||||
"""Initialize the Feed Entity Manager."""
|
||||
from geojson_client.nsw_rural_fire_service_feed \
|
||||
import NswRuralFireServiceFeed
|
||||
import NswRuralFireServiceFeedManager
|
||||
|
||||
self._hass = hass
|
||||
self._feed = NswRuralFireServiceFeed(
|
||||
(hass.config.latitude, hass.config.longitude),
|
||||
filter_radius=radius_in_km, filter_categories=categories)
|
||||
self._feed_manager = NswRuralFireServiceFeedManager(
|
||||
self._generate_entity, self._update_entity, self._remove_entity,
|
||||
coordinates, filter_radius=radius_in_km,
|
||||
filter_categories=categories)
|
||||
self._add_entities = add_entities
|
||||
self._scan_interval = scan_interval
|
||||
self.feed_entries = {}
|
||||
self._managed_external_ids = set()
|
||||
|
||||
def startup(self):
|
||||
"""Start up this manager."""
|
||||
self._update()
|
||||
self._feed_manager.update()
|
||||
self._init_regular_updates()
|
||||
|
||||
def _init_regular_updates(self):
|
||||
"""Schedule regular updates at the specified interval."""
|
||||
track_time_interval(
|
||||
self._hass, lambda now: self._update(), self._scan_interval)
|
||||
self._hass, lambda now: self._feed_manager.update(),
|
||||
self._scan_interval)
|
||||
|
||||
def _update(self):
|
||||
"""Update the feed and then update connected entities."""
|
||||
import geojson_client
|
||||
def get_entry(self, external_id):
|
||||
"""Get feed entry by external id."""
|
||||
return self._feed_manager.feed_entries.get(external_id)
|
||||
|
||||
status, feed_entries = self._feed.update()
|
||||
if status == geojson_client.UPDATE_OK:
|
||||
_LOGGER.debug("Data retrieved %s", feed_entries)
|
||||
# Keep a copy of all feed entries for future lookups by entities.
|
||||
self.feed_entries = {entry.external_id: entry
|
||||
for entry in feed_entries}
|
||||
# For entity management the external ids from the feed are used.
|
||||
feed_external_ids = set(self.feed_entries)
|
||||
remove_external_ids = self._managed_external_ids.difference(
|
||||
feed_external_ids)
|
||||
self._remove_entities(remove_external_ids)
|
||||
update_external_ids = self._managed_external_ids.intersection(
|
||||
feed_external_ids)
|
||||
self._update_entities(update_external_ids)
|
||||
create_external_ids = feed_external_ids.difference(
|
||||
self._managed_external_ids)
|
||||
self._generate_new_entities(create_external_ids)
|
||||
elif status == geojson_client.UPDATE_OK_NO_DATA:
|
||||
_LOGGER.debug(
|
||||
"Update successful, but no data received from %s", self._feed)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Update not successful, no data received from %s", self._feed)
|
||||
# Remove all entities.
|
||||
self._remove_entities(self._managed_external_ids.copy())
|
||||
|
||||
def _generate_new_entities(self, external_ids):
|
||||
"""Generate new entities for events."""
|
||||
new_entities = []
|
||||
for external_id in external_ids:
|
||||
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
|
||||
_LOGGER.debug("New entity added %s", external_id)
|
||||
new_entities.append(new_entity)
|
||||
self._managed_external_ids.add(external_id)
|
||||
def _generate_entity(self, external_id):
|
||||
"""Generate new entity."""
|
||||
new_entity = NswRuralFireServiceLocationEvent(self, external_id)
|
||||
# Add new entities to HA.
|
||||
self._add_entities(new_entities, True)
|
||||
self._add_entities([new_entity], True)
|
||||
|
||||
def _update_entities(self, external_ids):
|
||||
"""Update entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Existing entity found %s", external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
def _update_entity(self, external_id):
|
||||
"""Update entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
|
||||
def _remove_entities(self, external_ids):
|
||||
"""Remove entities."""
|
||||
for external_id in external_ids:
|
||||
_LOGGER.debug("Entity not current anymore %s", external_id)
|
||||
self._managed_external_ids.remove(external_id)
|
||||
dispatcher_send(
|
||||
self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
def _remove_entity(self, external_id):
|
||||
"""Remove entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
|
||||
|
||||
class NswRuralFireServiceLocationEvent(GeoLocationEvent):
|
||||
"""This represents an external event with GeoJSON data."""
|
||||
"""This represents an external event with NSW Rural Fire Service data."""
|
||||
|
||||
def __init__(self, feed_manager, external_id):
|
||||
"""Initialize entity with data from feed entry."""
|
||||
|
@ -209,13 +176,13 @@ class NswRuralFireServiceLocationEvent(GeoLocationEvent):
|
|||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for GeoJSON location events."""
|
||||
"""No polling needed for NSW Rural Fire Service location events."""
|
||||
return False
|
||||
|
||||
async def async_update(self):
|
||||
"""Update this entity from the data held in the feed manager."""
|
||||
_LOGGER.debug("Updating %s", self._external_id)
|
||||
feed_entry = self._feed_manager.feed_entries.get(self._external_id)
|
||||
feed_entry = self._feed_manager.get_entry(self._external_id)
|
||||
if feed_entry:
|
||||
self._update_from_feed(feed_entry)
|
||||
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
"""
|
||||
U.S. Geological Survey Earthquake Hazards Program Feed platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/geo_location/usgs_earthquakes_feed/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.geo_location import (
|
||||
PLATFORM_SCHEMA, GeoLocationEvent)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_RADIUS, CONF_SCAN_INTERVAL,
|
||||
EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, dispatcher_send)
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
|
||||
REQUIREMENTS = ['geojson_client==0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ALERT = 'alert'
|
||||
ATTR_EXTERNAL_ID = 'external_id'
|
||||
ATTR_MAGNITUDE = 'magnitude'
|
||||
ATTR_PLACE = 'place'
|
||||
ATTR_STATUS = 'status'
|
||||
ATTR_TIME = 'time'
|
||||
ATTR_TYPE = 'type'
|
||||
ATTR_UPDATED = 'updated'
|
||||
|
||||
CONF_FEED_TYPE = 'feed_type'
|
||||
CONF_MINIMUM_MAGNITUDE = 'minimum_magnitude'
|
||||
|
||||
DEFAULT_MINIMUM_MAGNITUDE = 0.0
|
||||
DEFAULT_RADIUS_IN_KM = 50.0
|
||||
DEFAULT_UNIT_OF_MEASUREMENT = 'km'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
SIGNAL_DELETE_ENTITY = 'usgs_earthquakes_feed_delete_{}'
|
||||
SIGNAL_UPDATE_ENTITY = 'usgs_earthquakes_feed_update_{}'
|
||||
|
||||
SOURCE = 'usgs_earthquakes_feed'
|
||||
|
||||
VALID_FEED_TYPES = [
|
||||
'past_hour_significant_earthquakes',
|
||||
'past_hour_m45_earthquakes',
|
||||
'past_hour_m25_earthquakes',
|
||||
'past_hour_m10_earthquakes',
|
||||
'past_hour_all_earthquakes',
|
||||
'past_day_significant_earthquakes',
|
||||
'past_day_m45_earthquakes',
|
||||
'past_day_m25_earthquakes',
|
||||
'past_day_m10_earthquakes',
|
||||
'past_day_all_earthquakes',
|
||||
'past_week_significant_earthquakes',
|
||||
'past_week_m45_earthquakes',
|
||||
'past_week_m25_earthquakes',
|
||||
'past_week_m10_earthquakes',
|
||||
'past_week_all_earthquakes',
|
||||
'past_month_significant_earthquakes',
|
||||
'past_month_m45_earthquakes',
|
||||
'past_month_m25_earthquakes',
|
||||
'past_month_m10_earthquakes',
|
||||
'past_month_all_earthquakes',
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_FEED_TYPE): vol.In(VALID_FEED_TYPES),
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
|
||||
vol.Optional(CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0))
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the USGS Earthquake Hazards Program Feed platform."""
|
||||
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
feed_type = config[CONF_FEED_TYPE]
|
||||
coordinates = (config.get(CONF_LATITUDE, hass.config.latitude),
|
||||
config.get(CONF_LONGITUDE, hass.config.longitude))
|
||||
radius_in_km = config[CONF_RADIUS]
|
||||
minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE]
|
||||
# Initialize the entity manager.
|
||||
feed = UsgsEarthquakesFeedEntityManager(
|
||||
hass, add_entities, scan_interval, coordinates, feed_type,
|
||||
radius_in_km, minimum_magnitude)
|
||||
|
||||
def start_feed_manager(event):
|
||||
"""Start feed manager."""
|
||||
feed.startup()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
|
||||
|
||||
|
||||
class UsgsEarthquakesFeedEntityManager:
|
||||
"""Feed Entity Manager for USGS Earthquake Hazards Program feed."""
|
||||
|
||||
def __init__(self, hass, add_entities, scan_interval, coordinates,
|
||||
feed_type, radius_in_km, minimum_magnitude):
|
||||
"""Initialize the Feed Entity Manager."""
|
||||
from geojson_client.usgs_earthquake_hazards_program_feed \
|
||||
import UsgsEarthquakeHazardsProgramFeedManager
|
||||
|
||||
self._hass = hass
|
||||
self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager(
|
||||
self._generate_entity, self._update_entity, self._remove_entity,
|
||||
coordinates, feed_type, filter_radius=radius_in_km,
|
||||
filter_minimum_magnitude=minimum_magnitude)
|
||||
self._add_entities = add_entities
|
||||
self._scan_interval = scan_interval
|
||||
|
||||
def startup(self):
|
||||
"""Start up this manager."""
|
||||
self._feed_manager.update()
|
||||
self._init_regular_updates()
|
||||
|
||||
def _init_regular_updates(self):
|
||||
"""Schedule regular updates at the specified interval."""
|
||||
track_time_interval(
|
||||
self._hass, lambda now: self._feed_manager.update(),
|
||||
self._scan_interval)
|
||||
|
||||
def get_entry(self, external_id):
|
||||
"""Get feed entry by external id."""
|
||||
return self._feed_manager.feed_entries.get(external_id)
|
||||
|
||||
def _generate_entity(self, external_id):
|
||||
"""Generate new entity."""
|
||||
new_entity = UsgsEarthquakesEvent(self, external_id)
|
||||
# Add new entities to HA.
|
||||
self._add_entities([new_entity], True)
|
||||
|
||||
def _update_entity(self, external_id):
|
||||
"""Update entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
|
||||
|
||||
def _remove_entity(self, external_id):
|
||||
"""Remove entity."""
|
||||
dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
|
||||
|
||||
|
||||
class UsgsEarthquakesEvent(GeoLocationEvent):
|
||||
"""This represents an external event with USGS Earthquake data."""
|
||||
|
||||
def __init__(self, feed_manager, external_id):
|
||||
"""Initialize entity with data from feed entry."""
|
||||
self._feed_manager = feed_manager
|
||||
self._external_id = external_id
|
||||
self._name = None
|
||||
self._distance = None
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._attribution = None
|
||||
self._place = None
|
||||
self._magnitude = None
|
||||
self._time = None
|
||||
self._updated = None
|
||||
self._status = None
|
||||
self._type = None
|
||||
self._alert = None
|
||||
self._remove_signal_delete = None
|
||||
self._remove_signal_update = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self._remove_signal_delete = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_DELETE_ENTITY.format(self._external_id),
|
||||
self._delete_callback)
|
||||
self._remove_signal_update = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ENTITY.format(self._external_id),
|
||||
self._update_callback)
|
||||
|
||||
@callback
|
||||
def _delete_callback(self):
|
||||
"""Remove this entity."""
|
||||
self._remove_signal_delete()
|
||||
self._remove_signal_update()
|
||||
self.hass.async_create_task(self.async_remove())
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed for USGS Earthquake events."""
|
||||
return False
|
||||
|
||||
async def async_update(self):
|
||||
"""Update this entity from the data held in the feed manager."""
|
||||
_LOGGER.debug("Updating %s", self._external_id)
|
||||
feed_entry = self._feed_manager.get_entry(self._external_id)
|
||||
if feed_entry:
|
||||
self._update_from_feed(feed_entry)
|
||||
|
||||
def _update_from_feed(self, feed_entry):
|
||||
"""Update the internal state from the provided feed entry."""
|
||||
self._name = feed_entry.title
|
||||
self._distance = feed_entry.distance_to_home
|
||||
self._latitude = feed_entry.coordinates[0]
|
||||
self._longitude = feed_entry.coordinates[1]
|
||||
self._attribution = feed_entry.attribution
|
||||
self._place = feed_entry.place
|
||||
self._magnitude = feed_entry.magnitude
|
||||
self._time = feed_entry.time
|
||||
self._updated = feed_entry.updated
|
||||
self._status = feed_entry.status
|
||||
self._type = feed_entry.type
|
||||
self._alert = feed_entry.alert
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
"""Return source value of this external event."""
|
||||
return SOURCE
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def distance(self) -> Optional[float]:
|
||||
"""Return distance value of this external event."""
|
||||
return self._distance
|
||||
|
||||
@property
|
||||
def latitude(self) -> Optional[float]:
|
||||
"""Return latitude value of this external event."""
|
||||
return self._latitude
|
||||
|
||||
@property
|
||||
def longitude(self) -> Optional[float]:
|
||||
"""Return longitude value of this external event."""
|
||||
return self._longitude
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return DEFAULT_UNIT_OF_MEASUREMENT
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attributes = {}
|
||||
for key, value in (
|
||||
(ATTR_EXTERNAL_ID, self._external_id),
|
||||
(ATTR_PLACE, self._place),
|
||||
(ATTR_MAGNITUDE, self._magnitude),
|
||||
(ATTR_TIME, self._time),
|
||||
(ATTR_UPDATED, self._updated),
|
||||
(ATTR_STATUS, self._status),
|
||||
(ATTR_TYPE, self._type),
|
||||
(ATTR_ALERT, self._alert),
|
||||
(ATTR_ATTRIBUTION, self._attribution),
|
||||
):
|
||||
if value or isinstance(value, bool):
|
||||
attributes[key] = value
|
||||
return attributes
|
|
@ -12,8 +12,9 @@ import voluptuous as vol
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
REQUIREMENTS = ['pysher==1.0.4']
|
||||
|
||||
# Version downgraded due to regression in library
|
||||
# For details: https://github.com/nlsdfnbch/Pysher/issues/38
|
||||
REQUIREMENTS = ['pysher==1.0.1']
|
||||
DOMAIN = 'goalfeed'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
|
|
|
@ -33,8 +33,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
DEFAULT_AGENT_USER_ID = 'home-assistant'
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_EXPOSE): cv.boolean,
|
||||
|
@ -70,10 +68,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
|||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
with async_timeout.timeout(5, loop=hass.loop):
|
||||
agent_user_id = call.data.get('agent_user_id') or \
|
||||
call.context.user_id
|
||||
res = await websession.post(
|
||||
REQUEST_SYNC_BASE_URL,
|
||||
params={'key': api_key},
|
||||
json={'agent_user_id': call.context.user_id})
|
||||
json={'agent_user_id': agent_user_id})
|
||||
_LOGGER.info("Submitted request_sync request to Google")
|
||||
res.raise_for_status()
|
||||
except aiohttp.ClientResponseError:
|
||||
|
|
|
@ -19,8 +19,6 @@ DEFAULT_EXPOSED_DOMAINS = [
|
|||
'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock',
|
||||
]
|
||||
DEFAULT_ALLOW_UNLOCK = False
|
||||
CLIMATE_MODE_HEATCOOL = 'heatcool'
|
||||
CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL}
|
||||
|
||||
PREFIX_TYPES = 'action.devices.types.'
|
||||
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
|
||||
|
|
|
@ -48,7 +48,7 @@ def async_register_http(hass, cfg):
|
|||
entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE)
|
||||
|
||||
domain_exposed_by_default = \
|
||||
expose_by_default or entity.domain in exposed_domains
|
||||
expose_by_default and entity.domain in exposed_domains
|
||||
|
||||
# Expose an entity if the entity's domain is exposed by default and
|
||||
# the configuration doesn't explicitly exclude it from being
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
request_sync:
|
||||
description: Send a request_sync command to Google.
|
||||
description: Send a request_sync command to Google.
|
||||
fields:
|
||||
agent_user_id:
|
||||
description: Optional. Only needed for automations. Specific Home Assistant user id to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing.
|
||||
|
|
|
@ -43,6 +43,7 @@ TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
|
|||
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
|
||||
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
|
||||
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
|
||||
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
|
||||
|
||||
PREFIX_COMMANDS = 'action.devices.commands.'
|
||||
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
|
||||
|
@ -59,7 +60,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
|
|||
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
|
||||
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
|
||||
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
|
||||
|
||||
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
|
||||
|
||||
TRAITS = []
|
||||
|
||||
|
@ -197,6 +198,8 @@ class OnOffTrait(_Trait):
|
|||
@staticmethod
|
||||
def supported(domain, features):
|
||||
"""Test if state is supported."""
|
||||
if domain == climate.DOMAIN:
|
||||
return features & climate.SUPPORT_ON_OFF != 0
|
||||
return domain in (
|
||||
group.DOMAIN,
|
||||
input_boolean.DOMAIN,
|
||||
|
@ -515,6 +518,9 @@ class TemperatureSettingTrait(_Trait):
|
|||
climate.STATE_COOL: 'cool',
|
||||
climate.STATE_OFF: 'off',
|
||||
climate.STATE_AUTO: 'heatcool',
|
||||
climate.STATE_FAN_ONLY: 'fan-only',
|
||||
climate.STATE_DRY: 'dry',
|
||||
climate.STATE_ECO: 'eco'
|
||||
}
|
||||
google_to_hass = {value: key for key, value in hass_to_google.items()}
|
||||
|
||||
|
@ -585,8 +591,11 @@ class TemperatureSettingTrait(_Trait):
|
|||
max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
|
||||
|
||||
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
|
||||
temp = temp_util.convert(params['thermostatTemperatureSetpoint'],
|
||||
TEMP_CELSIUS, unit)
|
||||
temp = temp_util.convert(
|
||||
params['thermostatTemperatureSetpoint'], TEMP_CELSIUS,
|
||||
unit)
|
||||
if unit == TEMP_FAHRENHEIT:
|
||||
temp = round(temp)
|
||||
|
||||
if temp < min_temp or temp > max_temp:
|
||||
raise SmartHomeError(
|
||||
|
@ -604,6 +613,8 @@ class TemperatureSettingTrait(_Trait):
|
|||
temp_high = temp_util.convert(
|
||||
params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS,
|
||||
unit)
|
||||
if unit == TEMP_FAHRENHEIT:
|
||||
temp_high = round(temp_high)
|
||||
|
||||
if temp_high < min_temp or temp_high > max_temp:
|
||||
raise SmartHomeError(
|
||||
|
@ -612,7 +623,10 @@ class TemperatureSettingTrait(_Trait):
|
|||
"{} and {}".format(min_temp, max_temp))
|
||||
|
||||
temp_low = temp_util.convert(
|
||||
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS, unit)
|
||||
params['thermostatTemperatureSetpointLow'], TEMP_CELSIUS,
|
||||
unit)
|
||||
if unit == TEMP_FAHRENHEIT:
|
||||
temp_low = round(temp_low)
|
||||
|
||||
if temp_low < min_temp or temp_low > max_temp:
|
||||
raise SmartHomeError(
|
||||
|
@ -752,3 +766,188 @@ class FanSpeedTrait(_Trait):
|
|||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
fan.ATTR_SPEED: params['fanSpeed']
|
||||
}, blocking=True)
|
||||
|
||||
|
||||
@register_trait
|
||||
class ModesTrait(_Trait):
|
||||
"""Trait to set modes.
|
||||
|
||||
https://developers.google.com/actions/smarthome/traits/modes
|
||||
"""
|
||||
|
||||
name = TRAIT_MODES
|
||||
commands = [
|
||||
COMMAND_MODES
|
||||
]
|
||||
|
||||
# Google requires specific mode names and settings. Here is the full list.
|
||||
# https://developers.google.com/actions/reference/smarthome/traits/modes
|
||||
# All settings are mapped here as of 2018-11-28 and can be used for other
|
||||
# entity types.
|
||||
|
||||
HA_TO_GOOGLE = {
|
||||
media_player.ATTR_INPUT_SOURCE: "input source",
|
||||
}
|
||||
SUPPORTED_MODE_SETTINGS = {
|
||||
'xsmall': [
|
||||
'xsmall', 'extra small', 'min', 'minimum', 'tiny', 'xs'],
|
||||
'small': ['small', 'half'],
|
||||
'large': ['large', 'big', 'full'],
|
||||
'xlarge': ['extra large', 'xlarge', 'xl'],
|
||||
'Cool': ['cool', 'rapid cool', 'rapid cooling'],
|
||||
'Heat': ['heat'], 'Low': ['low'],
|
||||
'Medium': ['medium', 'med', 'mid', 'half'],
|
||||
'High': ['high'],
|
||||
'Auto': ['auto', 'automatic'],
|
||||
'Bake': ['bake'], 'Roast': ['roast'],
|
||||
'Convection Bake': ['convection bake', 'convect bake'],
|
||||
'Convection Roast': ['convection roast', 'convect roast'],
|
||||
'Favorite': ['favorite'],
|
||||
'Broil': ['broil'],
|
||||
'Warm': ['warm'],
|
||||
'Off': ['off'],
|
||||
'On': ['on'],
|
||||
'Normal': [
|
||||
'normal', 'normal mode', 'normal setting', 'standard',
|
||||
'schedule', 'original', 'default', 'old settings'
|
||||
],
|
||||
'None': ['none'],
|
||||
'Tap Cold': ['tap cold'],
|
||||
'Cold Warm': ['cold warm'],
|
||||
'Hot': ['hot'],
|
||||
'Extra Hot': ['extra hot'],
|
||||
'Eco': ['eco'],
|
||||
'Wool': ['wool', 'fleece'],
|
||||
'Turbo': ['turbo'],
|
||||
'Rinse': ['rinse', 'rinsing', 'rinse wash'],
|
||||
'Away': ['away', 'holiday'],
|
||||
'maximum': ['maximum'],
|
||||
'media player': ['media player'],
|
||||
'chromecast': ['chromecast'],
|
||||
'tv': [
|
||||
'tv', 'television', 'tv position', 'television position',
|
||||
'watching tv', 'watching tv position', 'entertainment',
|
||||
'entertainment position'
|
||||
],
|
||||
'am fm': ['am fm', 'am radio', 'fm radio'],
|
||||
'internet radio': ['internet radio'],
|
||||
'satellite': ['satellite'],
|
||||
'game console': ['game console'],
|
||||
'antifrost': ['antifrost', 'anti-frost'],
|
||||
'boost': ['boost'],
|
||||
'Clock': ['clock'],
|
||||
'Message': ['message'],
|
||||
'Messages': ['messages'],
|
||||
'News': ['news'],
|
||||
'Disco': ['disco'],
|
||||
'antifreeze': ['antifreeze', 'anti-freeze', 'anti freeze'],
|
||||
'balanced': ['balanced', 'normal'],
|
||||
'swing': ['swing'],
|
||||
'media': ['media', 'media mode'],
|
||||
'panic': ['panic'],
|
||||
'ring': ['ring'],
|
||||
'frozen': ['frozen', 'rapid frozen', 'rapid freeze'],
|
||||
'cotton': ['cotton', 'cottons'],
|
||||
'blend': ['blend', 'mix'],
|
||||
'baby wash': ['baby wash'],
|
||||
'synthetics': ['synthetic', 'synthetics', 'compose'],
|
||||
'hygiene': ['hygiene', 'sterilization'],
|
||||
'smart': ['smart', 'intelligent', 'intelligence'],
|
||||
'comfortable': ['comfortable', 'comfort'],
|
||||
'manual': ['manual'],
|
||||
'energy saving': ['energy saving'],
|
||||
'sleep': ['sleep'],
|
||||
'quick wash': ['quick wash', 'fast wash'],
|
||||
'cold': ['cold'],
|
||||
'airsupply': ['airsupply', 'air supply'],
|
||||
'dehumidification': ['dehumidication', 'dehumidify'],
|
||||
'game': ['game', 'game mode']
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def supported(domain, features):
|
||||
"""Test if state is supported."""
|
||||
if domain != media_player.DOMAIN:
|
||||
return False
|
||||
|
||||
return features & media_player.SUPPORT_SELECT_SOURCE
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return mode attributes for a sync request."""
|
||||
sources_list = self.state.attributes.get(
|
||||
media_player.ATTR_INPUT_SOURCE_LIST, [])
|
||||
modes = []
|
||||
sources = {}
|
||||
|
||||
if sources_list:
|
||||
sources = {
|
||||
"name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE),
|
||||
"name_values": [{
|
||||
"name_synonym": ['input source'],
|
||||
"lang": "en"
|
||||
}],
|
||||
"settings": [],
|
||||
"ordered": False
|
||||
}
|
||||
for source in sources_list:
|
||||
if source in self.SUPPORTED_MODE_SETTINGS:
|
||||
src = source
|
||||
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
|
||||
elif source.lower() in self.SUPPORTED_MODE_SETTINGS:
|
||||
src = source.lower()
|
||||
synonyms = self.SUPPORTED_MODE_SETTINGS.get(src)
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
sources['settings'].append(
|
||||
{
|
||||
"setting_name": src,
|
||||
"setting_values": [{
|
||||
"setting_synonym": synonyms,
|
||||
"lang": "en"
|
||||
}]
|
||||
}
|
||||
)
|
||||
if sources:
|
||||
modes.append(sources)
|
||||
payload = {'availableModes': modes}
|
||||
|
||||
return payload
|
||||
|
||||
def query_attributes(self):
|
||||
"""Return current modes."""
|
||||
attrs = self.state.attributes
|
||||
response = {}
|
||||
mode_settings = {}
|
||||
|
||||
if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST):
|
||||
mode_settings.update({
|
||||
media_player.ATTR_INPUT_SOURCE: attrs.get(
|
||||
media_player.ATTR_INPUT_SOURCE)
|
||||
})
|
||||
if mode_settings:
|
||||
response['on'] = self.state.state != STATE_OFF
|
||||
response['online'] = True
|
||||
response['currentModeSettings'] = mode_settings
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, params):
|
||||
"""Execute an SetModes command."""
|
||||
settings = params.get('updateModeSettings')
|
||||
requested_source = settings.get(
|
||||
self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE))
|
||||
|
||||
if requested_source:
|
||||
for src in self.state.attributes.get(
|
||||
media_player.ATTR_INPUT_SOURCE_LIST):
|
||||
if src.lower() == requested_source.lower():
|
||||
source = src
|
||||
|
||||
await self.hass.services.async_call(
|
||||
media_player.DOMAIN,
|
||||
media_player.SERVICE_SELECT_SOURCE, {
|
||||
ATTR_ENTITY_ID: self.state.entity_id,
|
||||
media_player.ATTR_INPUT_SOURCE: source
|
||||
}, blocking=True)
|
||||
|
|
|
@ -213,13 +213,7 @@ async def async_setup(hass, config):
|
|||
embed_iframe=True,
|
||||
)
|
||||
|
||||
# Temporary. No refresh token tells supervisor to use API password.
|
||||
if hass.auth.active:
|
||||
token = refresh_token.token
|
||||
else:
|
||||
token = None
|
||||
|
||||
await hassio.update_hass_api(config.get('http', {}), token)
|
||||
await hassio.update_hass_api(config.get('http', {}), refresh_token.token)
|
||||
|
||||
if 'homeassistant' in config:
|
||||
await hassio.update_hass_timezone(config['homeassistant'])
|
||||
|
|
|
@ -15,7 +15,6 @@ from aiohttp import web
|
|||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
from aiohttp.web_exceptions import HTTPBadGateway
|
||||
|
||||
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
|
||||
from .const import X_HASSIO
|
||||
|
@ -63,8 +62,6 @@ class HassIOView(HomeAssistantView):
|
|||
client = await self._command_proxy(path, request)
|
||||
|
||||
data = await client.read()
|
||||
if path.endswith('/logs'):
|
||||
return _create_response_log(client, data)
|
||||
return _create_response(client, data)
|
||||
|
||||
get = _handle
|
||||
|
@ -114,18 +111,6 @@ def _create_response(client, data):
|
|||
)
|
||||
|
||||
|
||||
def _create_response_log(client, data):
|
||||
"""Convert a response from client request."""
|
||||
# Remove color codes
|
||||
log = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", data.decode())
|
||||
|
||||
return web.Response(
|
||||
text=log,
|
||||
status=client.status,
|
||||
content_type=CONTENT_TYPE_TEXT_PLAIN,
|
||||
)
|
||||
|
||||
|
||||
def _get_timeout(path):
|
||||
"""Return timeout for a URL path."""
|
||||
if NO_TIMEOUT.match(path):
|
||||
|
|
|
@ -320,38 +320,39 @@ def setup(hass: HomeAssistant, base_config):
|
|||
class CecDevice(Entity):
|
||||
"""Representation of a HDMI CEC device entity."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, device, logical) -> None:
|
||||
def __init__(self, device, logical) -> None:
|
||||
"""Initialize the device."""
|
||||
self._device = device
|
||||
self.hass = hass
|
||||
self._icon = None
|
||||
self._state = STATE_UNKNOWN
|
||||
self._logical_address = logical
|
||||
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
|
||||
device.set_update_callback(self._update)
|
||||
|
||||
def update(self):
|
||||
"""Update device status."""
|
||||
self._update()
|
||||
device = self._device
|
||||
from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \
|
||||
POWER_OFF, POWER_ON
|
||||
if device.power_status in [POWER_OFF, 3]:
|
||||
self._state = STATE_OFF
|
||||
elif device.status == STATUS_PLAY:
|
||||
self._state = STATE_PLAYING
|
||||
elif device.status == STATUS_STOP:
|
||||
self._state = STATE_IDLE
|
||||
elif device.status == STATUS_STILL:
|
||||
self._state = STATE_PAUSED
|
||||
elif device.power_status in [POWER_ON, 4]:
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
_LOGGER.warning("Unknown state: %d", device.power_status)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register HDMI callbacks after initialization."""
|
||||
self._device.set_update_callback(self._update)
|
||||
|
||||
def _update(self, device=None):
|
||||
"""Update device status."""
|
||||
if device:
|
||||
from pycec.const import STATUS_PLAY, STATUS_STOP, STATUS_STILL, \
|
||||
POWER_OFF, POWER_ON
|
||||
if device.power_status == POWER_OFF:
|
||||
self._state = STATE_OFF
|
||||
elif device.status == STATUS_PLAY:
|
||||
self._state = STATE_PLAYING
|
||||
elif device.status == STATUS_STOP:
|
||||
self._state = STATE_IDLE
|
||||
elif device.status == STATUS_STILL:
|
||||
self._state = STATE_PAUSED
|
||||
elif device.power_status == POWER_ON:
|
||||
self._state = STATE_ON
|
||||
else:
|
||||
_LOGGER.warning("Unknown state: %d", device.power_status)
|
||||
self.schedule_update_ha_state()
|
||||
"""Device status changed, schedule an update."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -38,20 +38,6 @@ SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
|
|||
IGNORE_DOMAINS = ('zone', 'scene',)
|
||||
|
||||
|
||||
def last_recorder_run(hass):
|
||||
"""Retrieve the last closed recorder run from the database."""
|
||||
from homeassistant.components.recorder.models import RecorderRuns
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
res = (session.query(RecorderRuns)
|
||||
.filter(RecorderRuns.end.isnot(None))
|
||||
.order_by(RecorderRuns.end.desc()).first())
|
||||
if res is None:
|
||||
return None
|
||||
session.expunge(res)
|
||||
return res
|
||||
|
||||
|
||||
def get_significant_states(hass, start_time, end_time=None, entity_ids=None,
|
||||
filters=None, include_start_time_state=True):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
"""
|
||||
Support for HLK-SW16 relay switch.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/hlk_sw16/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP, CONF_SWITCHES, CONF_NAME)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
|
||||
REQUIREMENTS = ['hlk-sw16==0.0.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_DEVICE_REGISTER = 'hlk_sw16_device_register'
|
||||
DEFAULT_RECONNECT_INTERVAL = 10
|
||||
CONNECTION_TIMEOUT = 10
|
||||
DEFAULT_PORT = 8080
|
||||
|
||||
DOMAIN = 'hlk_sw16'
|
||||
|
||||
SIGNAL_AVAILABILITY = 'hlk_sw16_device_available_{}'
|
||||
|
||||
SWITCH_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
RELAY_ID = vol.All(
|
||||
vol.Any(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f'),
|
||||
vol.Coerce(str))
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.string: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Required(CONF_SWITCHES): vol.Schema({RELAY_ID: SWITCH_SCHEMA}),
|
||||
}),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the HLK-SW16 switch."""
|
||||
# Allow platform to specify function to register new unknown devices
|
||||
from hlk_sw16 import create_hlk_sw16_connection
|
||||
hass.data[DATA_DEVICE_REGISTER] = {}
|
||||
|
||||
def add_device(device):
|
||||
switches = config[DOMAIN][device][CONF_SWITCHES]
|
||||
|
||||
host = config[DOMAIN][device][CONF_HOST]
|
||||
port = config[DOMAIN][device][CONF_PORT]
|
||||
|
||||
@callback
|
||||
def disconnected():
|
||||
"""Schedule reconnect after connection has been lost."""
|
||||
_LOGGER.warning('HLK-SW16 %s disconnected', device)
|
||||
async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device),
|
||||
False)
|
||||
|
||||
@callback
|
||||
def reconnected():
|
||||
"""Schedule reconnect after connection has been lost."""
|
||||
_LOGGER.warning('HLK-SW16 %s connected', device)
|
||||
async_dispatcher_send(hass, SIGNAL_AVAILABILITY.format(device),
|
||||
True)
|
||||
|
||||
async def connect():
|
||||
"""Set up connection and hook it into HA for reconnect/shutdown."""
|
||||
_LOGGER.info('Initiating HLK-SW16 connection to %s', device)
|
||||
|
||||
client = await create_hlk_sw16_connection(
|
||||
host=host,
|
||||
port=port,
|
||||
disconnect_callback=disconnected,
|
||||
reconnect_callback=reconnected,
|
||||
loop=hass.loop,
|
||||
timeout=CONNECTION_TIMEOUT,
|
||||
reconnect_interval=DEFAULT_RECONNECT_INTERVAL)
|
||||
|
||||
hass.data[DATA_DEVICE_REGISTER][device] = client
|
||||
|
||||
# Load platforms
|
||||
hass.async_create_task(
|
||||
async_load_platform(hass, 'switch', DOMAIN,
|
||||
(switches, device),
|
||||
config))
|
||||
|
||||
# handle shutdown of HLK-SW16 asyncio transport
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
lambda x: client.stop())
|
||||
|
||||
_LOGGER.info('Connected to HLK-SW16 device: %s', device)
|
||||
|
||||
hass.loop.create_task(connect())
|
||||
|
||||
for device in config[DOMAIN]:
|
||||
add_device(device)
|
||||
return True
|
||||
|
||||
|
||||
class SW16Device(Entity):
|
||||
"""Representation of a HLK-SW16 device.
|
||||
|
||||
Contains the common logic for HLK-SW16 entities.
|
||||
"""
|
||||
|
||||
def __init__(self, relay_name, device_port, device_id, client):
|
||||
"""Initialize the device."""
|
||||
# HLK-SW16 specific attributes for every component type
|
||||
self._device_id = device_id
|
||||
self._device_port = device_port
|
||||
self._is_on = None
|
||||
self._client = client
|
||||
self._name = relay_name
|
||||
|
||||
@callback
|
||||
def handle_event_callback(self, event):
|
||||
"""Propagate changes through ha."""
|
||||
_LOGGER.debug("Relay %s new state callback: %r",
|
||||
self._device_port, event)
|
||||
self._is_on = event
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return a name for the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return bool(self._client.is_connected)
|
||||
|
||||
@callback
|
||||
def _availability_callback(self, availability):
|
||||
"""Update availability state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register update callback."""
|
||||
self._client.register_status_callback(self.handle_event_callback,
|
||||
self._device_port)
|
||||
self._is_on = await self._client.status(self._device_port)
|
||||
async_dispatcher_connect(self.hass,
|
||||
SIGNAL_AVAILABILITY.format(self._device_id),
|
||||
self._availability_callback)
|
|
@ -13,12 +13,13 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD,
|
||||
CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
|
||||
CONF_PLATFORM, CONF_USERNAME, CONF_SSL, CONF_VERIFY_SSL,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.52']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.53']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -77,7 +78,8 @@ HM_DEVICE_TYPES = {
|
|||
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
|
||||
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
|
||||
'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus',
|
||||
'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage'],
|
||||
'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage',
|
||||
'UniversalSensor'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
|
@ -173,6 +175,9 @@ DEFAULT_PORT = 2001
|
|||
DEFAULT_PATH = ''
|
||||
DEFAULT_USERNAME = 'Admin'
|
||||
DEFAULT_PASSWORD = ''
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_VERIFY_SSL = False
|
||||
DEFAULT_CHANNEL = 1
|
||||
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
|
@ -180,7 +185,7 @@ DEVICE_SCHEMA = vol.Schema({
|
|||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_ADDRESS): cv.string,
|
||||
vol.Required(ATTR_INTERFACE): cv.string,
|
||||
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
|
||||
vol.Optional(ATTR_CHANNEL, default=DEFAULT_CHANNEL): vol.Coerce(int),
|
||||
vol.Optional(ATTR_PARAM): cv.string,
|
||||
vol.Optional(ATTR_UNIQUE_ID): cv.string,
|
||||
})
|
||||
|
@ -198,6 +203,9 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CALLBACK_IP): cv.string,
|
||||
vol.Optional(CONF_CALLBACK_PORT): cv.port,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Optional(
|
||||
CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
|
||||
}},
|
||||
vol.Optional(CONF_HOSTS, default={}): {cv.match_all: {
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
|
@ -268,6 +276,8 @@ def setup(hass, config):
|
|||
'password': rconfig.get(CONF_PASSWORD),
|
||||
'callbackip': rconfig.get(CONF_CALLBACK_IP),
|
||||
'callbackport': rconfig.get(CONF_CALLBACK_PORT),
|
||||
'ssl': rconfig.get(CONF_SSL),
|
||||
'verify_ssl': rconfig.get(CONF_VERIFY_SSL),
|
||||
'connect': True,
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"title": "Trieu el punt d'acc\u00e9s HomematicIP"
|
||||
},
|
||||
"link": {
|
||||
"description": "Premeu el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 de enviar per registrar HomematicIP amb Home Assistent. \n\n ![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)",
|
||||
"description": "Premeu el bot\u00f3 blau del punt d'acc\u00e9s i el bot\u00f3 de enviar per registrar HomematicIP amb Home Assistent. \n\n![Ubicaci\u00f3 del bot\u00f3 al pont](/static/images/config_flows/config_homematicip_cloud.png)",
|
||||
"title": "Enlla\u00e7ar punt d'acc\u00e9s"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432)",
|
||||
"pin": "PIN-\u043a\u043e\u0434 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)"
|
||||
},
|
||||
"title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 HomematicIP"
|
||||
"title": "HomematicIP Cloud"
|
||||
},
|
||||
"link": {
|
||||
"description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u043a\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c HomematicIP \u0432 Home Assistant. \n\n ![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438](/static/images/config_flows/config_homematicip_cloud.png)",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue