Merge pull request #19215 from home-assistant/rc

0.84
This commit is contained in:
Paulus Schoutsen 2018-12-12 14:17:53 +01:00 committed by GitHub
commit 88cda043ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
462 changed files with 14415 additions and 8348 deletions

View File

@ -148,6 +148,9 @@ omit =
homeassistant/components/hive.py homeassistant/components/hive.py
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/__init__.py
homeassistant/components/*/homekit_controller.py homeassistant/components/*/homekit_controller.py
@ -203,6 +206,9 @@ omit =
homeassistant/components/linode.py homeassistant/components/linode.py
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
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/*/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/*/tellduslive.py
homeassistant/components/tellstick.py homeassistant/components/tellstick.py
@ -400,6 +407,8 @@ omit =
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py homeassistant/components/*/zha.py
homeassistant/components/zigbee.py homeassistant/components/zigbee.py
@ -637,7 +646,6 @@ omit =
homeassistant/components/notify/group.py homeassistant/components/notify/group.py
homeassistant/components/notify/hipchat.py homeassistant/components/notify/hipchat.py
homeassistant/components/notify/homematic.py homeassistant/components/notify/homematic.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/kodi.py homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py homeassistant/components/notify/lannouncer.py
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
@ -780,6 +788,7 @@ omit =
homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py homeassistant/components/sensor/pyload.py
homeassistant/components/sensor/qbittorrent.py
homeassistant/components/sensor/qnap.py homeassistant/components/sensor/qnap.py
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py

View File

@ -51,6 +51,7 @@ homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/binary_sensor/hikvision.py @mezz64
homeassistant/components/binary_sensor/threshold.py @fabaff homeassistant/components/binary_sensor/threshold.py @fabaff
homeassistant/components/binary_sensor/uptimerobot.py @ludeeus
homeassistant/components/camera/yi.py @bachya homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/eq3btsmart.py @rytilahti
@ -61,9 +62,11 @@ homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/asuswrt.py @kennedyshead homeassistant/components/device_tracker/asuswrt.py @kennedyshead
homeassistant/components/device_tracker/automatic.py @armills 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/huawei_router.py @abmantis
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan
homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/device_tracker/tile.py @bachya
homeassistant/components/device_tracker/traccar.py @ludeeus
homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme homeassistant/components/device_tracker/bt_smarthub.py @jxwolstenholme
homeassistant/components/history_graph.py @andrey-git homeassistant/components/history_graph.py @andrey-git
homeassistant/components/influx.py @fabaff homeassistant/components/influx.py @fabaff
@ -109,6 +112,7 @@ homeassistant/components/sensor/glances.py @fabaff
homeassistant/components/sensor/gpsd.py @fabaff homeassistant/components/sensor/gpsd.py @fabaff
homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/launch_library.py @ludeeus
homeassistant/components/sensor/linux_battery.py @fabaff homeassistant/components/sensor/linux_battery.py @fabaff
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/min_max.py @fabaff 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/pollen.py @bachya
homeassistant/components/sensor/pvoutput.py @fabaff homeassistant/components/sensor/pvoutput.py @fabaff
homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/ruter.py @ludeeus
homeassistant/components/sensor/scrape.py @fabaff homeassistant/components/sensor/scrape.py @fabaff
homeassistant/components/sensor/serial.py @fabaff homeassistant/components/sensor/serial.py @fabaff
homeassistant/components/sensor/seventeentrack.py @bachya 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/statistics.py @fabaff
homeassistant/components/sensor/swiss*.py @fabaff homeassistant/components/sensor/swiss*.py @fabaff
homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tautulli.py @ludeeus
homeassistant/components/sensor/time_data.py @fabaff homeassistant/components/sensor/time_data.py @fabaff
homeassistant/components/sensor/version.py @fabaff homeassistant/components/sensor/version.py @fabaff
homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/sensor/worldclock.py @fabaff homeassistant/components/sensor/worldclock.py @fabaff
homeassistant/components/shiftr.py @fabaff homeassistant/components/shiftr.py @fabaff
homeassistant/components/spaceapi.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/switch/tplink.py @rytilahti
homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/vacuum/roomba.py @pschmitt
homeassistant/components/weather/__init__.py @fabaff homeassistant/components/weather/__init__.py @fabaff
@ -157,9 +165,12 @@ homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen homeassistant/components/*/broadlink.py @danielhiversen
# C # C
homeassistant/components/cloudflare.py @ludeeus
homeassistant/components/counter/* @fabaff homeassistant/components/counter/* @fabaff
# D # D
homeassistant/components/daikin.py @fredrike @rofrantz
homeassistant/components/*/daikin.py @fredrike @rofrantz
homeassistant/components/*/deconz.py @kane610 homeassistant/components/*/deconz.py @kane610
homeassistant/components/digital_ocean.py @fabaff homeassistant/components/digital_ocean.py @fabaff
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/* @bachya
homeassistant/components/*/openuv.py @bachya homeassistant/components/*/openuv.py @bachya
# P
homeassistant/components/point/* @fredrike
homeassistant/components/*/point.py @fredrike
# Q # Q
homeassistant/components/qwikswitch.py @kellerza homeassistant/components/qwikswitch.py @kellerza
homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza
@ -221,8 +236,8 @@ homeassistant/components/*/simplisafe.py @bachya
# T # T
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei
homeassistant/components/*/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei
homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/tellduslive/*.py @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @fredrike
homeassistant/components/tesla.py @zabuldon homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/thethingsnetwork.py @fabaff homeassistant/components/thethingsnetwork.py @fabaff

View File

@ -78,11 +78,6 @@ class AuthManager:
hass, self._async_create_login_flow, hass, self._async_create_login_flow,
self._async_finish_login_flow) self._async_finish_login_flow)
@property
def active(self) -> bool:
"""Return if any auth providers are registered."""
return bool(self._providers)
@property @property
def support_legacy(self) -> bool: def support_legacy(self) -> bool:
""" """

View File

@ -1,4 +1,5 @@
"""Storage for auth models.""" """Storage for auth models."""
import asyncio
from collections import OrderedDict from collections import OrderedDict
from datetime import timedelta from datetime import timedelta
import hmac import hmac
@ -11,7 +12,7 @@ from homeassistant.util import dt as dt_util
from . import models from . import models
from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY 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 from .permissions.types import PolicyType # noqa: F401
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -34,6 +35,7 @@ class AuthStore:
self.hass = hass self.hass = hass
self._users = None # type: Optional[Dict[str, models.User]] self._users = None # type: Optional[Dict[str, models.User]]
self._groups = None # type: Optional[Dict[str, models.Group]] 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, self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True) private=True)
@ -94,6 +96,7 @@ class AuthStore:
# Until we get group management, we just put everyone in the # Until we get group management, we just put everyone in the
# same group. # same group.
'groups': groups, 'groups': groups,
'perm_lookup': self._perm_lookup,
} # type: Dict[str, Any] } # type: Dict[str, Any]
if is_owner is not None: if is_owner is not None:
@ -269,13 +272,18 @@ class AuthStore:
async def _async_load(self) -> None: async def _async_load(self) -> None:
"""Load the users.""" """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 # Make sure that we're not overriding data if 2 loads happened at the
# same time # same time
if self._users is not None: if self._users is not None:
return return
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg)
if data is None: if data is None:
self._set_defaults() self._set_defaults()
return return
@ -374,6 +382,7 @@ class AuthStore:
is_owner=user_dict['is_owner'], is_owner=user_dict['is_owner'],
is_active=user_dict['is_active'], is_active=user_dict['is_active'],
system_generated=user_dict['system_generated'], system_generated=user_dict['system_generated'],
perm_lookup=perm_lookup,
) )
for cred_dict in data['credentials']: for cred_dict in data['credentials']:

View File

@ -4,13 +4,14 @@ Sending HOTP through notify service
""" """
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from typing import Any, Dict, Optional, Tuple, List # noqa: F401 from typing import Any, Dict, Optional, List
import attr import attr
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
@ -314,8 +315,11 @@ class NotifySetupFlow(SetupFlow):
_generate_otp, self._secret, self._count) _generate_otp, self._secret, self._count)
assert self._notify_service assert self._notify_service
await self._auth_module.async_notify( try:
code, self._notify_service, self._target) 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( return self.async_show_form(
step_id='setup', step_id='setup',

View File

@ -31,6 +31,9 @@ class User:
"""A user.""" """A user."""
name = attr.ib(type=str) # type: Optional[str] 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) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False) is_owner = attr.ib(type=bool, default=False)
is_active = 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( self._permissions = perm_mdl.PolicyPermissions(
perm_mdl.merge_policies([ perm_mdl.merge_policies([
group.policy for group in self.groups])) group.policy for group in self.groups]),
self.perm_lookup)
return self._permissions return self._permissions

View File

@ -1,15 +1,18 @@
"""Permissions for Home Assistant.""" """Permissions for Home Assistant."""
import logging import logging
from typing import ( # noqa: F401 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 import voluptuous as vol
from .const import CAT_ENTITIES from .const import CAT_ENTITIES
from .models import PermissionLookup
from .types import PolicyType from .types import PolicyType
from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .entities import ENTITY_POLICY_SCHEMA, compile_entities
from .merge import merge_policies # noqa from .merge import merge_policies # noqa
POLICY_SCHEMA = vol.Schema({ POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
}) })
@ -39,13 +42,16 @@ class AbstractPermissions:
class PolicyPermissions(AbstractPermissions): class PolicyPermissions(AbstractPermissions):
"""Handle permissions.""" """Handle permissions."""
def __init__(self, policy: PolicyType) -> None: def __init__(self, policy: PolicyType,
perm_lookup: PermissionLookup) -> None:
"""Initialize the permission class.""" """Initialize the permission class."""
self._policy = policy self._policy = policy
self._perm_lookup = perm_lookup
def _entity_func(self) -> Callable[[str, str], bool]: def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access.""" """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: def __eq__(self, other: Any) -> bool:
"""Equals check.""" """Equals check."""

View File

@ -1,11 +1,11 @@
"""Entity permissions.""" """Entity permissions."""
from functools import wraps from functools import wraps
from typing import ( # noqa: F401 from typing import Callable, List, Union # noqa: F401
Callable, Dict, List, Tuple, Union)
import voluptuous as vol import voluptuous as vol
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
from .models import PermissionLookup
from .types import CategoryType, ValueType from .types import CategoryType, ValueType
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
@ -15,6 +15,7 @@ SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
})) }))
ENTITY_DOMAINS = 'domains' ENTITY_DOMAINS = 'domains'
ENTITY_DEVICE_IDS = 'device_ids'
ENTITY_ENTITY_IDS = 'entity_ids' ENTITY_ENTITY_IDS = 'entity_ids'
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({ 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({ ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_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_DOMAINS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): 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) return schema.get(key)
def compile_entities(policy: CategoryType) \ def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \
-> Callable[[str, str], bool]: -> Callable[[str, str], bool]:
"""Compile policy into a function that tests policy.""" """Compile policy into a function that tests policy."""
# None, Empty Dict, False # None, Empty Dict, False
@ -58,6 +60,7 @@ def compile_entities(policy: CategoryType) \
assert isinstance(policy, dict) assert isinstance(policy, dict)
domains = policy.get(ENTITY_DOMAINS) domains = policy.get(ENTITY_DOMAINS)
device_ids = policy.get(ENTITY_DEVICE_IDS)
entity_ids = policy.get(ENTITY_ENTITY_IDS) entity_ids = policy.get(ENTITY_ENTITY_IDS)
all_entities = policy.get(SUBCAT_ALL) all_entities = policy.get(SUBCAT_ALL)
@ -85,6 +88,29 @@ def compile_entities(policy: CategoryType) \
funcs.append(allowed_entity_id_dict) 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): if isinstance(domains, bool):
def allowed_domain_bool(entity_id: str, key: str) \ def allowed_domain_bool(entity_id: str, key: str) \
-> Union[None, bool]: -> Union[None, bool]:

View File

@ -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')

View File

@ -1,6 +1,5 @@
"""Common code for permissions.""" """Common code for permissions."""
from typing import ( # noqa: F401 from typing import Mapping, Union
Mapping, Union, Any)
# MyPy doesn't support recursion yet. So writing it out as far as we need. # MyPy doesn't support recursion yet. So writing it out as far as we need.

View File

@ -226,7 +226,11 @@ class LoginFlow(data_entry_flow.FlowHandler):
if user_input is None and hasattr(auth_module, if user_input is None and hasattr(auth_module,
'async_initialize_login_mfa_step'): '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: if user_input is not None:
expires = self.created_at + MFA_SESSION_EXPIRATION expires = self.created_at + MFA_SESSION_EXPIRATION

View File

@ -1,8 +1,6 @@
"""Home Assistant auth provider.""" """Home Assistant auth provider."""
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
import hashlib
import hmac
from typing import Any, Dict, List, Optional, cast from typing import Any, Dict, List, Optional, cast
import bcrypt import bcrypt
@ -11,12 +9,10 @@ import voluptuous as vol
from homeassistant.const import CONF_ID from homeassistant.const import CONF_ID
from homeassistant.core import callback, HomeAssistant from homeassistant.core import callback, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.async_ import run_coroutine_threadsafe
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from ..models import Credentials, UserMeta from ..models import Credentials, UserMeta
from ..util import generate_secret
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -62,7 +58,6 @@ class Data:
if data is None: if data is None:
data = { data = {
'salt': generate_secret(),
'users': [] 'users': []
} }
@ -94,39 +89,11 @@ class Data:
user_hash = base64.b64decode(found['password']) 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 # bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(), if not bcrypt.checkpw(password.encode(),
user_hash): user_hash):
raise InvalidAuth 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 # pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes: def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password.""" """Encode a password."""

View File

@ -115,11 +115,6 @@ async def async_from_config_dict(config: Dict[str, Any],
conf_util.merge_packages_config( conf_util.merge_packages_config(
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})) 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) hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_load() await hass.config_entries.async_load()

View File

@ -25,21 +25,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
data = hass.data[BLINK_DATA] 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 = []
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) add_entities(sync_modules, True)
class BlinkSyncModule(AlarmControlPanel): class BlinkSyncModule(AlarmControlPanel):
"""Representation of a Blink Alarm Control Panel.""" """Representation of a Blink Alarm Control Panel."""
def __init__(self, data, name): def __init__(self, data, name, sync):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self.data = data self.data = data
self.sync = data.sync self.sync = sync
self._name = name self._name = name
self._state = None self._state = None
@ -68,6 +66,7 @@ class BlinkSyncModule(AlarmControlPanel):
"""Return the state attributes.""" """Return the state attributes."""
attr = self.sync.attributes attr = self.sync.attributes
attr['network_info'] = self.data.networks attr['network_info'] = self.data.networks
attr['associated_cameras'] = list(self.sync.cameras.keys())
attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION attr[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION
return attr return attr

View File

@ -12,7 +12,8 @@ from homeassistant.components.lupusec import DOMAIN as LUPUSEC_DOMAIN
from homeassistant.components.lupusec import LupusecDevice from homeassistant.components.lupusec import LupusecDevice
from homeassistant.const import (STATE_ALARM_ARMED_AWAY, from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED) STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED)
DEPENDENCIES = ['lupusec'] DEPENDENCIES = ['lupusec']
@ -50,6 +51,8 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
elif self._device.is_home: elif self._device.is_home:
state = STATE_ALARM_ARMED_HOME state = STATE_ALARM_ARMED_HOME
elif self._device.is_alarm_triggered:
state = STATE_ALARM_TRIGGERED
else: else:
state = None state = None
return state return state

View File

@ -21,7 +21,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
import homeassistant.util.dt as dt_util 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__) _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. Representation of an alarm status.
@ -310,7 +310,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when entity about to be added to hass.""" """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: if state:
self._state = state.state self._state = state.state
self._state_ts = state.last_updated self._state_ts = state.last_updated

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, 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 from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None): async_add_entities, discovery_info=None):
"""Set up MQTT alarm control panel through configuration.yaml.""" """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): 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): async def async_discover(discovery_payload):
"""Discover and add an MQTT alarm control panel.""" """Discover and add an MQTT alarm control panel."""
config = PLATFORM_SCHEMA(discovery_payload) 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]) discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect( async_dispatcher_connect(
@ -67,54 +67,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover) async_discover)
async def _async_setup_entity(hass, config, async_add_entities, async def _async_setup_entity(config, async_add_entities,
discovery_hash=None): discovery_hash=None):
"""Set up the MQTT Alarm Control Panel platform.""" """Set up the MQTT Alarm Control Panel platform."""
async_add_entities([MqttAlarm( async_add_entities([MqttAlarm(config, discovery_hash)])
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,)])
class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate, class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
alarm.AlarmControlPanel): alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""
def __init__(self, name, state_topic, command_topic, qos, retain, def __init__(self, config, discovery_hash):
payload_disarm, payload_arm_home, payload_arm_away, code,
availability_topic, payload_available, payload_not_available,
discovery_hash):
"""Init the MQTT Alarm Control Panel.""" """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, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash) MqttDiscoveryUpdate.__init__(self, discovery_hash,
self._state = STATE_UNKNOWN self.discovery_update)
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
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
await MqttAvailability.async_added_to_hass(self) await super().async_added_to_hass()
await MqttDiscoveryUpdate.async_added_to_hass(self) 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 @callback
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
"""Run when new MQTT message has been received.""" """Run when new MQTT message has been received."""
@ -126,8 +119,16 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
self._state = payload self._state = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
await mqtt.async_subscribe( self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._state_topic, message_received, self._qos) 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 @property
def should_poll(self): def should_poll(self):
@ -137,7 +138,7 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return self._name return self._config.get(CONF_NAME)
@property @property
def state(self): def state(self):
@ -147,9 +148,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: code = self._config.get(CONF_CODE)
if code is None:
return 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 'Number'
return 'Any' return 'Any'
@ -161,8 +163,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'disarming'): if not self._validate_code(code, 'disarming'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_disarm, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) 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): async def async_alarm_arm_home(self, code=None):
"""Send arm home command. """Send arm home command.
@ -172,8 +176,10 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'arming home'): if not self._validate_code(code, 'arming home'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_home, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) 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): async def async_alarm_arm_away(self, code=None):
"""Send arm away command. """Send arm away command.
@ -183,12 +189,15 @@ class MqttAlarm(MqttAvailability, MqttDiscoveryUpdate,
if not self._validate_code(code, 'arming away'): if not self._validate_code(code, 'arming away'):
return return
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_arm_away, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) self._config.get(CONF_PAYLOAD_ARM_AWAY),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
def _validate_code(self, code, state): def _validate_code(self, code, state):
"""Validate given code.""" """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: if not check:
_LOGGER.warning('Wrong code entered for %s', state) _LOGGER.warning('Wrong code entered for %s', state)
return check return check

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['yalesmartalarmclient==0.1.4'] REQUIREMENTS = ['yalesmartalarmclient==0.1.5']
CONF_AREA_ID = 'area_id' CONF_AREA_ID = 'area_id'

View File

@ -504,6 +504,20 @@ class _AlexaColorTemperatureController(_AlexaInterface):
def name(self): def name(self):
return 'Alexa.ColorTemperatureController' 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): class _AlexaPercentageController(_AlexaInterface):
"""Implements Alexa.PercentageController. """Implements Alexa.PercentageController.

View File

@ -9,7 +9,9 @@ import json
import logging import logging
from aiohttp import web from aiohttp import web
from aiohttp.web_exceptions import HTTPBadRequest
import async_timeout import async_timeout
import voluptuous as vol
from homeassistant.bootstrap import DATA_LOGGING from homeassistant.bootstrap import DATA_LOGGING
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -21,7 +23,8 @@ from homeassistant.const import (
URL_API_TEMPLATE, __version__) URL_API_TEMPLATE, __version__)
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.auth.permissions.const import POLICY_READ 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 import template
from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers.state import AsyncTrackStates
@ -339,8 +342,11 @@ class APIDomainServicesView(HomeAssistantView):
"Data should be valid JSON.", HTTP_BAD_REQUEST) "Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call( try:
domain, service, data, True, self.context(request)) await hass.services.async_call(
domain, service, data, True, self.context(request))
except (vol.Invalid, ServiceNotFound):
raise HTTPBadRequest()
return self.json(changed_states) return self.json(changed_states)

View File

@ -16,7 +16,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyatv==0.3.10'] REQUIREMENTS = ['pyatv==0.3.12']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,7 +11,6 @@ import voluptuous as vol
from requests import RequestException from requests import RequestException
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
from homeassistant.const import ( from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP) CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
@ -141,11 +140,11 @@ def setup(hass, config):
from requests import Session from requests import Session
conf = config[DOMAIN] conf = config[DOMAIN]
api_http_session = None
try: try:
api_http_session = Session() api_http_session = Session()
except RequestException as ex: except RequestException as ex:
_LOGGER.warning("Creating HTTP session failed with: %s", str(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) 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), install_id=conf.get(CONF_INSTALL_ID),
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE)) 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) return setup_august(hass, config, api, authenticator)
@ -178,22 +191,6 @@ class AugustData:
self._door_state_by_id = {} self._door_state_by_id = {}
self._activities_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 @property
def house_ids(self): def house_ids(self):
"""Return a list of house_ids.""" """Return a list of house_ids."""

View File

@ -13,7 +13,7 @@
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions" "title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
}, },
"setup": { "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" "title": "Verifiqueu la configuraci\u00f3"
} }
}, },

View File

@ -13,6 +13,7 @@
"title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify" "title": "Nastavte jednor\u00e1zov\u00e9 heslo dodan\u00e9 komponentou notify"
}, },
"setup": { "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" "title": "Ov\u011b\u0159en\u00ed nastaven\u00ed"
} }
} }
@ -20,7 +21,14 @@
"totp": { "totp": {
"error": { "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." "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"
} }
} }
} }

View File

@ -2,18 +2,18 @@
"mfa_setup": { "mfa_setup": {
"notify": { "notify": {
"abort": { "abort": {
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja." "no_available_service": "Storitve obve\u0161\u010danja niso na voljo."
}, },
"error": { "error": {
"invalid_code": "Neveljavna koda, poskusite znova." "invalid_code": "Neveljavna koda, poskusite znova."
}, },
"step": { "step": {
"init": { "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" "title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
}, },
"setup": { "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" "title": "Preverite nastavitev"
} }
}, },

View File

@ -16,12 +16,13 @@ from homeassistant.core import CoreState
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, 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.exceptions import HomeAssistantError
from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers import extract_domain_configs, script, condition
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent 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 from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -182,7 +183,7 @@ async def async_setup(hass, config):
return True return True
class AutomationEntity(ToggleEntity): class AutomationEntity(ToggleEntity, RestoreEntity):
"""Entity to show status of entity.""" """Entity to show status of entity."""
def __init__(self, automation_id, name, async_attach_triggers, cond_func, 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: async def async_added_to_hass(self) -> None:
"""Startup with initial state or previous state.""" """Startup with initial state or previous state."""
await super().async_added_to_hass()
if self._initial_state is not None: if self._initial_state is not None:
enable_automation = self._initial_state enable_automation = self._initial_state
_LOGGER.debug("Automation %s initial state %s from config " _LOGGER.debug("Automation %s initial state %s from config "
"initial_state", self.entity_id, enable_automation) "initial_state", self.entity_id, enable_automation)
else: else:
state = await async_get_last_state(self.hass, self.entity_id) state = await self.async_get_last_state()
if state: if state:
enable_automation = state.state == STATE_ON enable_automation = state.state == STATE_ON
self._last_triggered = state.attributes.get('last_triggered') self._last_triggered = state.attributes.get('last_triggered')
@ -285,12 +287,17 @@ class AutomationEntity(ToggleEntity):
""" """
if skip_condition or self._cond_func(variables): if skip_condition or self._cond_func(variables):
self.async_set_context(context) 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) await self._async_action(self.entity_id, variables, context)
self._last_triggered = utcnow() self._last_triggered = utcnow()
await self.async_update_ha_state() await self.async_update_ha_state()
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Remove listeners when removing automation from HASS.""" """Remove listeners when removing automation from HASS."""
await super().async_will_remove_from_hass()
await self.async_turn_off() await self.async_turn_off()
async def async_enable(self): async def async_enable(self):
@ -368,8 +375,6 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context): async def action(entity_id, variables, context):
"""Execute an action.""" """Execute an action."""
_LOGGER.info('Executing %s', name) _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) await script_obj.async_run(variables, context)
return action return action

View File

@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[BLINK_DATA] data = hass.data[BLINK_DATA]
devs = [] devs = []
for camera in data.sync.cameras: for camera in data.cameras:
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
devs.append(BlinkBinarySensor(data, camera, sensor_type)) devs.append(BlinkBinarySensor(data, camera, sensor_type))
add_entities(devs, True) add_entities(devs, True)
@ -34,7 +34,7 @@ class BlinkBinarySensor(BinarySensorDevice):
name, icon = BINARY_SENSORS[sensor_type] name, icon = BINARY_SENSORS[sensor_type]
self._name = "{} {} {}".format(BLINK_DATA, camera, name) self._name = "{} {} {}".format(BLINK_DATA, camera, name)
self._icon = icon self._icon = icon
self._camera = data.sync.cameras[camera] self._camera = data.cameras[camera]
self._state = None self._state = None
self._unique_id = "{}-{}".format(self._camera.serial, self._type) self._unique_id = "{}-{}".format(self._camera.serial, self._type)

View File

@ -16,6 +16,8 @@ DEPENDENCIES = ['fibaro']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { 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.doorSensor': ['Door', 'mdi:window-open', 'door'],
'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'], 'com.fibaro.windowSensor': ['Window', 'mdi:window-open', 'window'],
'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'], 'com.fibaro.smokeSensor': ['Smoke', 'mdi:smoking', 'smoke'],

View File

@ -3,59 +3,39 @@
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ihc/ https://home-assistant.io/components/binary_sensor.ihc/
""" """
import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) BinarySensorDevice)
from homeassistant.components.ihc import ( from homeassistant.components.ihc import (
validate_name, IHC_DATA, IHC_CONTROLLER, IHC_INFO) IHC_DATA, IHC_CONTROLLER, IHC_INFO)
from homeassistant.components.ihc.const import CONF_INVERTING from homeassistant.components.ihc.const import (
CONF_INVERTING)
from homeassistant.components.ihc.ihcdevice import IHCDevice from homeassistant.components.ihc.ihcdevice import IHCDevice
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_TYPE, CONF_ID, CONF_BINARY_SENSORS) CONF_TYPE)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['ihc'] 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the IHC binary sensor platform.""" """Set up the IHC binary sensor platform."""
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER] if discovery_info is None:
info = hass.data[IHC_DATA][IHC_INFO] return
devices = [] devices = []
if discovery_info: for name, device in discovery_info.items():
for name, device in discovery_info.items(): ihc_id = device['ihc_id']
ihc_id = device['ihc_id'] product_cfg = device['product_cfg']
product_cfg = device['product_cfg'] product = device['product']
product = device['product'] # Find controller that corresponds with device id
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info, ctrl_id = device['ctrl_id']
product_cfg.get(CONF_TYPE), ihc_key = IHC_DATA.format(ctrl_id)
product_cfg[CONF_INVERTING], info = hass.data[ihc_key][IHC_INFO]
product) ihc_controller = hass.data[ihc_key][IHC_CONTROLLER]
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)
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
product_cfg.get(CONF_TYPE),
product_cfg[CONF_INVERTING],
product)
devices.append(sensor)
add_entities(devices) add_entities(devices)

View File

@ -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_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
vol.Optional(CONF_OFF_DELAY): vol.Optional(CONF_OFF_DELAY):
vol.All(vol.Coerce(int), vol.Range(min=0)), vol.All(vol.Coerce(int), vol.Range(min=0)),
# Integrations shouldn't never expose unique_id through configuration # Integrations should never expose unique_id through configuration.
# this here is an exception because MQTT is a msg transport, not a protocol # This is an exception because MQTT is a message transport, not a protocol
vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.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 def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None): async_add_entities, discovery_info=None):
"""Set up MQTT binary sensor through configuration.yaml.""" """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): 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): async def async_discover(discovery_payload):
"""Discover and add a MQTT binary sensor.""" """Discover and add a MQTT binary sensor."""
config = PLATFORM_SCHEMA(discovery_payload) 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]) discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect( async_dispatcher_connect(
@ -71,17 +71,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover) async_discover)
async def _async_setup_entity(hass, config, async_add_entities, async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
discovery_hash=None):
"""Set up the MQTT binary sensor.""" """Set up the MQTT binary sensor."""
value_template = config.get(CONF_VALUE_TEMPLATE) async_add_entities([MqttBinarySensor(config, discovery_hash)])
if value_template is not None:
value_template.hass = hass
async_add_entities([MqttBinarySensor(
config,
discovery_hash
)])
class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate, class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@ -91,30 +83,18 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
def __init__(self, config, discovery_hash): def __init__(self, config, discovery_hash):
"""Initialize the MQTT binary sensor.""" """Initialize the MQTT binary sensor."""
self._config = config self._config = config
self._unique_id = config.get(CONF_UNIQUE_ID)
self._state = None self._state = None
self._sub_state = None self._sub_state = None
self._delay_listener = 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) availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
payload_available = config.get(CONF_PAYLOAD_AVAILABLE) payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE) payload_not_available = config.get(CONF_PAYLOAD_NOT_AVAILABLE)
qos = config.get(CONF_QOS)
device_config = config.get(CONF_DEVICE) device_config = config.get(CONF_DEVICE)
MqttAvailability.__init__(self, availability_topic, self._qos, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash, MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.discovery_update) self.discovery_update)
@ -122,37 +102,23 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe mqtt events.""" """Subscribe mqtt events."""
await MqttAvailability.async_added_to_hass(self) await super().async_added_to_hass()
await MqttDiscoveryUpdate.async_added_to_hass(self)
await self._subscribe_topics() await self._subscribe_topics()
async def discovery_update(self, discovery_payload): async def discovery_update(self, discovery_payload):
"""Handle updated discovery message.""" """Handle updated discovery message."""
config = PLATFORM_SCHEMA(discovery_payload) config = PLATFORM_SCHEMA(discovery_payload)
self._setup_from_config(config) self._config = config
await self.availability_discovery_update(config) await self.availability_discovery_update(config)
await self._subscribe_topics() await self._subscribe_topics()
self.async_schedule_update_ha_state() 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): async def _subscribe_topics(self):
"""(Re)Subscribe to topics.""" """(Re)Subscribe to topics."""
value_template = self._config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template.hass = self.hass
@callback @callback
def off_delay_listener(now): def off_delay_listener(now):
"""Switch device off after a delay.""" """Switch device off after a delay."""
@ -163,34 +129,37 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@callback @callback
def state_message_received(_topic, payload, _qos): def state_message_received(_topic, payload, _qos):
"""Handle a new received MQTT state message.""" """Handle a new received MQTT state message."""
if self._template is not None: value_template = self._config.get(CONF_VALUE_TEMPLATE)
payload = self._template.async_render_with_possible_json_value( if value_template is not None:
payload = value_template.async_render_with_possible_json_value(
payload) payload)
if payload == self._payload_on: if payload == self._config.get(CONF_PAYLOAD_ON):
self._state = True self._state = True
elif payload == self._payload_off: elif payload == self._config.get(CONF_PAYLOAD_OFF):
self._state = False self._state = False
else: # Payload is not for this entity else: # Payload is not for this entity
_LOGGER.warning('No matching payload found' _LOGGER.warning('No matching payload found'
' for entity: %s with state_topic: %s', ' for entity: %s with state_topic: %s',
self._name, self._state_topic) self._config.get(CONF_NAME),
self._config.get(CONF_STATE_TOPIC))
return return
if self._delay_listener is not None: if self._delay_listener is not None:
self._delay_listener() self._delay_listener()
self._delay_listener = None 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._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.async_schedule_update_ha_state()
self._sub_state = await subscription.async_subscribe_topics( self._sub_state = await subscription.async_subscribe_topics(
self.hass, self._sub_state, 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, 'msg_callback': state_message_received,
'qos': self._qos}}) 'qos': self._config.get(CONF_QOS)}})
async def async_will_remove_from_hass(self): async def async_will_remove_from_hass(self):
"""Unsubscribe when removed.""" """Unsubscribe when removed."""
@ -205,7 +174,7 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@property @property
def name(self): def name(self):
"""Return the name of the binary sensor.""" """Return the name of the binary sensor."""
return self._name return self._config.get(CONF_NAME)
@property @property
def is_on(self): def is_on(self):
@ -215,12 +184,12 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
@property @property
def device_class(self): def device_class(self):
"""Return the class of this sensor.""" """Return the class of this sensor."""
return self._device_class return self._config.get(CONF_DEVICE_CLASS)
@property @property
def force_update(self): def force_update(self):
"""Force update.""" """Force update."""
return self._force_update return self._config.get(CONF_FORCE_UPDATE)
@property @property
def unique_id(self): def unique_id(self):

View File

@ -7,10 +7,11 @@ https://home-assistant.io/components/binary_sensor.point/
import logging 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 import MinutPointEntity
from homeassistant.components.point.const import ( 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.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -40,10 +41,16 @@ EVENTS = {
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up a Point's binary sensors based on a config entry.""" """Set up a Point's binary sensors based on a config entry."""
device_id = config_entry.data[NEW_DEVICE] async def async_discover_sensor(device_id):
client = hass.data[POINT_DOMAIN][config_entry.entry_id] """Discover and add a discovered sensor."""
async_add_entities((MinutPointBinarySensor(client, device_id, device_class) client = hass.data[POINT_DOMAIN][config_entry.entry_id]
for device_class in EVENTS), True) 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): class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice):

View File

@ -14,46 +14,48 @@ DEPENDENCIES = ['sense']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BIN_SENSOR_CLASS = 'power' BIN_SENSOR_CLASS = 'power'
MDI_ICONS = {'ac': 'air-conditioner', MDI_ICONS = {
'aquarium': 'fish', 'ac': 'air-conditioner',
'car': 'car-electric', 'aquarium': 'fish',
'computer': 'desktop-classic', 'car': 'car-electric',
'cup': 'coffee', 'computer': 'desktop-classic',
'dehumidifier': 'water-off', 'cup': 'coffee',
'dishes': 'dishwasher', 'dehumidifier': 'water-off',
'drill': 'toolbox', 'dishes': 'dishwasher',
'fan': 'fan', 'drill': 'toolbox',
'freezer': 'fridge-top', 'fan': 'fan',
'fridge': 'fridge-bottom', 'freezer': 'fridge-top',
'game': 'gamepad-variant', 'fridge': 'fridge-bottom',
'garage': 'garage', 'game': 'gamepad-variant',
'grill': 'stove', 'garage': 'garage',
'heat': 'fire', 'grill': 'stove',
'heater': 'radiatior', 'heat': 'fire',
'humidifier': 'water', 'heater': 'radiatior',
'kettle': 'kettle', 'humidifier': 'water',
'leafblower': 'leaf', 'kettle': 'kettle',
'lightbulb': 'lightbulb', 'leafblower': 'leaf',
'media_console': 'set-top-box', 'lightbulb': 'lightbulb',
'modem': 'router-wireless', 'media_console': 'set-top-box',
'outlet': 'power-socket-us', 'modem': 'router-wireless',
'papershredder': 'shredder', 'outlet': 'power-socket-us',
'printer': 'printer', 'papershredder': 'shredder',
'pump': 'water-pump', 'printer': 'printer',
'settings': 'settings', 'pump': 'water-pump',
'skillet': 'pot', 'settings': 'settings',
'smartcamera': 'webcam', 'skillet': 'pot',
'socket': 'power-plug', 'smartcamera': 'webcam',
'sound': 'speaker', 'socket': 'power-plug',
'stove': 'stove', 'sound': 'speaker',
'trash': 'trash-can', 'stove': 'stove',
'tv': 'television', 'trash': 'trash-can',
'vacuum': 'robot-vacuum', 'tv': 'television',
'washer': 'washing-machine'} 'vacuum': 'robot-vacuum',
'washer': 'washing-machine',
}
def setup_platform(hass, config, add_entities, discovery_info=None): 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: if discovery_info is None:
return return
@ -67,14 +69,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
def sense_to_mdi(sense_icon): def sense_to_mdi(sense_icon):
"""Convert sense icon to mdi 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): class SenseDevice(BinarySensorDevice):
"""Implementation of a Sense energy device binary sensor.""" """Implementation of a Sense energy device binary sensor."""
def __init__(self, data, device): def __init__(self, data, device):
"""Initialize the sensor.""" """Initialize the Sense binary sensor."""
self._name = device['name'] self._name = device['name']
self._id = device['id'] self._id = device['id']
self._icon = sense_to_mdi(device['icon']) self._icon = sense_to_mdi(device['icon'])

View File

@ -41,6 +41,7 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
self._state = None self._state = None
self._icon = None self._icon = None
self._battery = None self._battery = None
self._available = False
@property @property
def is_on(self): def is_on(self):
@ -71,6 +72,11 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
attr[ATTR_BATTERY_LEVEL] = self._battery attr[ATTR_BATTERY_LEVEL] = self._battery
return attr return attr
@property
def available(self):
"""Return True if entity is available."""
return self._available
def update(self): def update(self):
"""Update the state.""" """Update the state."""
self.controller.get_states([self.tahoma_device]) self.controller.get_states([self.tahoma_device])
@ -82,11 +88,13 @@ class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
self._state = STATE_ON self._state = STATE_ON
if 'core:SensorDefectState' in self.tahoma_device.active_states: 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[ self._battery = self.tahoma_device.active_states[
'core:SensorDefectState'] 'core:SensorDefectState']
self._available = bool(self._battery != 'dead')
else: else:
self._battery = None self._battery = None
self._available = True
if self._state == STATE_ON: if self._state == STATE_ON:
self._icon = "mdi:fire" self._icon = "mdi:fire"

View File

@ -9,8 +9,9 @@ https://home-assistant.io/components/binary_sensor.tellduslive/
""" """
import logging import logging
from homeassistant.components.tellduslive import TelldusLiveEntity from homeassistant.components import tellduslive
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.tellduslive.entry import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,8 +20,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Tellstick sensors.""" """Set up Tellstick sensors."""
if discovery_info is None: if discovery_info is None:
return return
client = hass.data[tellduslive.DOMAIN]
add_entities( add_entities(
TelldusLiveSensor(hass, binary_sensor) TelldusLiveSensor(client, binary_sensor)
for binary_sensor in discovery_info for binary_sensor in discovery_info
) )

View File

@ -6,17 +6,19 @@ https://home-assistant.io/components/binary_sensor.volvooncall/
""" """
import logging import logging
from homeassistant.components.volvooncall import VolvoEntity from homeassistant.components.volvooncall import VolvoEntity, DATA_KEY
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES)
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Volvo sensors."""
if discovery_info is None: if discovery_info is None:
return return
add_entities([VolvoSensor(hass, *discovery_info)]) async_add_entities([VolvoSensor(hass.data[DATA_KEY], *discovery_info)])
class VolvoSensor(VolvoEntity, BinarySensorDevice): class VolvoSensor(VolvoEntity, BinarySensorDevice):
@ -25,14 +27,11 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return True if the binary sensor is on.""" """Return True if the binary sensor is on."""
val = getattr(self.vehicle, self._attribute) return self.instrument.is_on
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'
@property @property
def device_class(self): def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """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

View File

@ -7,7 +7,11 @@ at https://home-assistant.io/components/binary_sensor.zha/
import logging import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice 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__) _LOGGER = logging.getLogger(__name__)
@ -26,23 +30,43 @@ CLASS_MAPPING = {
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors.""" """Old way of setting up Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info) pass
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)
async def _async_setup_iaszone(hass, config, async_add_entities, async def async_setup_entry(hass, config_entry, async_add_entities):
discovery_info): """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 device_class = None
from zigpy.zcl.clusters.security import IasZone from zigpy.zcl.clusters.security import IasZone
cluster = discovery_info['in_clusters'][IasZone.cluster_id] 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 # If we fail to read from the device, use a non-specific class
pass pass
sensor = BinarySensor(device_class, **discovery_info) return BinarySensor(device_class, **discovery_info)
async_add_entities([sensor], update_before_add=True)
async def _async_setup_remote(hass, config, async_add_entities, async def _async_setup_remote(discovery_info):
discovery_info):
remote = Remote(**discovery_info) remote = Remote(**discovery_info)
if discovery_info['new_join']: 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'] out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters: if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id] 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, remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1 reportable_change=1
) )
if LevelControl.cluster_id in out_clusters: if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id] 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, remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1 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.""" """The ZHA Binary Sensor."""
_domain = DOMAIN _domain = DOMAIN
@ -130,16 +151,16 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
"""Retrieve latest state.""" """Retrieve latest state."""
from zigpy.types.basic import uint16_t from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone, result = await helpers.safe_read(self._endpoint.ias_zone,
['zone_status'], ['zone_status'],
allow_cache=False, allow_cache=False,
only_cache=(not self._initialized)) only_cache=(not self._initialized))
state = result.get('zone_status', self._state) state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)): if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3 self._state = result.get('zone_status', self._state) & 3
class Remote(zha.Entity, BinarySensorDevice): class Remote(ZhaEntity, BinarySensorDevice):
"""ZHA switch/remote controller/button.""" """ZHA switch/remote controller/button."""
_domain = DOMAIN _domain = DOMAIN
@ -252,7 +273,7 @@ class Remote(zha.Entity, BinarySensorDevice):
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read( result = await helpers.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id], self._endpoint.out_clusters[OnOff.cluster_id],
['on_off'], ['on_off'],
allow_cache=False, allow_cache=False,

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.10.3'] REQUIREMENTS = ['blinkpy==0.11.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -111,7 +111,7 @@ def setup(hass, config):
def trigger_camera(call): def trigger_camera(call):
"""Trigger a camera.""" """Trigger a camera."""
cameras = hass.data[BLINK_DATA].sync.cameras cameras = hass.data[BLINK_DATA].cameras
name = call.data[CONF_NAME] name = call.data[CONF_NAME]
if name in cameras: if name in cameras:
cameras[name].snap_picture() cameras[name].snap_picture()
@ -148,7 +148,7 @@ async def async_handle_save_video_service(hass, call):
def _write_video(camera_name, video_path): def _write_video(camera_name, video_path):
"""Call video write.""" """Call video write."""
all_cameras = hass.data[BLINK_DATA].sync.cameras all_cameras = hass.data[BLINK_DATA].cameras
if camera_name in all_cameras: if camera_name in all_cameras:
all_cameras[camera_name].video_to_file(video_path) all_cameras[camera_name].video_to_file(video_path)

View File

@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
data = hass.data[BLINK_DATA] data = hass.data[BLINK_DATA]
devs = [] devs = []
for name, camera in data.sync.cameras.items(): for name, camera in data.cameras.items():
devs.append(BlinkCamera(data, name, camera)) devs.append(BlinkCamera(data, name, camera))
add_entities(devs) add_entities(devs)

View File

@ -60,13 +60,20 @@ async def async_setup_platform(hass, config, async_add_entities,
def extract_image_from_mjpeg(stream): def extract_image_from_mjpeg(stream):
"""Take in a MJPEG stream object, return the jpg from it.""" """Take in a MJPEG stream object, return the jpg from it."""
data = b'' data = b''
for chunk in stream: for chunk in stream:
data += chunk data += chunk
jpg_start = data.find(b'\xff\xd8')
jpg_end = data.find(b'\xff\xd9') jpg_end = data.find(b'\xff\xd9')
if jpg_start != -1 and jpg_end != -1:
jpg = data[jpg_start:jpg_end + 2] if jpg_end == -1:
return jpg continue
jpg_start = data.find(b'\xff\xd8')
if jpg_start == -1:
continue
return data[jpg_start:jpg_end + 2]
class MjpegCamera(Camera): class MjpegCamera(Camera):

View File

@ -10,14 +10,15 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera 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.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util 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__) _LOGGER = logging.getLogger(__name__)
@ -26,21 +27,34 @@ CONF_FORCE_RESIZE = 'force_resize'
CONF_IMAGE_QUALITY = 'image_quality' CONF_IMAGE_QUALITY = 'image_quality'
CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate' CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
CONF_MAX_IMAGE_WIDTH = 'max_image_width' CONF_MAX_IMAGE_WIDTH = 'max_image_width'
CONF_MAX_IMAGE_HEIGHT = 'max_image_height'
CONF_MAX_STREAM_WIDTH = 'max_stream_width' 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' CONF_STREAM_QUALITY = 'stream_quality'
MODE_RESIZE = 'resize'
MODE_CROP = 'crop'
DEFAULT_BASENAME = "Camera Proxy" DEFAULT_BASENAME = "Camera Proxy"
DEFAULT_QUALITY = 75 DEFAULT_QUALITY = 75
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id, 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_CACHE_IMAGES, False): cv.boolean,
vol.Optional(CONF_FORCE_RESIZE, 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_QUALITY): int,
vol.Optional(CONF_IMAGE_REFRESH_RATE): float, vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
vol.Optional(CONF_MAX_IMAGE_WIDTH): int, vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
vol.Optional(CONF_MAX_IMAGE_HEIGHT): int,
vol.Optional(CONF_MAX_STREAM_WIDTH): 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, vol.Optional(CONF_STREAM_QUALITY): int,
}) })
@ -51,26 +65,37 @@ async def async_setup_platform(
async_add_entities([ProxyCamera(hass, config)]) 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): def _resize_image(image, opts):
"""Resize image.""" """Resize image."""
from PIL import Image from PIL import Image
import io import io
if not opts: try:
img = _precheck_image(image, opts)
except ValueError:
return image return image
quality = opts.quality or DEFAULT_QUALITY quality = opts.quality or DEFAULT_QUALITY
new_width = opts.max_width 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_width, old_height) = img.size
old_size = len(image) old_size = len(image)
if old_width <= new_width: if old_width <= new_width:
@ -87,7 +112,7 @@ def _resize_image(image, opts):
img.save(imgbuf, 'JPEG', optimize=True, quality=quality) img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
newimage = imgbuf.getvalue() newimage = imgbuf.getvalue()
if not opts.force_resize and len(newimage) >= old_size: 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", "because resized image (%d bytes) is not smaller",
old_size, len(newimage)) old_size, len(newimage))
return image return image
@ -98,12 +123,50 @@ def _resize_image(image, opts):
return newimage 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(): class ImageOpts():
"""The representation of image options.""" """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.""" """Initialize image options."""
self.max_width = max_width self.max_width = max_width
self.max_height = max_height
self.left = left
self.top = top
self.quality = quality self.quality = quality
self.force_resize = force_resize self.force_resize = force_resize
@ -125,11 +188,18 @@ class ProxyCamera(Camera):
"{} - {}".format(DEFAULT_BASENAME, self._proxied_camera)) "{} - {}".format(DEFAULT_BASENAME, self._proxied_camera))
self._image_opts = ImageOpts( self._image_opts = ImageOpts(
config.get(CONF_MAX_IMAGE_WIDTH), 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_IMAGE_QUALITY),
config.get(CONF_FORCE_RESIZE)) config.get(CONF_FORCE_RESIZE))
self._stream_opts = ImageOpts( 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) True)
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE) self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
@ -141,6 +211,7 @@ class ProxyCamera(Camera):
self._headers = ( self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None else None) if self.hass.config.api.api_password is not None else None)
self._mode = config.get(CONF_MODE)
def camera_image(self): def camera_image(self):
"""Return camera image.""" """Return camera image."""
@ -162,8 +233,12 @@ class ProxyCamera(Camera):
_LOGGER.error("Error getting original camera image") _LOGGER.error("Error getting original camera image")
return self._last_image return self._last_image
image = await self.hass.async_add_job( if self._mode == MODE_RESIZE:
_resize_image, image.content, self._image_opts) 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: if self._cache_images:
self._last_image = image self._last_image = image
@ -192,7 +267,11 @@ class ProxyCamera(Camera):
if not image: if not image:
return None return None
except HomeAssistantError: except HomeAssistantError:
raise asyncio.CancelledError raise asyncio.CancelledError()
return await self.hass.async_add_job( if self._mode == MODE_RESIZE:
_resize_image, image.content, self._stream_opts) job = _resize_image
else:
job = _crop_image
return await self.hass.async_add_executor_job(
job, image.content, self._stream_opts)

View File

@ -5,35 +5,33 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/camera.push/ https://home-assistant.io/components/camera.push/
""" """
import logging import logging
import asyncio
from collections import deque from collections import deque
from datetime import timedelta from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import aiohttp
import async_timeout
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\ from homeassistant.components.camera import Camera, PLATFORM_SCHEMA,\
STATE_IDLE, STATE_RECORDING STATE_IDLE, STATE_RECORDING, DOMAIN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.http.view import KEY_AUTHENTICATED,\ from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID
HomeAssistantView
from homeassistant.const import CONF_NAME, CONF_TIMEOUT,\
HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['webhook']
DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__)
CONF_BUFFER_SIZE = 'buffer' CONF_BUFFER_SIZE = 'buffer'
CONF_IMAGE_FIELD = 'field' CONF_IMAGE_FIELD = 'field'
CONF_TOKEN = 'token'
DEFAULT_NAME = "Push Camera" DEFAULT_NAME = "Push Camera"
ATTR_FILENAME = 'filename' ATTR_FILENAME = 'filename'
ATTR_LAST_TRIP = 'last_trip' ATTR_LAST_TRIP = 'last_trip'
ATTR_TOKEN = 'token'
PUSH_CAMERA_DATA = 'push_camera' PUSH_CAMERA_DATA = 'push_camera'
@ -43,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All( vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
cv.time_period, cv.positive_timedelta), cv.time_period, cv.positive_timedelta),
vol.Optional(CONF_IMAGE_FIELD, default='image'): cv.string, 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: if PUSH_CAMERA_DATA not in hass.data:
hass.data[PUSH_CAMERA_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_BUFFER_SIZE],
config[CONF_TIMEOUT], config[CONF_TIMEOUT],
config.get(CONF_TOKEN))] config[CONF_IMAGE_FIELD],
webhook_id)]
hass.http.register_view(CameraPushReceiver(hass,
config[CONF_IMAGE_FIELD]))
async_add_entities(cameras) async_add_entities(cameras)
class CameraPushReceiver(HomeAssistantView): async def handle_webhook(hass, webhook_id, request):
"""Handle pushes from remote camera.""" """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}" camera = hass.data[PUSH_CAMERA_DATA][webhook_id]
name = 'api:camera_push:camera_entity'
requires_auth = False
def __init__(self, hass, image_field): if camera.image_field not in data:
"""Initialize CameraPushReceiver with camera entity.""" _LOGGER.warning("Webhook call without POST parameter <%s>",
self._cameras = hass.data[PUSH_CAMERA_DATA] camera.image_field)
self._image = image_field return
async def post(self, request, entity_id): await camera.update_image(data[camera.image_field].file.read(),
"""Accept the POST from Camera.""" data[camera.image_field].filename)
_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)
class PushCamera(Camera): class PushCamera(Camera):
"""The representation of a Push 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.""" """Initialize push camera component."""
super().__init__() super().__init__()
self._name = name self._name = name
@ -126,11 +98,28 @@ class PushCamera(Camera):
self._timeout = timeout self._timeout = timeout
self.queue = deque([], buffer_size) self.queue = deque([], buffer_size)
self._current_image = None 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): async def async_added_to_hass(self):
"""Call when entity is added to hass.""" """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 @property
def state(self): def state(self):
@ -189,6 +178,5 @@ class PushCamera(Camera):
name: value for name, value in ( name: value for name, value in (
(ATTR_LAST_TRIP, self._last_trip), (ATTR_LAST_TRIP, self._last_trip),
(ATTR_FILENAME, self._filename), (ATTR_FILENAME, self._filename),
(ATTR_TOKEN, self.token),
) if value is not None ) if value is not None
} }

View File

@ -10,12 +10,12 @@ import socket
import requests import requests
import voluptuous as vol 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 from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
REQUIREMENTS = ['uvcclient==0.10.1'] REQUIREMENTS = ['uvcclient==0.11.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,12 +25,14 @@ CONF_PASSWORD = 'password'
DEFAULT_PASSWORD = 'ubnt' DEFAULT_PASSWORD = 'ubnt'
DEFAULT_PORT = 7080 DEFAULT_PORT = 7080
DEFAULT_SSL = False
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NVR): cv.string, vol.Required(CONF_NVR): cv.string,
vol.Required(CONF_KEY): cv.string, vol.Required(CONF_KEY): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, 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] key = config[CONF_KEY]
password = config[CONF_PASSWORD] password = config[CONF_PASSWORD]
port = config[CONF_PORT] port = config[CONF_PORT]
ssl = config[CONF_SSL]
from uvcclient import nvr from uvcclient import nvr
try: try:
# Exceptions may be raised in all method calls to the nvr library. # 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() cameras = nvrconn.index()
identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid' identifier = 'id' if nvrconn.server_version >= (3, 2, 0) else 'uuid'

View File

@ -192,6 +192,11 @@ class DaikinClimate(ClimateDevice):
"""Return the name of the thermostat, if any.""" """Return the name of the thermostat, if any."""
return self._api.name return self._api.name
@property
def unique_id(self):
"""Return a unique ID."""
return self._api.mac
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement which this thermostat uses.""" """Return the unit of measurement which this thermostat uses."""

View File

@ -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 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.evohome/ https://home-assistant.io/components/climate.evohome/
@ -13,29 +13,34 @@ import logging
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_ECO, STATE_MANUAL, STATE_OFF,
STATE_AUTO,
STATE_ECO,
STATE_OFF,
SUPPORT_OPERATION_MODE,
SUPPORT_AWAY_MODE, SUPPORT_AWAY_MODE,
SUPPORT_ON_OFF,
SUPPORT_OPERATION_MODE,
SUPPORT_TARGET_TEMPERATURE,
ClimateDevice
) )
from homeassistant.components.evohome import ( from homeassistant.components.evohome import (
CONF_LOCATION_IDX, DATA_EVOHOME, DISPATCHER_EVOHOME,
DATA_EVOHOME, CONF_LOCATION_IDX, SCAN_INTERVAL_DEFAULT,
MAX_TEMP, EVO_PARENT, EVO_CHILD,
MIN_TEMP, GWS, TCS,
SCAN_INTERVAL_MAX
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
PRECISION_TENTHS,
TEMP_CELSIUS,
HTTP_TOO_MANY_REQUESTS, 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__) _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_RESET = 'AutoWithReset'
EVO_AUTO = 'Auto' EVO_AUTO = 'Auto'
EVO_AUTOECO = 'AutoWithEco' EVO_AUTOECO = 'AutoWithEco'
@ -44,7 +49,14 @@ EVO_DAYOFF = 'DayOff'
EVO_CUSTOM = 'Custom' EVO_CUSTOM = 'Custom'
EVO_HEATOFF = 'HeatingOff' 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_RESET: STATE_AUTO,
EVO_AUTO: STATE_AUTO, EVO_AUTO: STATE_AUTO,
EVO_AUTOECO: STATE_ECO, EVO_AUTOECO: STATE_ECO,
@ -53,171 +65,150 @@ EVO_STATE_TO_HA = {
EVO_CUSTOM: STATE_AUTO, EVO_CUSTOM: STATE_AUTO,
EVO_HEATOFF: STATE_OFF EVO_HEATOFF: STATE_OFF
} }
HA_STATE_TO_TCS = {
HA_STATE_TO_EVO = {
STATE_AUTO: EVO_AUTO, STATE_AUTO: EVO_AUTO,
STATE_ECO: EVO_AUTOECO, STATE_ECO: EVO_AUTOECO,
STATE_OFF: EVO_HEATOFF 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 # for the Zones...
GWS = 'gateways' ZONE_STATE_TO_HA = {
TCS = 'temperatureControlSystems' EVO_FOLLOW: STATE_AUTO,
EVO_TEMPOVER: STATE_MANUAL,
# debug codes - these happen occasionally, but the cause is unknown EVO_PERMOVER: STATE_MANUAL
EVO_DEBUG_NO_RECENT_UPDATES = '0x01' }
EVO_DEBUG_NO_STATUS = '0x02' 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): async def async_setup_platform(hass, hass_config, async_add_entities,
"""Create a Honeywell (EMEA/EU) evohome CH/DHW system. discovery_info=None):
"""Create the evohome Controller, and its Zones, if any."""
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.
"""
evo_data = hass.data[DATA_EVOHOME] evo_data = hass.data[DATA_EVOHOME]
client = evo_data['client'] client = evo_data['client']
loc_idx = evo_data['params'][CONF_LOCATION_IDX] loc_idx = evo_data['params'][CONF_LOCATION_IDX]
# evohomeclient has no defined way of accessing non-default location other # evohomeclient has exposed no means of accessing non-default location
# than using a protected member, such as below # (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 tcs_obj_ref = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa E501; pylint: disable=protected-access
_LOGGER.debug( _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.systemId,
tcs_obj_ref.modelType,
tcs_obj_ref.location.name, 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): class EvoClimateDevice(ClimateDevice):
"""Base for a Honeywell evohome hub/Controller device. """Base for a Honeywell evohome Climate device."""
The Controller (aka TCS, temperature control system) is the parent of all # pylint: disable=no-member
the child (CH/DHW) devices.
"""
def __init__(self, evo_data, client, obj_ref): def __init__(self, evo_data, client, obj_ref):
"""Initialize the evohome entity. """Initialize the evohome entity."""
self._client = client
Most read-only properties are set here. So are pseudo read-only,
for example name (which _could_ change between update()s).
"""
self.client = client
self._obj = obj_ref 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._params = evo_data['params']
self._timers = evo_data['timers'] self._timers = evo_data['timers']
self._timers['statusUpdated'] = datetime.min
self._status = {} self._status = {}
self._available = False # should become True after first update() self._available = False # should become True after first update()
def _handle_requests_exceptions(self, err): async def async_added_to_hass(self):
# evohomeclient v2 api (>=0.2.7) exposes requests exceptions, incl.: """Run when entity about to be added."""
# - HTTP_BAD_REQUEST, is usually Bad user credentials async_dispatcher_connect(self.hass, DISPATCHER_EVOHOME, self._connect)
# - HTTP_TOO_MANY_REQUESTS, is api usuage limit exceeded
# - HTTP_SERVICE_UNAVAILABLE, is often Vendor's fault
@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: if err.response.status_code == HTTP_TOO_MANY_REQUESTS:
# execute a back off: pause, and reduce rate # execute a backoff: pause, and also reduce rate
old_scan_interval = self._params[CONF_SCAN_INTERVAL] old_interval = self._params[CONF_SCAN_INTERVAL]
new_scan_interval = min(old_scan_interval * 2, SCAN_INTERVAL_MAX) new_interval = min(old_interval, SCAN_INTERVAL_DEFAULT) * 2
self._params[CONF_SCAN_INTERVAL] = new_scan_interval self._params[CONF_SCAN_INTERVAL] = new_interval
_LOGGER.warning( _LOGGER.warning(
"API rate limit has been exceeded: increasing '%s' from %s to " "API rate limit has been exceeded. Suspending polling for %s "
"%s seconds, and suspending polling for %s seconds.", "seconds, and increasing '%s' from %s to %s seconds.",
new_interval * 3,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
old_scan_interval, old_interval,
new_scan_interval, new_interval,
new_scan_interval * 3
) )
self._timers['statusUpdated'] = datetime.now() + \ self._timers['statusUpdated'] = datetime.now() + new_interval * 3
timedelta(seconds=new_scan_interval * 3)
else: else:
raise err raise err # we dont handle any other HTTPErrors
@property @property
def name(self): def name(self) -> str:
"""Return the name to use in the frontend UI.""" """Return the name to use in the frontend UI."""
return self._name return self._name
@property @property
def available(self): def icon(self):
"""Return True if the device is available. """Return the icon to use in the frontend UI."""
return self._icon
All evohome entities are initially unavailable. Once HA has started, @property
state data is then retrieved by the Controller, and then the children def device_state_attributes(self):
will get a state (e.g. operating_mode, current_temperature). """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 return self._available
@property @property
def supported_features(self): def supported_features(self):
"""Get the list of supported features of the Controller.""" """Get the list of supported features of the device."""
return SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE return self._supported_features
@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
@property @property
def operation_list(self): def operation_list(self):
"""Return the list of available operations.""" """Return the list of available operations."""
return HA_OP_LIST return self._operation_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
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -227,47 +218,313 @@ class EvoController(ClimateDevice):
@property @property
def precision(self): def precision(self):
"""Return the temperature precision to use in the frontend UI.""" """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 @property
def min_temp(self): def min_temp(self):
"""Return the minimum target temp (setpoint) of a evohome entity.""" """Return the minimum target temperature of a evohome Zone.
return MIN_TEMP
The default is 5 (in Celsius), but it is configurable within 5-35.
"""
return self._config['setpointCapabilities']['minHeatSetpoint']
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum target temp (setpoint) of a evohome entity.""" """Return the minimum target temperature of a evohome Zone.
return MAX_TEMP
The default is 35 (in Celsius), but it is configurable within 5-35.
"""
return self._config['setpointCapabilities']['maxHeatSetpoint']
@property @property
def is_on(self): def target_temperature(self):
"""Return true as evohome controllers are always on. """Return the target temperature of the evohome Zone."""
return self._status['setpointStatus']['targetHeatTemperature']
Operating modes can include 'HeatingOff', but (for example) DHW would @property
remain on. 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 return True
@property @property
def is_away_mode_on(self): def is_away_mode_on(self) -> bool:
"""Return true if away mode is on.""" """Return True if away mode is on."""
return self._status['systemModeStatus']['mode'] == EVO_AWAY return self._status['systemModeStatus']['mode'] == EVO_AWAY
def turn_away_mode_on(self): 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) self._set_operation_mode(EVO_AWAY)
def turn_away_mode_off(self): 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) self._set_operation_mode(EVO_AUTO)
def _set_operation_mode(self, operation_mode): 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: try:
self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access self._obj._set_status(operation_mode) # noqa: E501; pylint: disable=protected-access
except HTTPError as err: except HTTPError as err:
@ -279,93 +536,45 @@ class EvoController(ClimateDevice):
Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away' Currently limited to 'Auto', 'AutoWithEco' & 'HeatingOff'. If 'Away'
mode is needed, it can be enabled via turn_away_mode_on method. 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): @property
client = evo_data['client'] def should_poll(self) -> bool:
loc_idx = evo_data['params'][CONF_LOCATION_IDX] """Return True as the evohome Controller should always be polled."""
return True
_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']
)
def update(self): 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 This includes state data for the Controller and all its child devices,
as the operating_mode of the Controller and the current_temperature such as the operating mode of the Controller and the current temp of
of its children. its children (e.g. Zones, DHW controller).
This is not asyncio-friendly due to the underlying client api.
""" """
evo_data = self.hass.data[DATA_EVOHOME] # should the latest evohome state data be retreived this cycle?
timeout = datetime.now() + timedelta(seconds=55) timeout = datetime.now() + timedelta(seconds=55)
expired = timeout > self._timers['statusUpdated'] + \ expired = timeout > self._timers['statusUpdated'] + \
timedelta(seconds=evo_data['params'][CONF_SCAN_INTERVAL]) self._params[CONF_SCAN_INTERVAL]
if not expired: if not expired:
return return
was_available = self._available or \ # Retreive the latest state data via the client api
self._timers['statusUpdated'] == datetime.min loc_idx = self._params[CONF_LOCATION_IDX]
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
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: else:
self._timers['statusUpdated'] = datetime.now()
self._available = True self._available = True
if not self._available and was_available: _LOGGER.debug(
# only warn if available went from True to False "_update_state_data(): self._status = %s",
_LOGGER.warning( self._status
"The entity, %s, has become unavailable, debug code is: %s", )
self._id + " [" + self._name + "]",
debug_code
)
elif self._available and not was_available: # inform the child devices that state data has been updated
# this isn't the first re-available (e.g. _after_ STARTUP) pkt = {'sender': 'controller', 'signal': 'refresh', 'to': EVO_CHILD}
_LOGGER.debug( dispatcher_send(self.hass, DISPATCHER_EVOHOME, pkt)
"The entity, %s, has become available",
self._id + " [" + self._name + "]"
)

View File

@ -23,7 +23,7 @@ from homeassistant.helpers import condition
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_time_interval) async_track_state_change, async_track_time_interval)
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
@ -96,7 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities,
precision)]) precision)])
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice, RestoreEntity):
"""Representation of a Generic Thermostat device.""" """Representation of a Generic Thermostat device."""
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, 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): async def async_added_to_hass(self):
"""Run when entity about to be added.""" """Run when entity about to be added."""
await super().async_added_to_hass()
# Check If we have an old state # 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 old_state is not None:
# If we have no initial temperature, restore # If we have no initial temperature, restore
if self._target_temp is None: if self._target_temp is None:

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_TEMPERATURE, CONF_REGION) 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__) _LOGGER = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.2.8'] REQUIREMENTS = ['millheater==0.2.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -22,7 +22,8 @@ from homeassistant.const import (
from homeassistant.components.mqtt import ( from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_QOS, CONF_RETAIN,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, 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 from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -77,6 +78,18 @@ CONF_MIN_TEMP = 'min_temp'
CONF_MAX_TEMP = 'max_temp' CONF_MAX_TEMP = 'max_temp'
CONF_TEMP_STEP = 'temp_step' 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) SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({ PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, 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, async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None): discovery_hash=None):
"""Set up the MQTT climate devices.""" """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([ async_add_entities([
MqttClimate( MqttClimate(
hass, hass,
config.get(CONF_NAME), config,
{
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),
discovery_hash, discovery_hash,
)]) )])
@ -223,54 +177,103 @@ async def _async_setup_entity(hass, config, async_add_entities,
class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice): class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Representation of an MQTT climate device.""" """Representation of an MQTT climate device."""
def __init__(self, hass, name, topic, value_templates, qos, retain, def __init__(self, hass, config, discovery_hash):
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):
"""Initialize the climate device.""" """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, MqttAvailability.__init__(self, availability_topic, qos,
payload_available, payload_not_available) payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash) MqttDiscoveryUpdate.__init__(self, discovery_hash,
self.hass = hass self.discovery_update)
self._name = name
self._topic = topic async def async_added_to_hass(self):
self._value_templates = value_templates """Handle being added to home assistant."""
self._qos = qos await super().async_added_to_hass()
self._retain = retain 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 # set to None in non-optimistic mode
self._target_temperature = self._current_fan_mode = \ self._target_temperature = self._current_fan_mode = \
self._current_operation = self._current_swing_mode = None self._current_operation = self._current_swing_mode = None
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None: if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is None:
self._target_temperature = target_temperature self._target_temperature = config.get(CONF_INITIAL)
self._unit_of_measurement = hass.config.units.temperature_unit
self._away = away
self._hold = hold
self._current_temperature = None
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = current_fan_mode self._current_fan_mode = SPEED_LOW
if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = current_operation
self._aux = aux
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = current_swing_mode self._current_swing_mode = STATE_OFF
self._fan_list = fan_mode_list if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._operation_list = mode_list self._current_operation = STATE_OFF
self._swing_list = swing_mode_list self._away = False
self._target_temperature_step = temp_step self._hold = None
self._send_if_off = send_if_off self._aux = False
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
async def async_added_to_hass(self): value_templates = {}
"""Handle being added to home assistant.""" if CONF_VALUE_TEMPLATE in config:
await MqttAvailability.async_added_to_hass(self) value_template = config.get(CONF_VALUE_TEMPLATE)
await MqttDiscoveryUpdate.async_added_to_hass(self) 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 @callback
def handle_current_temp_received(topic, payload, qos): 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) _LOGGER.error("Could not parse temperature from %s", payload)
if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None: if self._topic[CONF_CURRENT_TEMPERATURE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_CURRENT_TEMPERATURE_TOPIC] = {
self.hass, self._topic[CONF_CURRENT_TEMPERATURE_TOPIC], 'topic': self._topic[CONF_CURRENT_TEMPERATURE_TOPIC],
handle_current_temp_received, self._qos) 'msg_callback': handle_current_temp_received,
'qos': qos}
@callback @callback
def handle_mode_received(topic, payload, qos): def handle_mode_received(topic, payload, qos):
@ -298,16 +302,17 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\ payload = self._value_templates[CONF_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload) 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) _LOGGER.error("Invalid mode: %s", payload)
else: else:
self._current_operation = payload self._current_operation = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_MODE_STATE_TOPIC] is not None: if self._topic[CONF_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_MODE_STATE_TOPIC] = {
self.hass, self._topic[CONF_MODE_STATE_TOPIC], 'topic': self._topic[CONF_MODE_STATE_TOPIC],
handle_mode_received, self._qos) 'msg_callback': handle_mode_received,
'qos': qos}
@callback @callback
def handle_temperature_received(topic, payload, qos): 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) _LOGGER.error("Could not parse temperature from %s", payload)
if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None: if self._topic[CONF_TEMPERATURE_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_TEMPERATURE_STATE_TOPIC] = {
self.hass, self._topic[CONF_TEMPERATURE_STATE_TOPIC], 'topic': self._topic[CONF_TEMPERATURE_STATE_TOPIC],
handle_temperature_received, self._qos) 'msg_callback': handle_temperature_received,
'qos': qos}
@callback @callback
def handle_fan_mode_received(topic, payload, qos): 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].\ self._value_templates[CONF_FAN_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload) 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) _LOGGER.error("Invalid fan mode: %s", payload)
else: else:
self._current_fan_mode = payload self._current_fan_mode = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None: if self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_FAN_MODE_STATE_TOPIC] = {
self.hass, self._topic[CONF_FAN_MODE_STATE_TOPIC], 'topic': self._topic[CONF_FAN_MODE_STATE_TOPIC],
handle_fan_mode_received, self._qos) 'msg_callback': handle_fan_mode_received,
'qos': qos}
@callback @callback
def handle_swing_mode_received(topic, payload, qos): 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].\ self._value_templates[CONF_SWING_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload) 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) _LOGGER.error("Invalid swing mode: %s", payload)
else: else:
self._current_swing_mode = payload self._current_swing_mode = payload
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None: if self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_SWING_MODE_STATE_TOPIC] = {
self.hass, self._topic[CONF_SWING_MODE_STATE_TOPIC], 'topic': self._topic[CONF_SWING_MODE_STATE_TOPIC],
handle_swing_mode_received, self._qos) 'msg_callback': handle_swing_mode_received,
'qos': qos}
@callback @callback
def handle_away_mode_received(topic, payload, qos): def handle_away_mode_received(topic, payload, qos):
"""Handle receiving away mode via MQTT.""" """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: if CONF_AWAY_MODE_STATE_TEMPLATE in self._value_templates:
payload = \ payload = \
self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\ self._value_templates[CONF_AWAY_MODE_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload) async_render_with_possible_json_value(payload)
if payload == "True": if payload == "True":
payload = self._payload_on payload = payload_on
elif payload == "False": elif payload == "False":
payload = self._payload_off payload = payload_off
if payload == self._payload_on: if payload == payload_on:
self._away = True self._away = True
elif payload == self._payload_off: elif payload == payload_off:
self._away = False self._away = False
else: else:
_LOGGER.error("Invalid away mode: %s", payload) _LOGGER.error("Invalid away mode: %s", payload)
@ -388,24 +398,27 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_AWAY_MODE_STATE_TOPIC] = {
self.hass, self._topic[CONF_AWAY_MODE_STATE_TOPIC], 'topic': self._topic[CONF_AWAY_MODE_STATE_TOPIC],
handle_away_mode_received, self._qos) 'msg_callback': handle_away_mode_received,
'qos': qos}
@callback @callback
def handle_aux_mode_received(topic, payload, qos): def handle_aux_mode_received(topic, payload, qos):
"""Handle receiving aux mode via MQTT.""" """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: if CONF_AUX_STATE_TEMPLATE in self._value_templates:
payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\ payload = self._value_templates[CONF_AUX_STATE_TEMPLATE].\
async_render_with_possible_json_value(payload) async_render_with_possible_json_value(payload)
if payload == "True": if payload == "True":
payload = self._payload_on payload = payload_on
elif payload == "False": elif payload == "False":
payload = self._payload_off payload = payload_off
if payload == self._payload_on: if payload == payload_on:
self._aux = True self._aux = True
elif payload == self._payload_off: elif payload == payload_off:
self._aux = False self._aux = False
else: else:
_LOGGER.error("Invalid aux mode: %s", payload) _LOGGER.error("Invalid aux mode: %s", payload)
@ -413,9 +426,10 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_AUX_STATE_TOPIC] is not None: if self._topic[CONF_AUX_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_AUX_STATE_TOPIC] = {
self.hass, self._topic[CONF_AUX_STATE_TOPIC], 'topic': self._topic[CONF_AUX_STATE_TOPIC],
handle_aux_mode_received, self._qos) 'msg_callback': handle_aux_mode_received,
'qos': qos}
@callback @callback
def handle_hold_mode_received(topic, payload, qos): def handle_hold_mode_received(topic, payload, qos):
@ -428,9 +442,19 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_HOLD_STATE_TOPIC] is not None: if self._topic[CONF_HOLD_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_HOLD_STATE_TOPIC] = {
self.hass, self._topic[CONF_HOLD_STATE_TOPIC], 'topic': self._topic[CONF_HOLD_STATE_TOPIC],
handle_hold_mode_received, self._qos) '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 @property
def should_poll(self): def should_poll(self):
@ -440,7 +464,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property @property
def name(self): def name(self):
"""Return the name of the climate device.""" """Return the name of the climate device."""
return self._name return self._config.get(CONF_NAME)
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -465,12 +489,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property @property
def operation_list(self): def operation_list(self):
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
return self._operation_list return self._config.get(CONF_MODE_LIST)
@property @property
def target_temperature_step(self): def target_temperature_step(self):
"""Return the supported step of target temperature.""" """Return the supported step of target temperature."""
return self._target_temperature_step return self._config.get(CONF_TEMP_STEP)
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
@ -495,7 +519,7 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property @property
def fan_list(self): def fan_list(self):
"""Return the list of available fan modes.""" """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): async def async_set_temperature(self, **kwargs):
"""Set new target temperatures.""" """Set new target temperatures."""
@ -508,19 +532,23 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
# optimistic mode # optimistic mode
self._target_temperature = kwargs.get(ATTR_TEMPERATURE) 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( mqtt.async_publish(
self.hass, self._topic[CONF_TEMPERATURE_COMMAND_TOPIC], 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() self.async_schedule_update_ha_state()
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode):
"""Set new 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( mqtt.async_publish(
self.hass, self._topic[CONF_SWING_MODE_COMMAND_TOPIC], 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: if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None:
self._current_swing_mode = swing_mode self._current_swing_mode = swing_mode
@ -528,10 +556,12 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
"""Set new target temperature.""" """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( mqtt.async_publish(
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC], 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: if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
self._current_fan_mode = fan_mode 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: async def async_set_operation_mode(self, operation_mode) -> None:
"""Set new operation mode.""" """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._topic[CONF_POWER_COMMAND_TOPIC] is not None:
if (self._current_operation == STATE_OFF and if (self._current_operation == STATE_OFF and
operation_mode != STATE_OFF): operation_mode != STATE_OFF):
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC], 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 elif (self._current_operation != STATE_OFF and
operation_mode == STATE_OFF): operation_mode == STATE_OFF):
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_POWER_COMMAND_TOPIC], 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: if self._topic[CONF_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_MODE_COMMAND_TOPIC], 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: if self._topic[CONF_MODE_STATE_TOPIC] is None:
self._current_operation = operation_mode self._current_operation = operation_mode
@ -568,14 +600,16 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property @property
def swing_list(self): def swing_list(self):
"""List of available swing modes.""" """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): async def async_turn_away_mode_on(self):
"""Turn away mode on.""" """Turn away mode on."""
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, mqtt.async_publish(self.hass,
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC], 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: if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
self._away = True self._away = True
@ -586,7 +620,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None: if self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, mqtt.async_publish(self.hass,
self._topic[CONF_AWAY_MODE_COMMAND_TOPIC], 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: if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is None:
self._away = False self._away = False
@ -597,7 +633,8 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None: if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, mqtt.async_publish(self.hass,
self._topic[CONF_HOLD_COMMAND_TOPIC], 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: if self._topic[CONF_HOLD_STATE_TOPIC] is None:
self._hold = hold_mode self._hold = hold_mode
@ -607,7 +644,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Turn auxiliary heater on.""" """Turn auxiliary heater on."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], 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: if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = True self._aux = True
@ -617,7 +656,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
"""Turn auxiliary heater off.""" """Turn auxiliary heater off."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None: if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC], 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: if self._topic[CONF_AUX_STATE_TOPIC] is None:
self._aux = False self._aux = False
@ -661,9 +702,9 @@ class MqttClimate(MqttAvailability, MqttDiscoveryUpdate, ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Return the minimum temperature.""" """Return the minimum temperature."""
return self._min_temp return self._config.get(CONF_MIN_TEMP)
@property @property
def max_temp(self): def max_temp(self):
"""Return the maximum temperature.""" """Return the maximum temperature."""
return self._max_temp return self._config.get(CONF_MAX_TEMP)

View File

@ -15,6 +15,14 @@ from homeassistant.const import TEMP_CELSIUS
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE 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): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Toon climate device.""" """Set up the Toon climate device."""
@ -58,8 +66,7 @@ class ThermostatDevice(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation i.e. comfort, home, away.""" """Return current operation i.e. comfort, home, away."""
state = self.thermos.get_data('state') return TOON_HA.get(self.thermos.get_data('state'))
return state
@property @property
def operation_list(self): def operation_list(self):
@ -83,14 +90,7 @@ class ThermostatDevice(ClimateDevice):
def set_operation_mode(self, operation_mode): def set_operation_mode(self, operation_mode):
"""Set new operation mode.""" """Set new operation mode."""
toonlib_values = { self.thermos.set_state(HA_TOON[operation_mode])
STATE_AUTO: 'Comfort',
STATE_HEAT: 'Home',
STATE_ECO: 'Away',
STATE_COOL: 'Sleep',
}
self.thermos.set_state(toonlib_values[operation_mode])
def update(self): def update(self):
"""Update local state.""" """Update local state."""

View File

@ -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 helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c 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 from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1'] REQUIREMENTS = ['warrant==0.6.1']
@ -37,6 +37,7 @@ CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id' CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url' CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
CONF_CLOUDHOOK_CREATE_URL = 'cloudhook_create_url'
DEFAULT_MODE = 'production' DEFAULT_MODE = 'production'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -78,6 +79,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_RELAYER): str, vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str, vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_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_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}), }),
@ -113,7 +115,7 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions, def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None, cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=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.""" """Create an instance of Cloud."""
self.hass = hass self.hass = hass
self.mode = mode self.mode = mode
@ -125,6 +127,7 @@ class Cloud:
self.access_token = None self.access_token = None
self.refresh_token = None self.refresh_token = None
self.iot = iot.CloudIoT(self) self.iot = iot.CloudIoT(self)
self.cloudhooks = cloudhooks.Cloudhooks(self)
if mode == MODE_DEV: if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id self.cognito_client_id = cognito_client_id
@ -133,6 +136,7 @@ class Cloud:
self.relayer = relayer self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url self.subscription_info_url = subscription_info_url
self.cloudhook_create_url = cloudhook_create_url
else: else:
info = SERVERS[mode] info = SERVERS[mode]
@ -143,6 +147,7 @@ class Cloud:
self.relayer = info['relayer'] self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url'] self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url'] self.subscription_info_url = info['subscription_info_url']
self.cloudhook_create_url = info['cloudhook_create_url']
@property @property
def is_logged_in(self): def is_logged_in(self):
@ -247,8 +252,7 @@ class Cloud:
return json.loads(file.read()) return json.loads(file.read())
info = await self.hass.async_add_job(load_config) info = await self.hass.async_add_job(load_config)
await self.prefs.async_initialize()
await self.prefs.async_initialize(bool(info))
if info is None: if info is None:
return return

View File

@ -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
})

View File

@ -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()

View File

@ -6,6 +6,7 @@ REQUEST_TIMEOUT = 10
PREF_ENABLE_ALEXA = 'alexa_enabled' PREF_ENABLE_ALEXA = 'alexa_enabled'
PREF_ENABLE_GOOGLE = 'google_enabled' PREF_ENABLE_GOOGLE = 'google_enabled'
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock' PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
PREF_CLOUDHOOKS = 'cloudhooks'
SERVERS = { SERVERS = {
'production': { 'production': {
@ -16,7 +17,8 @@ SERVERS = {
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.' 'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'), 'amazonaws.com/prod/smart_home_sync'),
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/' 'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
'subscription_info') 'subscription_info'),
'cloudhook_create_url': 'https://webhooks-api.nabucasa.com/generate'
} }
} }

View File

@ -3,6 +3,7 @@ import asyncio
from functools import wraps from functools import wraps
import logging import logging
import aiohttp
import async_timeout import async_timeout
import voluptuous as vol 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): async def async_setup(hass):
"""Initialize the HTTP API.""" """Initialize the HTTP API."""
hass.components.websocket_api.async_register_command( hass.components.websocket_api.async_register_command(
@ -58,6 +73,14 @@ async def async_setup(hass):
WS_TYPE_UPDATE_PREFS, websocket_update_prefs, WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
SCHEMA_WS_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(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudLogoutView)
@ -76,7 +99,7 @@ _CLOUD_ERRORS = {
def _handle_cloud_errors(handler): def _handle_cloud_errors(handler):
"""Handle auth errors.""" """Webview decorator to handle auth errors."""
@wraps(handler) @wraps(handler)
async def error_handler(view, request, *args, **kwargs): async def error_handler(view, request, *args, **kwargs):
"""Handle exceptions that raise from the wrapped request handler.""" """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))) 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 @websocket_api.async_response
async def websocket_subscription(hass, connection, msg): async def websocket_subscription(hass, connection, msg):
"""Handle request for account info.""" """Handle request for account info."""
cloud = hass.data[DOMAIN] 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): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info() 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)) connection.send_message(websocket_api.result_message(msg['id'], data))
@_require_cloud_login
@websocket_api.async_response @websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg): async def websocket_update_prefs(hass, connection, msg):
"""Handle request for account info.""" """Handle request for account info."""
cloud = hass.data[DOMAIN] 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 = dict(msg)
changes.pop('id') changes.pop('id')
changes.pop('type') changes.pop('type')
await cloud.prefs.async_update(**changes) await cloud.prefs.async_update(**changes)
connection.send_message(websocket_api.result_message( connection.send_message(websocket_api.result_message(msg['id']))
msg['id'], {'success': True}))
@_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): def _account_data(cloud):

View File

@ -2,13 +2,16 @@
import asyncio import asyncio
import logging import logging
import pprint import pprint
import uuid
from aiohttp import hdrs, client_exceptions, WSMsgType from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home as alexa from homeassistant.components.alexa import smart_home as alexa
from homeassistant.components.google_assistant import smart_home as ga from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.core import callback
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
from homeassistant.util.aiohttp import MockRequest, serialize_response
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api from . import auth_api
from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL from .const import MESSAGE_EXPIRATION, MESSAGE_AUTH_FAIL
@ -25,6 +28,19 @@ class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler.""" """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 CloudIoT:
"""Class to manage the IoT connection.""" """Class to manage the IoT connection."""
@ -41,6 +57,19 @@ class CloudIoT:
self.tries = 0 self.tries = 0
# Current state of the connection # Current state of the connection
self.state = STATE_DISCONNECTED 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 @asyncio.coroutine
def connect(self): def connect(self):
@ -91,6 +120,30 @@ class CloudIoT:
if remove_hass_stop_listener is not None: if remove_hass_stop_listener is not None:
remove_hass_stop_listener() 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 @asyncio.coroutine
def _handle_connection(self): def _handle_connection(self):
"""Connect to the IoT broker.""" """Connect to the IoT broker."""
@ -134,6 +187,9 @@ class CloudIoT:
_LOGGER.info("Connected") _LOGGER.info("Connected")
self.state = STATE_CONNECTED self.state = STATE_CONNECTED
if self._on_connect:
yield from asyncio.wait([cb() for cb in self._on_connect])
while not client.closed: while not client.closed:
msg = yield from client.receive() msg = yield from client.receive()
@ -159,6 +215,17 @@ class CloudIoT:
_LOGGER.debug("Received message:\n%s\n", _LOGGER.debug("Received message:\n%s\n",
pprint.pformat(msg)) 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 = { response = {
'msgid': msg['msgid'], 'msgid': msg['msgid'],
} }
@ -257,3 +324,43 @@ def async_handle_cloud(hass, cloud, payload):
payload['reason']) payload['reason'])
else: else:
_LOGGER.warning("Received unknown cloud action: %s", action) _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
}
}

View File

@ -1,7 +1,7 @@
"""Preference management for cloud.""" """Preference management for cloud."""
from .const import ( from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
PREF_GOOGLE_ALLOW_UNLOCK) PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS)
STORAGE_KEY = DOMAIN STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1 STORAGE_VERSION = 1
@ -16,28 +16,29 @@ class CloudPreferences:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._prefs = None self._prefs = None
async def async_initialize(self, logged_in): async def async_initialize(self):
"""Finish initializing the preferences.""" """Finish initializing the preferences."""
prefs = await self._store.async_load() prefs = await self._store.async_load()
if prefs is None: if prefs is None:
# Backwards compat: we enable alexa/google if already logged in
prefs = { prefs = {
PREF_ENABLE_ALEXA: logged_in, PREF_ENABLE_ALEXA: True,
PREF_ENABLE_GOOGLE: logged_in, PREF_ENABLE_GOOGLE: True,
PREF_GOOGLE_ALLOW_UNLOCK: False, PREF_GOOGLE_ALLOW_UNLOCK: False,
PREF_CLOUDHOOKS: {}
} }
await self._store.async_save(prefs)
self._prefs = prefs self._prefs = prefs
async def async_update(self, *, google_enabled=_UNDEF, 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.""" """Update user preferences."""
for key, value in ( for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_GOOGLE, google_enabled),
(PREF_ENABLE_ALEXA, alexa_enabled), (PREF_ENABLE_ALEXA, alexa_enabled),
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock), (PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
(PREF_CLOUDHOOKS, cloudhooks),
): ):
if value is not _UNDEF: if value is not _UNDEF:
self._prefs[key] = value self._prefs[key] = value
@ -62,3 +63,8 @@ class CloudPreferences:
def google_allow_unlock(self): def google_allow_unlock(self):
"""Return if Google is allowed to unlock locks.""" """Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False) return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""
return self._prefs.get(PREF_CLOUDHOOKS, {})

View File

@ -14,6 +14,8 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config' DOMAIN = 'config'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
SECTIONS = ( SECTIONS = (
'auth',
'auth_provider_homeassistant',
'automation', 'automation',
'config_entries', 'config_entries',
'core', 'core',
@ -58,10 +60,6 @@ async def async_setup(hass, config):
tasks = [setup_panel(panel_name) for panel_name in SECTIONS] 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: for panel_name in ON_DEMAND:
if panel_name in hass.config.components: if panel_name in hass.config.components:
tasks.append(setup_panel(panel_name)) tasks.append(setup_panel(panel_name))

View File

@ -10,9 +10,8 @@ import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent 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__) _LOGGER = logging.getLogger(__name__)
@ -86,7 +85,7 @@ async def async_setup(hass, config):
return True return True
class Counter(Entity): class Counter(RestoreEntity):
"""Representation of a counter.""" """Representation of a counter."""
def __init__(self, object_id, name, initial, restore, step, icon): def __init__(self, object_id, name, initial, restore, step, icon):
@ -128,10 +127,11 @@ class Counter(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Call when entity about to be added to Home Assistant.""" """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 # __init__ will set self._state to self._initial, only override
# if needed. # if needed.
if self._restore: 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: if state is not None:
self._state = int(state.state) self._state = int(state.state)

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mqtt/ https://home-assistant.io/components/cover.mqtt/
""" """
import logging import logging
from typing import Optional
import voluptuous as vol import voluptuous as vol
@ -24,7 +23,7 @@ from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, valid_publish_topic, valid_subscribe_topic, 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 from homeassistant.components.mqtt.discovery import MQTT_DISCOVERY_NEW
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect 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 def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
async_add_entities, discovery_info=None): async_add_entities, discovery_info=None):
"""Set up MQTT cover through configuration.yaml.""" """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): 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): async def async_discover(discovery_payload):
"""Discover and add an MQTT cover.""" """Discover and add an MQTT cover."""
config = PLATFORM_SCHEMA(discovery_payload) 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]) discovery_payload[ATTR_DISCOVERY_HASH])
async_dispatcher_connect( async_dispatcher_connect(
@ -146,112 +145,78 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_discover) async_discover)
async def _async_setup_entity(hass, config, async_add_entities, async def _async_setup_entity(config, async_add_entities, discovery_hash=None):
discovery_hash=None):
"""Set up the MQTT Cover.""" """Set up the MQTT Cover."""
value_template = config.get(CONF_VALUE_TEMPLATE) async_add_entities([MqttCover(config, discovery_hash)])
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
)])
class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
CoverDevice): CoverDevice):
"""Representation of a cover that can be controlled using MQTT.""" """Representation of a cover that can be controlled using MQTT."""
def __init__(self, name, state_topic, get_position_topic, def __init__(self, config, discovery_hash):
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):
"""Initialize the cover.""" """Initialize the cover."""
MqttAvailability.__init__(self, availability_topic, qos, self._unique_id = config.get(CONF_UNIQUE_ID)
payload_available, payload_not_available)
MqttDiscoveryUpdate.__init__(self, discovery_hash)
MqttEntityDeviceInfo.__init__(self, device_config)
self._position = None self._position = None
self._state = None self._state = None
self._name = name self._sub_state = None
self._state_topic = state_topic
self._get_position_topic = get_position_topic self._optimistic = None
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._tilt_value = None self._tilt_value = None
self._tilt_min = tilt_min self._tilt_optimistic = None
self._tilt_max = tilt_max
self._tilt_optimistic = tilt_optimistic # Load config
self._tilt_invert = tilt_invert self._setup_from_config(config)
self._set_position_topic = set_position_topic
self._set_position_template = set_position_template availability_topic = config.get(CONF_AVAILABILITY_TOPIC)
self._unique_id = unique_id payload_available = config.get(CONF_PAYLOAD_AVAILABLE)
self._discovery_hash = discovery_hash 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): async def async_added_to_hass(self):
"""Subscribe MQTT events.""" """Subscribe MQTT events."""
await MqttAvailability.async_added_to_hass(self) await super().async_added_to_hass()
await MqttDiscoveryUpdate.async_added_to_hass(self) 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 @callback
def tilt_updated(topic, payload, qos): def tilt_updated(topic, payload, qos):
"""Handle tilt updates.""" """Handle tilt updates."""
if (payload.isnumeric() and 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)) level = self.find_percentage_in_range(float(payload))
self._tilt_value = level self._tilt_value = level
@ -260,13 +225,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@callback @callback
def state_message_received(topic, payload, qos): def state_message_received(topic, payload, qos):
"""Handle new MQTT state messages.""" """Handle new MQTT state messages."""
if self._template is not None: if template is not None:
payload = self._template.async_render_with_possible_json_value( payload = template.async_render_with_possible_json_value(
payload) payload)
if payload == self._state_open: if payload == self._config.get(CONF_STATE_OPEN):
self._state = False self._state = False
elif payload == self._state_closed: elif payload == self._config.get(CONF_STATE_CLOSED):
self._state = True self._state = True
else: else:
_LOGGER.warning("Payload is not True or False: %s", payload) _LOGGER.warning("Payload is not True or False: %s", payload)
@ -276,8 +241,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@callback @callback
def position_message_received(topic, payload, qos): def position_message_received(topic, payload, qos):
"""Handle new MQTT state messages.""" """Handle new MQTT state messages."""
if self._template is not None: if template is not None:
payload = self._template.async_render_with_possible_json_value( payload = template.async_render_with_possible_json_value(
payload) payload)
if payload.isnumeric(): if payload.isnumeric():
@ -292,25 +257,38 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
return return
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._get_position_topic: if self._config.get(CONF_GET_POSITION_TOPIC):
await mqtt.async_subscribe( topics['get_position_topic'] = {
self.hass, self._get_position_topic, 'topic': self._config.get(CONF_GET_POSITION_TOPIC),
position_message_received, self._qos) 'msg_callback': position_message_received,
elif self._state_topic: 'qos': self._config.get(CONF_QOS)}
await mqtt.async_subscribe( elif self._config.get(CONF_STATE_TOPIC):
self.hass, self._state_topic, topics['state_topic'] = {
state_message_received, self._qos) 'topic': self._config.get(CONF_STATE_TOPIC),
'msg_callback': state_message_received,
'qos': self._config.get(CONF_QOS)}
else: else:
# Force into optimistic mode. # Force into optimistic mode.
self._optimistic = True self._optimistic = True
if self._tilt_status_topic is None: if self._config.get(CONF_TILT_STATUS_TOPIC) is None:
self._tilt_optimistic = True self._tilt_optimistic = True
else: else:
self._tilt_optimistic = False self._tilt_optimistic = False
self._tilt_value = STATE_UNKNOWN self._tilt_value = STATE_UNKNOWN
await mqtt.async_subscribe( topics['tilt_status_topic'] = {
self.hass, self._tilt_status_topic, tilt_updated, self._qos) '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 @property
def should_poll(self): def should_poll(self):
@ -325,7 +303,7 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@property @property
def name(self): def name(self):
"""Return the name of the cover.""" """Return the name of the cover."""
return self._name return self._config.get(CONF_NAME)
@property @property
def is_closed(self): def is_closed(self):
@ -349,13 +327,13 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
supported_features = 0 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 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 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 supported_features |= TILT_FEATURES
return supported_features return supported_features
@ -366,14 +344,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine. This method is a coroutine.
""" """
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_open, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) self._config.get(CONF_PAYLOAD_OPEN), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = False 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 = self.find_percentage_in_range(
self._position_open, COVER_PAYLOAD) self._config.get(CONF_POSITION_OPEN), COVER_PAYLOAD)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
@ -382,14 +361,15 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine. This method is a coroutine.
""" """
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_close, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) self._config.get(CONF_PAYLOAD_CLOSE), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic: if self._optimistic:
# Optimistically assume that cover has changed state. # Optimistically assume that cover has changed state.
self._state = True 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 = self.find_percentage_in_range(
self._position_closed, COVER_PAYLOAD) self._config.get(CONF_POSITION_CLOSED), COVER_PAYLOAD)
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
async def async_stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
@ -398,25 +378,30 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
This method is a coroutine. This method is a coroutine.
""" """
mqtt.async_publish( mqtt.async_publish(
self.hass, self._command_topic, self._payload_stop, self._qos, self.hass, self._config.get(CONF_COMMAND_TOPIC),
self._retain) self._config.get(CONF_PAYLOAD_STOP), self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
async def async_open_cover_tilt(self, **kwargs): async def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open.""" """Tilt the cover open."""
mqtt.async_publish(self.hass, self._tilt_command_topic, mqtt.async_publish(self.hass,
self._tilt_open_position, self._qos, self._config.get(CONF_TILT_COMMAND_TOPIC),
self._retain) self._config.get(CONF_TILT_OPEN_POSITION),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._tilt_optimistic: 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() self.async_schedule_update_ha_state()
async def async_close_cover_tilt(self, **kwargs): async def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed.""" """Tilt the cover closed."""
mqtt.async_publish(self.hass, self._tilt_command_topic, mqtt.async_publish(self.hass,
self._tilt_closed_position, self._qos, self._config.get(CONF_TILT_COMMAND_TOPIC),
self._retain) self._config.get(CONF_TILT_CLOSED_POSITION),
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._tilt_optimistic: 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() self.async_schedule_update_ha_state()
async def async_set_cover_tilt_position(self, **kwargs): 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 # The position needs to be between min and max
level = self.find_in_range_from_percent(position) level = self.find_in_range_from_percent(position)
mqtt.async_publish(self.hass, self._tilt_command_topic, mqtt.async_publish(self.hass,
level, self._qos, self._retain) 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): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
set_position_template = self._config.get(CONF_SET_POSITION_TEMPLATE)
if ATTR_POSITION in kwargs: if ATTR_POSITION in kwargs:
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
percentage_position = position percentage_position = position
if self._set_position_template is not None: if set_position_template is not None:
try: try:
position = self._set_position_template.async_render( position = set_position_template.async_render(
**kwargs) **kwargs)
except TemplateError as ex: except TemplateError as ex:
_LOGGER.error(ex) _LOGGER.error(ex)
self._state = None 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 = self.find_in_range_from_percent(
position, COVER_PAYLOAD) position, COVER_PAYLOAD)
mqtt.async_publish(self.hass, self._set_position_topic, mqtt.async_publish(self.hass,
position, self._qos, self._retain) self._config.get(CONF_SET_POSITION_TOPIC),
position,
self._config.get(CONF_QOS),
self._config.get(CONF_RETAIN))
if self._optimistic: 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._position = percentage_position
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
@ -459,11 +453,11 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
"""Find the 0-100% value within the specified range.""" """Find the 0-100% value within the specified range."""
# the range of motion as defined by the min max values # the range of motion as defined by the min max values
if range_type == COVER_PAYLOAD: if range_type == COVER_PAYLOAD:
max_range = self._position_open max_range = self._config.get(CONF_POSITION_OPEN)
min_range = self._position_closed min_range = self._config.get(CONF_POSITION_CLOSED)
else: else:
max_range = self._tilt_max max_range = self._config.get(CONF_TILT_MAX)
min_range = self._tilt_min min_range = self._config.get(CONF_TILT_MIN)
current_range = max_range - min_range current_range = max_range - min_range
# offset to be zero based # offset to be zero based
offset_position = position - min_range offset_position = position - min_range
@ -474,7 +468,8 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
min_percent = 0 min_percent = 0
position_percentage = min(max(position_percentage, min_percent), position_percentage = min(max(position_percentage, min_percent),
max_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 100 - position_percentage
return position_percentage return position_percentage
@ -488,17 +483,18 @@ class MqttCover(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
returning the offset returning the offset
""" """
if range_type == COVER_PAYLOAD: if range_type == COVER_PAYLOAD:
max_range = self._position_open max_range = self._config.get(CONF_POSITION_OPEN)
min_range = self._position_closed min_range = self._config.get(CONF_POSITION_CLOSED)
else: else:
max_range = self._tilt_max max_range = self._config.get(CONF_TILT_MAX)
min_range = self._tilt_min min_range = self._config.get(CONF_TILT_MIN)
offset = min_range offset = min_range
current_range = max_range - min_range current_range = max_range - min_range
position = round(current_range * (percentage / 100.0)) position = round(current_range * (percentage / 100.0))
position += offset 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 position = max_range - position + offset
return position return position

View File

@ -8,8 +8,9 @@ https://home-assistant.io/components/cover.tellduslive/
""" """
import logging import logging
from homeassistant.components import tellduslive
from homeassistant.components.cover import CoverDevice from homeassistant.components.cover import CoverDevice
from homeassistant.components.tellduslive import TelldusLiveEntity from homeassistant.components.tellduslive.entry import TelldusLiveEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,7 +20,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if discovery_info is None: if discovery_info is None:
return 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): class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
@ -33,14 +35,11 @@ class TelldusLiveCover(TelldusLiveEntity, CoverDevice):
def close_cover(self, **kwargs): def close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
self.device.down() self.device.down()
self.changed()
def open_cover(self, **kwargs): def open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
self.device.up() self.device.up()
self.changed()
def stop_cover(self, **kwargs): def stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
self.device.stop() self.device.stop()
self.changed()

View File

@ -132,3 +132,8 @@ class DaikinApi:
_LOGGER.warning( _LOGGER.warning(
"Connection failed for %s", self.ip_address "Connection failed for %s", self.ip_address
) )
@property
def mac(self):
"""Return mac-address of device."""
return self.device.values.get('mac')

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Amfitri\u00f3", "host": "Amfitri\u00f3",
"port": "Port (predeterminat: '80')" "port": "Port"
}, },
"title": "Definiu la passarel\u00b7la deCONZ" "title": "Definiu la passarel\u00b7la deCONZ"
}, },

View File

@ -17,7 +17,7 @@
"title": "deCONZ gateway d\u00e9fin\u00e9ieren" "title": "deCONZ gateway d\u00e9fin\u00e9ieren"
}, },
"link": { "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" "title": "Link mat deCONZ"
}, },
"options": { "options": {

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "\u0425\u043e\u0441\u0442", "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" "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ"
}, },

View File

@ -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.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery 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.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 from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant import util from homeassistant import util
@ -384,7 +383,6 @@ class DeviceTracker:
for device in self.devices.values(): for device in self.devices.values():
if (device.track and device.last_update_home) and \ if (device.track and device.last_update_home) and \
device.stale(now): device.stale(now):
device.mark_stale()
self.hass.async_create_task(device.async_update_ha_state(True)) self.hass.async_create_task(device.async_update_ha_state(True))
async def async_setup_tracked_device(self): async def async_setup_tracked_device(self):
@ -407,7 +405,7 @@ class DeviceTracker:
await asyncio.wait(tasks, loop=self.hass.loop) await asyncio.wait(tasks, loop=self.hass.loop)
class Device(Entity): class Device(RestoreEntity):
"""Represent a tracked device.""" """Represent a tracked device."""
host_name = None # type: str host_name = None # type: str
@ -575,7 +573,8 @@ class Device(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Add an entity.""" """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: if not state:
return return
self._state = state.state self._state = state.state

View File

@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
REQUIREMENTS = ['btsmarthub_devicelist==0.1.1'] REQUIREMENTS = ['btsmarthub_devicelist==0.1.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify, dt as dt_util from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.8'] REQUIREMENTS = ['locationsharinglib==3.0.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, CONF_METHOD) 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__) _LOGGER = logging.getLogger(__name__)
@ -128,7 +128,8 @@ class MikrotikScanner(DeviceScanner):
librouteros.exceptions.ConnectionError): librouteros.exceptions.ConnectionError):
self.wireless_exist = False 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( _LOGGER.info(
"Mikrotik %s: Wireless adapters not found. Try to " "Mikrotik %s: Wireless adapters not found. Try to "
"use DHCP lease table as presence tracker source. " "use DHCP lease table as presence tracker source. "
@ -143,12 +144,18 @@ class MikrotikScanner(DeviceScanner):
librouteros.exceptions.MultiTrapError, librouteros.exceptions.MultiTrapError,
librouteros.exceptions.ConnectionError) as api_error: librouteros.exceptions.ConnectionError) as api_error:
_LOGGER.error("Connection error: %s", api_error) _LOGGER.error("Connection error: %s", api_error)
return self.connected return self.connected
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device MACs.""" """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] return [device for device in self.last_results]
def get_device_name(self, device): def get_device_name(self, device):

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.5'] REQUIREMENTS = ['pysnmp==4.4.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,33 +7,32 @@ https://home-assistant.io/components/device_tracker.volvooncall/
import logging import logging
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect
dispatcher_connect, dispatcher_send) from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_VEHICLE_SEEN from homeassistant.components.volvooncall import DATA_KEY, SIGNAL_STATE_UPDATED
_LOGGER = logging.getLogger(__name__) _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.""" """Set up the Volvo tracker."""
if discovery_info is None: if discovery_info is None:
return return
vin, _ = discovery_info vin, component, attr = discovery_info
voc = hass.data[DATA_KEY] data = hass.data[DATA_KEY]
vehicle = voc.vehicles[vin] instrument = data.instrument(vin, component, attr)
def see_vehicle(vehicle): async def see_vehicle():
"""Handle the reporting of the vehicle position.""" """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)) dev_id = 'volvo_{}'.format(slugify(host_name))
see(dev_id=dev_id, await async_see(dev_id=dev_id,
host_name=host_name, host_name=host_name,
gps=(vehicle.position['latitude'], source_type=SOURCE_TYPE_GPS,
vehicle.position['longitude']), gps=instrument.state,
icon='mdi:car') icon='mdi:car')
dispatcher_connect(hass, SIGNAL_VEHICLE_SEEN, see_vehicle) async_dispatcher_connect(hass, SIGNAL_STATE_UPDATED, see_vehicle)
dispatcher_send(hass, SIGNAL_VEHICLE_SEEN, vehicle)
return True return True

View File

@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import CONF_HOST, CONF_TOKEN from homeassistant.const import CONF_HOST, CONF_TOKEN
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)

View File

@ -5,7 +5,7 @@
"one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia."
}, },
"create_entry": { "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": { "step": {
"user": { "user": {

View File

@ -0,0 +1,10 @@
{
"config": {
"step": {
"user": {
"title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa"
}
},
"title": "Dialogflow"
}
}

View File

@ -5,7 +5,7 @@
"one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
}, },
"create_entry": { "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": { "step": {
"user": { "user": {

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "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?", "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" "title": "Dialogflow"

View File

@ -134,6 +134,7 @@ async def async_setup(hass, config):
discovery_hash = json.dumps([service, info], sort_keys=True) discovery_hash = json.dumps([service, info], sort_keys=True)
if discovery_hash in already_discovered: if discovery_hash in already_discovered:
logger.debug("Already discoverd service %s %s.", service, info)
return return
already_discovered.add(discovery_hash) already_discovered.add(discovery_hash)

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType # noqa
DOMAIN = "elkm1" DOMAIN = "elkm1"
REQUIREMENTS = ['elkm1-lib==0.7.12'] REQUIREMENTS = ['elkm1-lib==0.7.13']
CONF_AREA = 'area' CONF_AREA = 'area'
CONF_COUNTER = 'counter' CONF_COUNTER = 'counter'

View File

@ -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 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) and, optionally, a DHW controller.
@ -8,46 +8,48 @@ https://home-assistant.io/components/evohome/
""" """
# Glossary: # Glossary:
# TCS - temperature control system (a.k.a. Controller, Parent), which can # TCS - temperature control system (a.k.a. Controller, Parent), which can
# have up to 13 Children: # have up to 13 Children:
# 0-12 Heating zones (a.k.a. Zone), and # 0-12 Heating zones (a.k.a. Zone), and
# 0-1 DHW controller, (a.k.a. Boiler) # 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 import logging
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_USERNAME, CONF_SCAN_INTERVAL, CONF_USERNAME, CONF_PASSWORD,
CONF_PASSWORD, EVENT_HOMEASSISTANT_START,
CONF_SCAN_INTERVAL, HTTP_BAD_REQUEST, HTTP_SERVICE_UNAVAILABLE, HTTP_TOO_MANY_REQUESTS
HTTP_BAD_REQUEST
) )
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
REQUIREMENTS = ['evohomeclient==0.2.7'] REQUIREMENTS = ['evohomeclient==0.2.8']
# If ever > 0.2.7, re-check the work-around wrapper is still required when
# instantiating the client, below.
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'evohome' DOMAIN = 'evohome'
DATA_EVOHOME = 'data_' + DOMAIN DATA_EVOHOME = 'data_' + DOMAIN
DISPATCHER_EVOHOME = 'dispatcher_' + DOMAIN
CONF_LOCATION_IDX = 'location_idx' CONF_LOCATION_IDX = 'location_idx'
MAX_TEMP = 28 SCAN_INTERVAL_DEFAULT = timedelta(seconds=300)
MIN_TEMP = 5 SCAN_INTERVAL_MINIMUM = timedelta(seconds=180)
SCAN_INTERVAL_DEFAULT = 180
SCAN_INTERVAL_MAX = 300
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): 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) }, extra=vol.ALLOW_EXTRA)
@ -55,91 +57,107 @@ CONFIG_SCHEMA = vol.Schema({
GWS = 'gateways' GWS = 'gateways'
TCS = 'temperatureControlSystems' 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 def setup(hass, hass_config):
DHW controller. Does not work for US-based systems. """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 = hass.data[DATA_EVOHOME] = {}
evo_data['timers'] = {} evo_data['timers'] = {}
evo_data['params'] = dict(config[DOMAIN]) # use a copy, since scan_interval is rounded up to nearest 60s
evo_data['params'][CONF_SCAN_INTERVAL] = SCAN_INTERVAL_DEFAULT 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 from evohomeclient2 import EvohomeClient
_LOGGER.debug("setup(): API call [4 request(s)]: client.__init__()...")
try: 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( client = EvohomeClient(
evo_data['params'][CONF_USERNAME], evo_data['params'][CONF_USERNAME],
evo_data['params'][CONF_PASSWORD], evo_data['params'][CONF_PASSWORD],
debug=False debug=False
) )
# ...then restore it to what it was before instantiating the client
logging.getLogger().setLevel(log_level)
except HTTPError as err: except HTTPError as err:
if err.response.status_code == HTTP_BAD_REQUEST: if err.response.status_code == HTTP_BAD_REQUEST:
_LOGGER.error( _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." "Check your username (%s), and password are correct."
"Unable to continue. Resolve any errors and restart HA.", "Unable to continue. Resolve any errors and restart HA.",
evo_data['params'][CONF_USERNAME] 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_USERNAME] = 'REDACTED'
evo_data['params'][CONF_PASSWORD] = 'REDACTED' evo_data['params'][CONF_PASSWORD] = 'REDACTED'
evo_data['client'] = client evo_data['client'] = client
evo_data['status'] = {}
# Redact any installation data we'll never need. # Redact any installation data we'll never need
if client.installation_info[0]['locationInfo']['locationId'] != 'REDACTED': for loc in client.installation_info:
for loc in client.installation_info: loc['locationInfo']['locationId'] = 'REDACTED'
loc['locationInfo']['streetAddress'] = 'REDACTED' loc['locationInfo']['locationOwner'] = 'REDACTED'
loc['locationInfo']['city'] = 'REDACTED' loc['locationInfo']['streetAddress'] = 'REDACTED'
loc['locationInfo']['locationOwner'] = 'REDACTED' loc['locationInfo']['city'] = 'REDACTED'
loc[GWS][0]['gatewayInfo'] = 'REDACTED' loc[GWS][0]['gatewayInfo'] = 'REDACTED'
# Pull down the installation configuration. # Pull down the installation configuration
loc_idx = evo_data['params'][CONF_LOCATION_IDX] loc_idx = evo_data['params'][CONF_LOCATION_IDX]
try: try:
evo_data['config'] = client.installation_info[loc_idx] evo_data['config'] = client.installation_info[loc_idx]
except IndexError: except IndexError:
_LOGGER.warning( _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, CONF_LOCATION_IDX,
loc_idx, loc_idx,
len(client.installation_info) - 1 len(client.installation_info) - 1
) )
return False # unable to continue return False # unable to continue
evo_data['status'] = {}
if _LOGGER.isEnabledFor(logging.DEBUG): if _LOGGER.isEnabledFor(logging.DEBUG):
tmp_loc = dict(evo_data['config']) tmp_loc = dict(evo_data['config'])
tmp_loc['locationInfo']['postcode'] = 'REDACTED' tmp_loc['locationInfo']['postcode'] = 'REDACTED'
tmp_tcs = tmp_loc[GWS][0][TCS][0] if 'dhw' in tmp_loc[GWS][0][TCS][0]: # if this location has DHW...
if 'zones' in tmp_tcs: tmp_loc[GWS][0][TCS][0]['dhw'] = '...'
tmp_tcs['zones'] = '...'
if 'dhw' in tmp_tcs:
tmp_tcs['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 return True

View File

@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/fan.mqtt/ https://home-assistant.io/components/fan.mqtt/
""" """
import logging import logging
from typing import Optional
import voluptuous as vol import voluptuous as vol
@ -18,7 +17,7 @@ from homeassistant.components.mqtt import (
ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC,
CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate, CONF_QOS, CONF_RETAIN, MqttAvailability, MqttDiscoveryUpdate,
MqttEntityDeviceInfo) MqttEntityDeviceInfo, subscription)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
@ -107,40 +106,7 @@ async def _async_setup_entity(hass, config, async_add_entities,
discovery_hash=None): discovery_hash=None):
"""Set up the MQTT fan.""" """Set up the MQTT fan."""
async_add_entities([MqttFan( async_add_entities([MqttFan(
config.get(CONF_NAME), config,
{
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),
discovery_hash, discovery_hash,
)]) )])
@ -149,43 +115,95 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
FanEntity): FanEntity):
"""A MQTT fan component.""" """A MQTT fan component."""
def __init__(self, name, topic, templates, qos, retain, payload, def __init__(self, config, discovery_hash):
speed_list, optimistic, availability_topic, payload_available,
payload_not_available, unique_id: Optional[str],
device_config: Optional[ConfigType], discovery_hash):
"""Initialize the MQTT fan.""" """Initialize the MQTT fan."""
MqttAvailability.__init__(self, availability_topic, qos, self._unique_id = config.get(CONF_UNIQUE_ID)
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._state = False self._state = False
self._speed = None self._speed = None
self._oscillation = None self._oscillation = None
self._supported_features = 0 self._supported_features = 0
self._supported_features |= (topic[CONF_OSCILLATION_STATE_TOPIC] self._sub_state = None
is not None and SUPPORT_OSCILLATE)
self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] self._topic = None
is not None and SUPPORT_SET_SPEED) self._payload = None
self._unique_id = unique_id self._templates = None
self._discovery_hash = discovery_hash 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): async def async_added_to_hass(self):
"""Subscribe to MQTT events.""" """Subscribe to MQTT events."""
await MqttAvailability.async_added_to_hass(self) await super().async_added_to_hass()
await MqttDiscoveryUpdate.async_added_to_hass(self) 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 = {} templates = {}
for key, tpl in list(self._templates.items()): for key, tpl in list(self._templates.items()):
if tpl is None: if tpl is None:
@ -205,9 +223,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_STATE_TOPIC] is not None: if self._topic[CONF_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_STATE_TOPIC] = {
self.hass, self._topic[CONF_STATE_TOPIC], state_received, 'topic': self._topic[CONF_STATE_TOPIC],
self._qos) 'msg_callback': state_received,
'qos': self._config.get(CONF_QOS)}
@callback @callback
def speed_received(topic, payload, qos): def speed_received(topic, payload, qos):
@ -222,9 +241,10 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_SPEED_STATE_TOPIC] is not None: if self._topic[CONF_SPEED_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_SPEED_STATE_TOPIC] = {
self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received, 'topic': self._topic[CONF_SPEED_STATE_TOPIC],
self._qos) 'msg_callback': speed_received,
'qos': self._config.get(CONF_QOS)}
self._speed = SPEED_OFF self._speed = SPEED_OFF
@callback @callback
@ -238,11 +258,21 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None:
await mqtt.async_subscribe( topics[CONF_OSCILLATION_STATE_TOPIC] = {
self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC], 'topic': self._topic[CONF_OSCILLATION_STATE_TOPIC],
oscillation_received, self._qos) 'msg_callback': oscillation_received,
'qos': self._config.get(CONF_QOS)}
self._oscillation = False 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 @property
def should_poll(self): def should_poll(self):
"""No polling needed for a MQTT fan.""" """No polling needed for a MQTT fan."""
@ -261,12 +291,12 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
@property @property
def name(self) -> str: def name(self) -> str:
"""Get entity name.""" """Get entity name."""
return self._name return self._config.get(CONF_NAME)
@property @property
def speed_list(self) -> list: def speed_list(self) -> list:
"""Get the list of available speeds.""" """Get the list of available speeds."""
return self._speed_list return self._config.get(CONF_SPEED_LIST)
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
@ -290,7 +320,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
""" """
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC], 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: if speed:
await self.async_set_speed(speed) await self.async_set_speed(speed)
@ -301,7 +332,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
""" """
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_COMMAND_TOPIC], 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: async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan. """Set the speed of the fan.
@ -322,7 +354,8 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_SPEED_COMMAND_TOPIC], 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: if self._optimistic_speed:
self._speed = speed self._speed = speed
@ -343,7 +376,7 @@ class MqttFan(MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo,
mqtt.async_publish( mqtt.async_publish(
self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], 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: if self._optimistic_oscillation:
self._oscillation = oscillating self._oscillation = oscillating

View File

@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv 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__) _LOGGER = logging.getLogger(__name__)
@ -755,12 +755,13 @@ class XiaomiAirHumidifier(XiaomiGenericDevice):
if self._model == MODEL_AIRHUMIDIFIER_CA: if self._model == MODEL_AIRHUMIDIFIER_CA:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA
self._available_attributes = AVAILABLE_ATTRIBUTES_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: else:
self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER
self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER
self._speed_list = [mode.name for mode in OperationMode if self._speed_list = [mode.name for mode in OperationMode if
mode.name != 'Auto'] mode is not OperationMode.Auto]
self._state_attrs.update( self._state_attrs.update(
{attribute: None for attribute in self._available_attributes}) {attribute: None for attribute in self._available_attributes})

View File

@ -5,10 +5,15 @@ For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/ at https://home-assistant.io/components/fan.zha/
""" """
import logging import logging
from homeassistant.components import zha
from homeassistant.components.fan import ( from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_SET_SPEED,
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'] 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, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
"""Set up the Zigbee Home Automation fans.""" """Old way of setting up Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info) pass
if discovery_info is None:
return
async_add_entities([ZhaFan(**discovery_info)], update_before_add=True)
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.""" """Representation of a ZHA fan."""
_domain = DOMAIN _domain = DOMAIN
@ -101,9 +129,9 @@ class ZhaFan(zha.Entity, FanEntity):
async def async_update(self): async def async_update(self):
"""Retrieve latest state.""" """Retrieve latest state."""
result = await zha.safe_read(self._endpoint.fan, ['fan_mode'], result = await helpers.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False, allow_cache=False,
only_cache=(not self._initialized)) only_cache=(not self._initialized))
new_value = result.get('fan_mode', None) new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None) self._state = VALUE_TO_SPEED.get(new_value, None)

View File

@ -7,6 +7,7 @@ https://home-assistant.io/components/fibaro/
import logging import logging
from collections import defaultdict from collections import defaultdict
from typing import Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (ATTR_ARMED, ATTR_BATTERY_LEVEL, 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" ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
CONF_PLUGINS = "plugins" CONF_PLUGINS = "plugins"
FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light', 'sensor', 'switch'] FIBARO_COMPONENTS = ['binary_sensor', 'cover', 'light',
'scene', 'sensor', 'switch']
FIBARO_TYPEMAP = { FIBARO_TYPEMAP = {
'com.fibaro.multilevelSensor': "sensor", 'com.fibaro.multilevelSensor': "sensor",
@ -43,7 +45,8 @@ FIBARO_TYPEMAP = {
'com.fibaro.smokeSensor': 'binary_sensor', 'com.fibaro.smokeSensor': 'binary_sensor',
'com.fibaro.remoteSwitch': 'switch', 'com.fibaro.remoteSwitch': 'switch',
'com.fibaro.sensor': 'sensor', 'com.fibaro.sensor': 'sensor',
'com.fibaro.colorController': 'light' 'com.fibaro.colorController': 'light',
'com.fibaro.securitySensor': 'binary_sensor'
} }
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
@ -63,19 +66,23 @@ class FibaroController():
_device_map = None # Dict for mapping deviceId to device object _device_map = None # Dict for mapping deviceId to device object
fibaro_devices = None # List of devices by type fibaro_devices = None # List of devices by type
_callbacks = {} # Dict of update value callbacks by deviceId _callbacks = {} # Dict of update value callbacks by deviceId
_client = None # Fiblary's Client object for communication _client = None # Fiblary's Client object for communication
_state_handler = None # Fiblary's StateHandler object _state_handler = None # Fiblary's StateHandler object
_import_plugins = None # Whether to import devices from plugins _import_plugins = None # Whether to import devices from plugins
def __init__(self, username, password, url, import_plugins): def __init__(self, username, password, url, import_plugins):
"""Initialize the Fibaro controller.""" """Initialize the Fibaro controller."""
from fiblary3.client.v4.client import Client as FibaroClient from fiblary3.client.v4.client import Client as FibaroClient
self._client = FibaroClient(url, username, password) self._client = FibaroClient(url, username, password)
self._scene_map = None
self.hub_serial = None # Unique serial number of the hub
def connect(self): def connect(self):
"""Start the communication with the Fibaro controller.""" """Start the communication with the Fibaro controller."""
try: try:
login = self._client.login.get() login = self._client.login.get()
info = self._client.info.get()
self.hub_serial = slugify(info.serialNumber)
except AssertionError: except AssertionError:
_LOGGER.error("Can't connect to Fibaro HC. " _LOGGER.error("Can't connect to Fibaro HC. "
"Please check URL.") "Please check URL.")
@ -87,6 +94,7 @@ class FibaroController():
self._room_map = {room.id: room for room in self._client.rooms.list()} self._room_map = {room.id: room for room in self._client.rooms.list()}
self._read_devices() self._read_devices()
self._read_scenes()
return True return True
def enable_state_handler(self): def enable_state_handler(self):
@ -166,6 +174,25 @@ class FibaroController():
device_type = 'light' device_type = 'light'
return device_type 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): def _read_devices(self):
"""Read and process the device list.""" """Read and process the device list."""
devices = self._client.devices.list() devices = self._client.devices.list()
@ -177,6 +204,7 @@ class FibaroController():
room_name = 'Unknown' room_name = 'Unknown'
else: else:
room_name = self._room_map[device.roomID].name room_name = self._room_map[device.roomID].name
device.room_name = room_name
device.friendly_name = room_name + ' ' + device.name device.friendly_name = room_name + ' ' + device.name
device.ha_id = '{}_{}_{}'.format( device.ha_id = '{}_{}_{}'.format(
slugify(room_name), slugify(device.name), device.id) slugify(room_name), slugify(device.name), device.id)
@ -187,6 +215,8 @@ class FibaroController():
else: else:
device.mapped_type = None device.mapped_type = None
if device.mapped_type: if device.mapped_type:
device.unique_id_str = "{}.{}".format(
self.hub_serial, device.id)
self._device_map[device.id] = device self._device_map[device.id] = device
self.fibaro_devices[device.mapped_type].append(device) self.fibaro_devices[device.mapped_type].append(device)
else: else:
@ -283,11 +313,14 @@ class FibaroDevice(Entity):
def call_set_color(self, red, green, blue, white): def call_set_color(self, red, green, blue, white):
"""Set the color of Fibaro device.""" """Set the color of Fibaro device."""
color_str = "{},{},{},{}".format(int(red), int(green), red = int(max(0, min(255, red)))
int(blue), int(white)) 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.fibaro_device.properties.color = color_str
self.action("setColor", str(int(red)), str(int(green)), self.action("setColor", str(red), str(green),
str(int(blue)), str(int(white))) str(blue), str(white))
def action(self, cmd, *args): def action(self, cmd, *args):
"""Perform an action on the Fibaro HC.""" """Perform an action on the Fibaro HC."""
@ -324,7 +357,12 @@ class FibaroDevice(Entity):
return False return False
@property @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 the name of the device."""
return self._name return self._name
@ -357,5 +395,5 @@ class FibaroDevice(Entity):
except (ValueError, KeyError): except (ValueError, KeyError):
pass pass
attr['id'] = self.ha_id attr['fibaro_id'] = self.fibaro_device.id
return attr return attr

View File

@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20181121.1'] REQUIREMENTS = ['home-assistant-frontend==20181211.0']
DOMAIN = 'frontend' DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log', DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',
@ -238,7 +238,7 @@ async def async_setup(hass, config):
if os.path.isdir(local): if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev) 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(index_view)
hass.http.register_view(AuthorizeView(repo_path, js_version)) hass.http.register_view(AuthorizeView(repo_path, js_version))
@ -250,7 +250,7 @@ async def async_setup(hass, config):
await asyncio.wait( await asyncio.wait(
[async_register_built_in_panel(hass, panel) for panel in ( [async_register_built_in_panel(hass, panel) for panel in (
'dev-event', 'dev-info', 'dev-service', 'dev-state', '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) loop=hass.loop)
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
@ -362,13 +362,11 @@ class IndexView(HomeAssistantView):
url = '/' url = '/'
name = 'frontend:index' name = 'frontend:index'
requires_auth = False 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.""" """Initialize the frontend view."""
self.repo_path = repo_path self.repo_path = repo_path
self.js_option = js_option self.js_option = js_option
self.auth_active = auth_active
self._template_cache = {} self._template_cache = {}
def get_template(self, latest): def get_template(self, latest):
@ -415,8 +413,6 @@ class IndexView(HomeAssistantView):
# do not try to auto connect on load # do not try to auto connect on load
no_auth = '0' no_auth = '0'
use_oauth = '1' if self.auth_active else '0'
template = await hass.async_add_job(self.get_template, latest) template = await hass.async_add_job(self.get_template, latest)
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 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, no_auth=no_auth,
theme_color=MANIFEST_JSON['theme_color'], theme_color=MANIFEST_JSON['theme_color'],
extra_urls=hass.data[extra_key], extra_urls=hass.data[extra_key],
use_oauth=use_oauth use_oauth='1'
) )
return web.Response(text=template.render(**template_params), return web.Response(text=template.render(**template_params),

View File

@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.components.geo_location import ( from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent) PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import ( 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 from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -38,6 +39,8 @@ SOURCE = 'geo_json_events'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string, 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), 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.""" """Set up the GeoJSON Events platform."""
url = config[CONF_URL] url = config[CONF_URL]
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) 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] radius_in_km = config[CONF_RADIUS]
# Initialize the entity manager. # Initialize the entity manager.
feed = GeoJsonFeedManager(hass, add_entities, scan_interval, url, feed = GeoJsonFeedEntityManager(
radius_in_km) hass, add_entities, scan_interval, coordinates, url, radius_in_km)
def start_feed_manager(event): def start_feed_manager(event):
"""Start feed manager.""" """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) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class GeoJsonFeedManager: class GeoJsonFeedEntityManager:
"""Feed Manager for GeoJSON feeds.""" """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.""" """Initialize the GeoJSON Feed Manager."""
from geojson_client.generic_feed import GenericFeed from geojson_client.generic_feed import GenericFeedManager
self._hass = hass self._hass = hass
self._feed = GenericFeed( self._feed_manager = GenericFeedManager(
(hass.config.latitude, hass.config.longitude), self._generate_entity, self._update_entity, self._remove_entity,
filter_radius=radius_in_km, url=url) coordinates, url, filter_radius=radius_in_km)
self._add_entities = add_entities self._add_entities = add_entities
self._scan_interval = scan_interval self._scan_interval = scan_interval
self.feed_entries = {}
self._managed_external_ids = set()
def startup(self): def startup(self):
"""Start up this manager.""" """Start up this manager."""
self._update() self._feed_manager.update()
self._init_regular_updates() self._init_regular_updates()
def _init_regular_updates(self): def _init_regular_updates(self):
"""Schedule regular updates at the specified interval.""" """Schedule regular updates at the specified interval."""
track_time_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): def get_entry(self, external_id):
"""Update the feed and then update connected entities.""" """Get feed entry by external id."""
import geojson_client return self._feed_manager.feed_entries.get(external_id)
status, feed_entries = self._feed.update() def _generate_entity(self, external_id):
if status == geojson_client.UPDATE_OK: """Generate new entity."""
_LOGGER.debug("Data retrieved %s", feed_entries) new_entity = GeoJsonLocationEvent(self, external_id)
# 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)
# Add new entities to HA. # Add new entities to HA.
self._add_entities(new_entities, True) self._add_entities([new_entity], True)
def _update_entities(self, external_ids): def _update_entity(self, external_id):
"""Update entities.""" """Update entity."""
for external_id in external_ids: dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
_LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids): def _remove_entity(self, external_id):
"""Remove entities.""" """Remove entity."""
for external_id in external_ids: dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
_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))
class GeoJsonLocationEvent(GeoLocationEvent): class GeoJsonLocationEvent(GeoLocationEvent):
@ -184,7 +151,7 @@ class GeoJsonLocationEvent(GeoLocationEvent):
async def async_update(self): async def async_update(self):
"""Update this entity from the data held in the feed manager.""" """Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id) _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: if feed_entry:
self._update_from_feed(feed_entry) self._update_from_feed(feed_entry)

View File

@ -14,7 +14,7 @@ from homeassistant.components.geo_location import (
PLATFORM_SCHEMA, GeoLocationEvent) PLATFORM_SCHEMA, GeoLocationEvent)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL, ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_RADIUS, CONF_SCAN_INTERVAL,
EVENT_HOMEASSISTANT_START) EVENT_HOMEASSISTANT_START, CONF_LATITUDE, CONF_LONGITUDE)
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -57,18 +57,23 @@ VALID_CATEGORIES = [
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CATEGORIES, default=[]): vol.Optional(CONF_CATEGORIES, default=[]):
vol.All(cv.ensure_list, [vol.In(VALID_CATEGORIES)]), 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), vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM): vol.Coerce(float),
}) })
def setup_platform(hass, config, add_entities, discovery_info=None): 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) 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] radius_in_km = config[CONF_RADIUS]
categories = config.get(CONF_CATEGORIES) categories = config.get(CONF_CATEGORIES)
# Initialize the entity manager. # Initialize the entity manager.
feed = NswRuralFireServiceFeedManager( feed = NswRuralFireServiceFeedEntityManager(
hass, add_entities, scan_interval, radius_in_km, categories) hass, add_entities, scan_interval, coordinates, radius_in_km,
categories)
def start_feed_manager(event): def start_feed_manager(event):
"""Start feed manager.""" """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) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager)
class NswRuralFireServiceFeedManager: class NswRuralFireServiceFeedEntityManager:
"""Feed Manager for NSW Rural Fire Service GeoJSON feed.""" """Feed Entity Manager for NSW Rural Fire Service GeoJSON feed."""
def __init__(self, hass, add_entities, scan_interval, radius_in_km, def __init__(self, hass, add_entities, scan_interval, coordinates,
categories): radius_in_km, categories):
"""Initialize the GeoJSON Feed Manager.""" """Initialize the Feed Entity Manager."""
from geojson_client.nsw_rural_fire_service_feed \ from geojson_client.nsw_rural_fire_service_feed \
import NswRuralFireServiceFeed import NswRuralFireServiceFeedManager
self._hass = hass self._hass = hass
self._feed = NswRuralFireServiceFeed( self._feed_manager = NswRuralFireServiceFeedManager(
(hass.config.latitude, hass.config.longitude), self._generate_entity, self._update_entity, self._remove_entity,
filter_radius=radius_in_km, filter_categories=categories) coordinates, filter_radius=radius_in_km,
filter_categories=categories)
self._add_entities = add_entities self._add_entities = add_entities
self._scan_interval = scan_interval self._scan_interval = scan_interval
self.feed_entries = {}
self._managed_external_ids = set()
def startup(self): def startup(self):
"""Start up this manager.""" """Start up this manager."""
self._update() self._feed_manager.update()
self._init_regular_updates() self._init_regular_updates()
def _init_regular_updates(self): def _init_regular_updates(self):
"""Schedule regular updates at the specified interval.""" """Schedule regular updates at the specified interval."""
track_time_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): def get_entry(self, external_id):
"""Update the feed and then update connected entities.""" """Get feed entry by external id."""
import geojson_client return self._feed_manager.feed_entries.get(external_id)
status, feed_entries = self._feed.update() def _generate_entity(self, external_id):
if status == geojson_client.UPDATE_OK: """Generate new entity."""
_LOGGER.debug("Data retrieved %s", feed_entries) new_entity = NswRuralFireServiceLocationEvent(self, external_id)
# 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)
# Add new entities to HA. # Add new entities to HA.
self._add_entities(new_entities, True) self._add_entities([new_entity], True)
def _update_entities(self, external_ids): def _update_entity(self, external_id):
"""Update entities.""" """Update entity."""
for external_id in external_ids: dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
_LOGGER.debug("Existing entity found %s", external_id)
dispatcher_send(
self._hass, SIGNAL_UPDATE_ENTITY.format(external_id))
def _remove_entities(self, external_ids): def _remove_entity(self, external_id):
"""Remove entities.""" """Remove entity."""
for external_id in external_ids: dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id))
_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))
class NswRuralFireServiceLocationEvent(GeoLocationEvent): 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): def __init__(self, feed_manager, external_id):
"""Initialize entity with data from feed entry.""" """Initialize entity with data from feed entry."""
@ -209,13 +176,13 @@ class NswRuralFireServiceLocationEvent(GeoLocationEvent):
@property @property
def should_poll(self): def should_poll(self):
"""No polling needed for GeoJSON location events.""" """No polling needed for NSW Rural Fire Service location events."""
return False return False
async def async_update(self): async def async_update(self):
"""Update this entity from the data held in the feed manager.""" """Update this entity from the data held in the feed manager."""
_LOGGER.debug("Updating %s", self._external_id) _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: if feed_entry:
self._update_from_feed(feed_entry) self._update_from_feed(feed_entry)

View File

@ -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

View File

@ -12,8 +12,9 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME 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' DOMAIN = 'goalfeed'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({

View File

@ -33,8 +33,6 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
DEFAULT_AGENT_USER_ID = 'home-assistant'
ENTITY_SCHEMA = vol.Schema({ ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_EXPOSE): cv.boolean, 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) websession = async_get_clientsession(hass)
try: try:
with async_timeout.timeout(5, loop=hass.loop): 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( res = await websession.post(
REQUEST_SYNC_BASE_URL, REQUEST_SYNC_BASE_URL,
params={'key': api_key}, 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") _LOGGER.info("Submitted request_sync request to Google")
res.raise_for_status() res.raise_for_status()
except aiohttp.ClientResponseError: except aiohttp.ClientResponseError:

View File

@ -19,8 +19,6 @@ DEFAULT_EXPOSED_DOMAINS = [
'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock',
] ]
DEFAULT_ALLOW_UNLOCK = False DEFAULT_ALLOW_UNLOCK = False
CLIMATE_MODE_HEATCOOL = 'heatcool'
CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL}
PREFIX_TYPES = 'action.devices.types.' PREFIX_TYPES = 'action.devices.types.'
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT' TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'

View File

@ -48,7 +48,7 @@ def async_register_http(hass, cfg):
entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE) entity_config.get(entity.entity_id, {}).get(CONF_EXPOSE)
domain_exposed_by_default = \ 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 # Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being # the configuration doesn't explicitly exclude it from being

View File

@ -1,2 +1,5 @@
request_sync: 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.

View File

@ -43,6 +43,7 @@ TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting' TRAIT_TEMPERATURE_SETTING = PREFIX_TRAITS + 'TemperatureSetting'
TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock'
TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed'
TRAIT_MODES = PREFIX_TRAITS + 'Modes'
PREFIX_COMMANDS = 'action.devices.commands.' PREFIX_COMMANDS = 'action.devices.commands.'
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
@ -59,7 +60,7 @@ COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE = (
COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode' COMMAND_THERMOSTAT_SET_MODE = PREFIX_COMMANDS + 'ThermostatSetMode'
COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock'
COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed'
COMMAND_MODES = PREFIX_COMMANDS + 'SetModes'
TRAITS = [] TRAITS = []
@ -197,6 +198,8 @@ class OnOffTrait(_Trait):
@staticmethod @staticmethod
def supported(domain, features): def supported(domain, features):
"""Test if state is supported.""" """Test if state is supported."""
if domain == climate.DOMAIN:
return features & climate.SUPPORT_ON_OFF != 0
return domain in ( return domain in (
group.DOMAIN, group.DOMAIN,
input_boolean.DOMAIN, input_boolean.DOMAIN,
@ -515,6 +518,9 @@ class TemperatureSettingTrait(_Trait):
climate.STATE_COOL: 'cool', climate.STATE_COOL: 'cool',
climate.STATE_OFF: 'off', climate.STATE_OFF: 'off',
climate.STATE_AUTO: 'heatcool', 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()} 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] max_temp = self.state.attributes[climate.ATTR_MAX_TEMP]
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
temp = temp_util.convert(params['thermostatTemperatureSetpoint'], temp = temp_util.convert(
TEMP_CELSIUS, unit) params['thermostatTemperatureSetpoint'], TEMP_CELSIUS,
unit)
if unit == TEMP_FAHRENHEIT:
temp = round(temp)
if temp < min_temp or temp > max_temp: if temp < min_temp or temp > max_temp:
raise SmartHomeError( raise SmartHomeError(
@ -604,6 +613,8 @@ class TemperatureSettingTrait(_Trait):
temp_high = temp_util.convert( temp_high = temp_util.convert(
params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS, params['thermostatTemperatureSetpointHigh'], TEMP_CELSIUS,
unit) unit)
if unit == TEMP_FAHRENHEIT:
temp_high = round(temp_high)
if temp_high < min_temp or temp_high > max_temp: if temp_high < min_temp or temp_high > max_temp:
raise SmartHomeError( raise SmartHomeError(
@ -612,7 +623,10 @@ class TemperatureSettingTrait(_Trait):
"{} and {}".format(min_temp, max_temp)) "{} and {}".format(min_temp, max_temp))
temp_low = temp_util.convert( 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: if temp_low < min_temp or temp_low > max_temp:
raise SmartHomeError( raise SmartHomeError(
@ -752,3 +766,188 @@ class FanSpeedTrait(_Trait):
ATTR_ENTITY_ID: self.state.entity_id, ATTR_ENTITY_ID: self.state.entity_id,
fan.ATTR_SPEED: params['fanSpeed'] fan.ATTR_SPEED: params['fanSpeed']
}, blocking=True) }, 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)

View File

@ -213,13 +213,7 @@ async def async_setup(hass, config):
embed_iframe=True, embed_iframe=True,
) )
# Temporary. No refresh token tells supervisor to use API password. await hassio.update_hass_api(config.get('http', {}), refresh_token.token)
if hass.auth.active:
token = refresh_token.token
else:
token = None
await hassio.update_hass_api(config.get('http', {}), token)
if 'homeassistant' in config: if 'homeassistant' in config:
await hassio.update_hass_timezone(config['homeassistant']) await hassio.update_hass_timezone(config['homeassistant'])

View File

@ -15,7 +15,6 @@ from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE from aiohttp.hdrs import CONTENT_TYPE
from aiohttp.web_exceptions import HTTPBadGateway from aiohttp.web_exceptions import HTTPBadGateway
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
from .const import X_HASSIO from .const import X_HASSIO
@ -63,8 +62,6 @@ class HassIOView(HomeAssistantView):
client = await self._command_proxy(path, request) client = await self._command_proxy(path, request)
data = await client.read() data = await client.read()
if path.endswith('/logs'):
return _create_response_log(client, data)
return _create_response(client, data) return _create_response(client, data)
get = _handle 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): def _get_timeout(path):
"""Return timeout for a URL path.""" """Return timeout for a URL path."""
if NO_TIMEOUT.match(path): if NO_TIMEOUT.match(path):

View File

@ -320,38 +320,39 @@ def setup(hass: HomeAssistant, base_config):
class CecDevice(Entity): class CecDevice(Entity):
"""Representation of a HDMI CEC device 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.""" """Initialize the device."""
self._device = device self._device = device
self.hass = hass
self._icon = None self._icon = None
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
self._logical_address = logical self._logical_address = logical
self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) self.entity_id = "%s.%d" % (DOMAIN, self._logical_address)
device.set_update_callback(self._update)
def update(self): def update(self):
"""Update device status.""" """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): def _update(self, device=None):
"""Update device status.""" """Device status changed, schedule an update."""
if device: self.schedule_update_ha_state(True)
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()
@property @property
def name(self): def name(self):

View File

@ -38,20 +38,6 @@ SIGNIFICANT_DOMAINS = ('thermostat', 'climate')
IGNORE_DOMAINS = ('zone', 'scene',) 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, def get_significant_states(hass, start_time, end_time=None, entity_ids=None,
filters=None, include_start_time_state=True): filters=None, include_start_time_state=True):
""" """

View File

@ -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)

View File

@ -13,12 +13,13 @@ import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, 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 from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyhomematic==0.1.52'] REQUIREMENTS = ['pyhomematic==0.1.53']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -77,7 +78,8 @@ HM_DEVICE_TYPES = {
'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat',
'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor', 'IPWeatherSensor', 'RotaryHandleSensorIP', 'IPPassageSensor',
'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus', 'IPKeySwitchPowermeter', 'IPThermostatWall230V', 'IPWeatherSensorPlus',
'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage'], 'IPWeatherSensorBasic', 'IPBrightnessSensor', 'IPGarage',
'UniversalSensor'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
@ -173,6 +175,9 @@ DEFAULT_PORT = 2001
DEFAULT_PATH = '' DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin' DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = '' DEFAULT_PASSWORD = ''
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = False
DEFAULT_CHANNEL = 1
DEVICE_SCHEMA = vol.Schema({ DEVICE_SCHEMA = vol.Schema({
@ -180,7 +185,7 @@ DEVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_ADDRESS): cv.string,
vol.Required(ATTR_INTERFACE): 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_PARAM): cv.string,
vol.Optional(ATTR_UNIQUE_ID): 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_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_CALLBACK_IP): cv.string, vol.Optional(CONF_CALLBACK_IP): cv.string,
vol.Optional(CONF_CALLBACK_PORT): cv.port, 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.Optional(CONF_HOSTS, default={}): {cv.match_all: {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -268,6 +276,8 @@ def setup(hass, config):
'password': rconfig.get(CONF_PASSWORD), 'password': rconfig.get(CONF_PASSWORD),
'callbackip': rconfig.get(CONF_CALLBACK_IP), 'callbackip': rconfig.get(CONF_CALLBACK_IP),
'callbackport': rconfig.get(CONF_CALLBACK_PORT), 'callbackport': rconfig.get(CONF_CALLBACK_PORT),
'ssl': rconfig.get(CONF_SSL),
'verify_ssl': rconfig.get(CONF_VERIFY_SSL),
'connect': True, 'connect': True,
} }

View File

@ -21,7 +21,7 @@
"title": "Trieu el punt d'acc\u00e9s HomematicIP" "title": "Trieu el punt d'acc\u00e9s HomematicIP"
}, },
"link": { "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" "title": "Enlla\u00e7ar punt d'acc\u00e9s"
} }
}, },

View File

@ -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)", "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)" "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": { "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)", "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