mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
commit
88cda043ac
13
.coveragerc
13
.coveragerc
@ -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
|
||||||
|
19
CODEOWNERS
19
CODEOWNERS
@ -51,6 +51,7 @@ homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
|||||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
homeassistant/components/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
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
@ -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']:
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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]:
|
||||||
|
17
homeassistant/auth/permissions/models.py
Normal file
17
homeassistant/auth/permissions/models.py
Normal 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')
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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."""
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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."""
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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'],
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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'])
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
|
@ -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."""
|
||||||
|
@ -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 + "]"
|
|
||||||
)
|
|
||||||
|
@ -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:
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
42
homeassistant/components/cloud/cloud_api.py
Normal file
42
homeassistant/components/cloud/cloud_api.py
Normal 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
|
||||||
|
})
|
66
homeassistant/components/cloud/cloudhooks.py
Normal file
66
homeassistant/components/cloud/cloudhooks.py
Normal 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()
|
@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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, {})
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
|
@ -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')
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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": {
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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__)
|
||||||
|
|
||||||
|
@ -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": {
|
||||||
|
10
homeassistant/components/dialogflow/.translations/hu.json
Normal file
10
homeassistant/components/dialogflow/.translations/hu.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Dialogflow Webhook be\u00e1ll\u00edt\u00e1sa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Dialogflow"
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@
|
|||||||
"one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4."
|
"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": {
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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})
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
268
homeassistant/components/geo_location/usgs_earthquakes_feed.py
Normal file
268
homeassistant/components/geo_location/usgs_earthquakes_feed.py
Normal 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
|
@ -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({
|
||||||
|
@ -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:
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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'])
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
163
homeassistant/components/hlk_sw16.py
Normal file
163
homeassistant/components/hlk_sw16.py
Normal 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)
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 ",
|
"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",
|
||||||
"title": "Enlla\u00e7ar punt d'acc\u00e9s"
|
"title": "Enlla\u00e7ar punt d'acc\u00e9s"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0432\u0441\u0435\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432)",
|
"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 ",
|
"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 ",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user