mirror of
https://github.com/home-assistant/core.git
synced 2026-01-08 16:47:42 +01:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ef7d1dfd0 | ||
|
|
2c915af348 | ||
|
|
2b733917a4 | ||
|
|
353010712f | ||
|
|
07a0bc4abe | ||
|
|
f9b48844e6 | ||
|
|
6053d02e44 | ||
|
|
6ac33e5c7b | ||
|
|
586566e6ab | ||
|
|
5a46adfebf | ||
|
|
09f7a09ce7 | ||
|
|
e2e01f5020 | ||
|
|
0e1450838e | ||
|
|
006419b96c | ||
|
|
3f54533e72 | ||
|
|
cc126761e1 | ||
|
|
8fdf68c8d1 | ||
|
|
6a8582750c | ||
|
|
5b51f740df | ||
|
|
881b35f9d6 | ||
|
|
8f852bd656 | ||
|
|
368d04b2a1 | ||
|
|
6d83dafff2 | ||
|
|
4bb319e658 | ||
|
|
3d9b6332c8 | ||
|
|
1a6535ff8b | ||
|
|
3fd14ca3cf | ||
|
|
1c70435df6 | ||
|
|
8dbdf0b0e1 | ||
|
|
fe9c85aaf1 | ||
|
|
9de800ab6a | ||
|
|
f530ea10af | ||
|
|
64224f46ec | ||
|
|
413545bd91 | ||
|
|
f5af77d00c | ||
|
|
4bc520c724 | ||
|
|
8e5f46d5b5 | ||
|
|
9fa0779c1f | ||
|
|
c138a93454 | ||
|
|
c6b96f7250 | ||
|
|
4a8ecb82a8 | ||
|
|
80f3cb7d79 |
@@ -274,6 +274,7 @@ homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rfxtrx/* @danielhiversen
|
||||
homeassistant/components/ring/* @balloob
|
||||
homeassistant/components/rmvtransport/* @cgtobi
|
||||
homeassistant/components/roomba/* @pschmitt
|
||||
homeassistant/components/saj/* @fredericvl
|
||||
|
||||
@@ -43,7 +43,7 @@ stages:
|
||||
release="$(Build.SourceBranchName)"
|
||||
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
|
||||
|
||||
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then
|
||||
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -1073,6 +1073,15 @@ class AlexaSecurityPanelController(AlexaCapability):
|
||||
class AlexaModeController(AlexaCapability):
|
||||
"""Implements Alexa.ModeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
|
||||
"""
|
||||
|
||||
@@ -1183,28 +1192,38 @@ class AlexaModeController(AlexaCapability):
|
||||
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
# Add open/close semantics if tilt is not supported.
|
||||
if not supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
|
||||
raise_labels.append(AlexaSemantics.ACTION_OPEN)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_OPEN],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
||||
lower_labels,
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
||||
raise_labels,
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
|
||||
)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_OPEN],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
)
|
||||
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
return None
|
||||
@@ -1213,6 +1232,15 @@ class AlexaModeController(AlexaCapability):
|
||||
class AlexaRangeController(AlexaCapability):
|
||||
"""Implements Alexa.RangeController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html
|
||||
"""
|
||||
|
||||
@@ -1268,8 +1296,8 @@ class AlexaRangeController(AlexaCapability):
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||
|
||||
# Input Number Value
|
||||
@@ -1321,10 +1349,10 @@ class AlexaRangeController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Tilt Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt Resources
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
self._resource = AlexaPresetResource(
|
||||
["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING],
|
||||
["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION],
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
precision=1,
|
||||
@@ -1358,24 +1386,35 @@ class AlexaRangeController(AlexaCapability):
|
||||
|
||||
def semantics(self):
|
||||
"""Build and return semantics object."""
|
||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
|
||||
# Cover Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||
self._semantics = AlexaSemantics()
|
||||
|
||||
# Add open/close semantics if tilt is not supported.
|
||||
if not supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
|
||||
raise_labels.append(AlexaSemantics.ACTION_OPEN)
|
||||
self._semantics.add_states_to_value(
|
||||
[AlexaSemantics.STATES_CLOSED], value=0
|
||||
)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
||||
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
|
||||
self._semantics.add_states_to_range(
|
||||
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
|
||||
raise_labels, "SetRangeValue", {"rangeValue": 100}
|
||||
)
|
||||
return self._semantics.serialize_semantics()
|
||||
|
||||
# Cover Tilt Position
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||
self._semantics = AlexaSemantics()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
||||
@@ -1395,6 +1434,15 @@ class AlexaRangeController(AlexaCapability):
|
||||
class AlexaToggleController(AlexaCapability):
|
||||
"""Implements Alexa.ToggleController.
|
||||
|
||||
The instance property must be unique across ModeController, RangeController, ToggleController within the same device.
|
||||
The instance property should be a concatenated string of device domain period and single word.
|
||||
e.g. fan.speed & fan.direction.
|
||||
|
||||
The instance property must not contain words from other instance property strings within the same device.
|
||||
e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail.
|
||||
|
||||
An instance property string value may be reused for different devices.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
|
||||
"""
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Config helpers for Alexa."""
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .state_report import async_enable_proactive_mode
|
||||
|
||||
|
||||
class AbstractConfig:
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_unsub_proactive_report = None
|
||||
@@ -29,9 +31,9 @@ class AbstractConfig:
|
||||
return None
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def locale(self):
|
||||
"""Return config locale."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
|
||||
@@ -404,9 +404,7 @@ class CoverCapabilities(AlexaEntity):
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||
)
|
||||
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
||||
)
|
||||
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
||||
@@ -1118,8 +1118,8 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = range_value
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
@@ -1192,8 +1192,8 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
100, max(0, range_delta + current)
|
||||
)
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_delta = int(range_delta)
|
||||
service = SERVICE_SET_COVER_TILT_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||
|
||||
@@ -190,7 +190,12 @@ class AlexaGlobalCatalog:
|
||||
|
||||
|
||||
class AlexaCapabilityResource:
|
||||
"""Base class for Alexa capabilityResources, ModeResources, and presetResources objects.
|
||||
"""Base class for Alexa capabilityResources, modeResources, and presetResources objects.
|
||||
|
||||
Resources objects labels must be unique across all modeResources and presetResources within the same device.
|
||||
To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array.
|
||||
You cannot use any words from the following list as friendly names:
|
||||
https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
@@ -312,6 +317,14 @@ class AlexaSemantics:
|
||||
|
||||
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||
|
||||
Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has
|
||||
multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail.
|
||||
|
||||
You can support semantics actionMappings on different controllers for the same device, however each controller must
|
||||
support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController,
|
||||
but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported
|
||||
for one interface on the same device.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||
"""
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.helpers.event import async_call_later
|
||||
from .config_flow import configured_instances
|
||||
from .const import (
|
||||
ATTR_LAST_DATA,
|
||||
ATTR_MONITORED_CONDITIONS,
|
||||
CONF_APP_KEY,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
@@ -341,7 +342,6 @@ class AmbientStation:
|
||||
self._watchdog_listener = None
|
||||
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
|
||||
self.client = client
|
||||
self.monitored_conditions = []
|
||||
self.stations = {}
|
||||
|
||||
async def _attempt_connect(self):
|
||||
@@ -398,19 +398,19 @@ class AmbientStation:
|
||||
|
||||
_LOGGER.debug("New station subscription: %s", data)
|
||||
|
||||
self.monitored_conditions = [
|
||||
# Only create entities based on the data coming through the socket.
|
||||
# If the user is monitoring brightness (in W/m^2), make sure we also
|
||||
# add a calculated sensor for the same data measured in lx:
|
||||
monitored_conditions = [
|
||||
k for k in station["lastData"] if k in SENSOR_TYPES
|
||||
]
|
||||
|
||||
# If the user is monitoring brightness (in W/m^2),
|
||||
# make sure we also add a calculated sensor for the
|
||||
# same data measured in lx:
|
||||
if TYPE_SOLARRADIATION in self.monitored_conditions:
|
||||
self.monitored_conditions.append(TYPE_SOLARRADIATION_LX)
|
||||
if TYPE_SOLARRADIATION in monitored_conditions:
|
||||
monitored_conditions.append(TYPE_SOLARRADIATION_LX)
|
||||
|
||||
self.stations[station["macAddress"]] = {
|
||||
ATTR_LAST_DATA: station["lastData"],
|
||||
ATTR_LOCATION: station.get("info", {}).get("location"),
|
||||
ATTR_MONITORED_CONDITIONS: monitored_conditions,
|
||||
ATTR_NAME: station.get("info", {}).get(
|
||||
"name", station["macAddress"]
|
||||
),
|
||||
|
||||
@@ -19,7 +19,13 @@ from . import (
|
||||
TYPE_BATTOUT,
|
||||
AmbientWeatherEntity,
|
||||
)
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR
|
||||
from .const import (
|
||||
ATTR_LAST_DATA,
|
||||
ATTR_MONITORED_CONDITIONS,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
TYPE_BINARY_SENSOR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,7 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
|
||||
binary_sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in ambient.monitored_conditions:
|
||||
for condition in station[ATTR_MONITORED_CONDITIONS]:
|
||||
name, _, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == TYPE_BINARY_SENSOR:
|
||||
binary_sensor_list.append(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
DOMAIN = "ambient_station"
|
||||
|
||||
ATTR_LAST_DATA = "last_data"
|
||||
ATTR_MONITORED_CONDITIONS = "monitored_conditions"
|
||||
|
||||
CONF_APP_KEY = "app_key"
|
||||
|
||||
|
||||
@@ -9,7 +9,13 @@ from . import (
|
||||
TYPE_SOLARRADIATION_LX,
|
||||
AmbientWeatherEntity,
|
||||
)
|
||||
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR
|
||||
from .const import (
|
||||
ATTR_LAST_DATA,
|
||||
ATTR_MONITORED_CONDITIONS,
|
||||
DATA_CLIENT,
|
||||
DOMAIN,
|
||||
TYPE_SENSOR,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +31,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
||||
|
||||
sensor_list = []
|
||||
for mac_address, station in ambient.stations.items():
|
||||
for condition in ambient.monitored_conditions:
|
||||
for condition in station[ATTR_MONITORED_CONDITIONS]:
|
||||
name, unit, kind, device_class = SENSOR_TYPES[condition]
|
||||
if kind == TYPE_SENSOR:
|
||||
sensor_list.append(
|
||||
|
||||
@@ -132,8 +132,6 @@ class AmcrestChecker(Http):
|
||||
offline = not self.available
|
||||
if offline and was_online:
|
||||
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
|
||||
with self._token_lock:
|
||||
self._token = None
|
||||
dispatcher_send(
|
||||
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)
|
||||
)
|
||||
|
||||
@@ -78,6 +78,12 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
return self._endpoint
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
"""Return config locale."""
|
||||
# Not clear how to determine locale atm.
|
||||
return "en-US"
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
"""Return entity config."""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Support for deCONZ devices."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import _UNDEF
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
from .config_flow import get_master_gateway
|
||||
from .const import CONF_MASTER_GATEWAY, DOMAIN
|
||||
from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
|
||||
from .gateway import DeconzGateway
|
||||
from .services import async_setup_services, async_unload_services
|
||||
|
||||
@@ -37,8 +38,14 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
# 0.104 introduced config entry unique id, this makes upgrading possible
|
||||
if config_entry.unique_id is None:
|
||||
|
||||
new_data = _UNDEF
|
||||
if CONF_BRIDGE_ID in config_entry.data:
|
||||
new_data = dict(config_entry.data)
|
||||
new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID]
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=gateway.api.config.bridgeid
|
||||
config_entry, unique_id=gateway.api.config.bridgeid, data=new_data
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][config_entry.unique_id] = gateway
|
||||
|
||||
@@ -54,11 +54,13 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
|
||||
"""Representation of a deCONZ binary sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"on", "reachable", "state"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.helpers import aiohttp_client
|
||||
from .const import (
|
||||
CONF_ALLOW_CLIP_SENSOR,
|
||||
CONF_ALLOW_DECONZ_GROUPS,
|
||||
CONF_BRIDGEID,
|
||||
CONF_BRIDGE_ID,
|
||||
DEFAULT_ALLOW_CLIP_SENSOR,
|
||||
DEFAULT_ALLOW_DECONZ_GROUPS,
|
||||
DEFAULT_PORT,
|
||||
@@ -74,7 +74,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.bridge_id = bridge[CONF_BRIDGEID]
|
||||
self.bridge_id = bridge[CONF_BRIDGE_ID]
|
||||
self.deconz_config = {
|
||||
CONF_HOST: bridge[CONF_HOST],
|
||||
CONF_PORT: bridge[CONF_PORT],
|
||||
|
||||
@@ -5,7 +5,8 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "deconz"
|
||||
|
||||
CONF_BRIDGEID = "bridgeid"
|
||||
CONF_BRIDGE_ID = "bridgeid"
|
||||
CONF_GROUP_ID_BASE = "group_id_base"
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_ALLOW_CLIP_SENSOR = False
|
||||
|
||||
@@ -91,8 +91,11 @@ class DeconzDevice(DeconzBase, Entity):
|
||||
unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the device's state."""
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
||||
@@ -39,17 +39,21 @@ class DeconzEvent(DeconzBase):
|
||||
self._device = None
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Fire the event if reason is that state is updated."""
|
||||
if "state" in self._device.changed_keys:
|
||||
data = {
|
||||
CONF_ID: self.event_id,
|
||||
CONF_UNIQUE_ID: self.serial,
|
||||
CONF_EVENT: self._device.state,
|
||||
}
|
||||
if self._device.gesture:
|
||||
data[CONF_GESTURE] = self._device.gesture
|
||||
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
|
||||
if ignore_update or "state" not in self._device.changed_keys:
|
||||
return
|
||||
|
||||
data = {
|
||||
CONF_ID: self.event_id,
|
||||
CONF_UNIQUE_ID: self.serial,
|
||||
CONF_EVENT: self._device.state,
|
||||
}
|
||||
|
||||
if self._device.gesture:
|
||||
data[CONF_GESTURE] = self._device.gesture
|
||||
|
||||
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
|
||||
|
||||
async def async_update_device_registry(self):
|
||||
"""Update device registry."""
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import (
|
||||
CONF_GROUP_ID_BASE,
|
||||
COVER_TYPES,
|
||||
DOMAIN as DECONZ_DOMAIN,
|
||||
NEW_GROUP,
|
||||
@@ -205,7 +206,11 @@ class DeconzGroup(DeconzLight):
|
||||
"""Set up group and create an unique id."""
|
||||
super().__init__(device, gateway)
|
||||
|
||||
self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}"
|
||||
group_id_base = self.gateway.config_entry.unique_id
|
||||
if CONF_GROUP_ID_BASE in self.gateway.config_entry.data:
|
||||
group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE]
|
||||
|
||||
self._unique_id = f"{group_id_base}-{self._device.deconz_id}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
"name": "deCONZ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||
"requirements": ["pydeconz==67"],
|
||||
"requirements": [
|
||||
"pydeconz==68"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@kane610"],
|
||||
"codeowners": [
|
||||
"@kane610"
|
||||
],
|
||||
"quality_scale": "platinum"
|
||||
}
|
||||
}
|
||||
@@ -97,11 +97,13 @@ class DeconzSensor(DeconzDevice):
|
||||
"""Representation of a deCONZ sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"on", "reachable", "state"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -155,11 +157,13 @@ class DeconzBattery(DeconzDevice):
|
||||
"""Battery class for when a device is only represented as an event."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the battery's state, if needed."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"battery", "reachable"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@@ -217,7 +221,7 @@ class DeconzSensorStateTracker:
|
||||
self.sensor = None
|
||||
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
def async_update_callback(self, ignore_update=False):
|
||||
"""Sensor state updated."""
|
||||
if "battery" in self.sensor.changed_keys:
|
||||
async_dispatcher_send(
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from .config_flow import get_master_gateway
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_BRIDGEID,
|
||||
CONF_BRIDGE_ID,
|
||||
DOMAIN,
|
||||
NEW_GROUP,
|
||||
NEW_LIGHT,
|
||||
@@ -27,14 +27,14 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
|
||||
vol.Optional(SERVICE_ENTITY): cv.entity_id,
|
||||
vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
|
||||
vol.Required(SERVICE_DATA): dict,
|
||||
vol.Optional(CONF_BRIDGEID): str,
|
||||
vol.Optional(CONF_BRIDGE_ID): str,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
|
||||
)
|
||||
|
||||
SERVICE_DEVICE_REFRESH = "device_refresh"
|
||||
SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str}))
|
||||
SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
|
||||
|
||||
|
||||
async def async_setup_services(hass):
|
||||
@@ -97,7 +97,7 @@ async def async_configure_service(hass, data):
|
||||
See Dresden Elektroniks REST API documentation for details:
|
||||
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
||||
"""
|
||||
bridgeid = data.get(CONF_BRIDGEID)
|
||||
bridgeid = data.get(CONF_BRIDGE_ID)
|
||||
field = data.get(SERVICE_FIELD, "")
|
||||
entity_id = data.get(SERVICE_ENTITY)
|
||||
data = data[SERVICE_DATA]
|
||||
@@ -119,15 +119,15 @@ async def async_configure_service(hass, data):
|
||||
async def async_refresh_devices_service(hass, data):
|
||||
"""Refresh available devices from deCONZ."""
|
||||
gateway = get_master_gateway(hass)
|
||||
if CONF_BRIDGEID in data:
|
||||
gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]]
|
||||
if CONF_BRIDGE_ID in data:
|
||||
gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]]
|
||||
|
||||
groups = set(gateway.api.groups.keys())
|
||||
lights = set(gateway.api.lights.keys())
|
||||
scenes = set(gateway.api.scenes.keys())
|
||||
sensors = set(gateway.api.sensors.keys())
|
||||
|
||||
await gateway.api.refresh_state()
|
||||
await gateway.api.refresh_state(ignore_update=True)
|
||||
|
||||
gateway.async_add_device_callback(
|
||||
NEW_GROUP,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Emulated Roku",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/emulated_roku",
|
||||
"requirements": ["emulated_roku==0.1.8"],
|
||||
"requirements": ["emulated_roku==0.1.9"],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20200108.0"
|
||||
],
|
||||
"requirements": ["home-assistant-frontend==20200108.2"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
@@ -14,8 +12,6 @@
|
||||
"system_log",
|
||||
"websocket_api"
|
||||
],
|
||||
"codeowners": [
|
||||
"@home-assistant/frontend"
|
||||
],
|
||||
"codeowners": ["@home-assistant/frontend"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +66,20 @@ class Light(HomeAccessory):
|
||||
self._features = self.hass.states.get(self.entity_id).attributes.get(
|
||||
ATTR_SUPPORTED_FEATURES
|
||||
)
|
||||
|
||||
if self._features & SUPPORT_BRIGHTNESS:
|
||||
self.chars.append(CHAR_BRIGHTNESS)
|
||||
if self._features & SUPPORT_COLOR_TEMP:
|
||||
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||
|
||||
if self._features & SUPPORT_COLOR:
|
||||
self.chars.append(CHAR_HUE)
|
||||
self.chars.append(CHAR_SATURATION)
|
||||
self._hue = None
|
||||
self._saturation = None
|
||||
elif self._features & SUPPORT_COLOR_TEMP:
|
||||
# ColorTemperature and Hue characteristic should not be
|
||||
# exposed both. Both states are tracked separately in HomeKit,
|
||||
# causing "source of truth" problems.
|
||||
self.chars.append(CHAR_COLOR_TEMPERATURE)
|
||||
|
||||
serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars)
|
||||
self.char_on = serv_light.configure_char(
|
||||
@@ -88,6 +93,7 @@ class Light(HomeAccessory):
|
||||
self.char_brightness = serv_light.configure_char(
|
||||
CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness
|
||||
)
|
||||
|
||||
if CHAR_COLOR_TEMPERATURE in self.chars:
|
||||
min_mireds = self.hass.states.get(self.entity_id).attributes.get(
|
||||
ATTR_MIN_MIREDS, 153
|
||||
@@ -101,10 +107,12 @@ class Light(HomeAccessory):
|
||||
properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds},
|
||||
setter_callback=self.set_color_temperature,
|
||||
)
|
||||
|
||||
if CHAR_HUE in self.chars:
|
||||
self.char_hue = serv_light.configure_char(
|
||||
CHAR_HUE, value=0, setter_callback=self.set_hue
|
||||
)
|
||||
|
||||
if CHAR_SATURATION in self.chars:
|
||||
self.char_saturation = serv_light.configure_char(
|
||||
CHAR_SATURATION, value=75, setter_callback=self.set_saturation
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Support for HomeMatic devices."""
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
@@ -18,231 +18,68 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_DISCOVER_DEVICES,
|
||||
ATTR_DISCOVERY_TYPE,
|
||||
ATTR_ERRORCODE,
|
||||
ATTR_INTERFACE,
|
||||
ATTR_LOW_BAT,
|
||||
ATTR_LOWBAT,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_PARAM,
|
||||
ATTR_PARAMSET,
|
||||
ATTR_PARAMSET_KEY,
|
||||
ATTR_TIME,
|
||||
ATTR_UNIQUE_ID,
|
||||
ATTR_VALUE,
|
||||
ATTR_VALUE_TYPE,
|
||||
CONF_CALLBACK_IP,
|
||||
CONF_CALLBACK_PORT,
|
||||
CONF_INTERFACES,
|
||||
CONF_JSONPORT,
|
||||
CONF_LOCAL_IP,
|
||||
CONF_LOCAL_PORT,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_RESOLVENAMES,
|
||||
CONF_RESOLVENAMES_OPTIONS,
|
||||
DATA_CONF,
|
||||
DATA_HOMEMATIC,
|
||||
DATA_STORE,
|
||||
DISCOVER_BATTERY,
|
||||
DISCOVER_BINARY_SENSORS,
|
||||
DISCOVER_CLIMATE,
|
||||
DISCOVER_COVER,
|
||||
DISCOVER_LIGHTS,
|
||||
DISCOVER_LOCKS,
|
||||
DISCOVER_SENSORS,
|
||||
DISCOVER_SWITCHES,
|
||||
DOMAIN,
|
||||
EVENT_ERROR,
|
||||
EVENT_IMPULSE,
|
||||
EVENT_KEYPRESS,
|
||||
HM_DEVICE_TYPES,
|
||||
HM_IGNORE_DISCOVERY_NODE,
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS,
|
||||
HM_IMPULSE_EVENTS,
|
||||
HM_PRESS_EVENTS,
|
||||
SERVICE_PUT_PARAMSET,
|
||||
SERVICE_RECONNECT,
|
||||
SERVICE_SET_DEVICE_VALUE,
|
||||
SERVICE_SET_INSTALL_MODE,
|
||||
SERVICE_SET_VARIABLE_VALUE,
|
||||
SERVICE_VIRTUALKEY,
|
||||
)
|
||||
from .entity import HMHub
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "homematic"
|
||||
|
||||
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
||||
|
||||
DISCOVER_SWITCHES = "homematic.switch"
|
||||
DISCOVER_LIGHTS = "homematic.light"
|
||||
DISCOVER_SENSORS = "homematic.sensor"
|
||||
DISCOVER_BINARY_SENSORS = "homematic.binary_sensor"
|
||||
DISCOVER_COVER = "homematic.cover"
|
||||
DISCOVER_CLIMATE = "homematic.climate"
|
||||
DISCOVER_LOCKS = "homematic.locks"
|
||||
DISCOVER_BATTERY = "homematic.battery"
|
||||
|
||||
ATTR_DISCOVER_DEVICES = "devices"
|
||||
ATTR_PARAM = "param"
|
||||
ATTR_CHANNEL = "channel"
|
||||
ATTR_ADDRESS = "address"
|
||||
ATTR_VALUE = "value"
|
||||
ATTR_VALUE_TYPE = "value_type"
|
||||
ATTR_INTERFACE = "interface"
|
||||
ATTR_ERRORCODE = "error"
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_TIME = "time"
|
||||
ATTR_UNIQUE_ID = "unique_id"
|
||||
ATTR_PARAMSET_KEY = "paramset_key"
|
||||
ATTR_PARAMSET = "paramset"
|
||||
ATTR_DISCOVERY_TYPE = "discovery_type"
|
||||
ATTR_LOW_BAT = "LOW_BAT"
|
||||
ATTR_LOWBAT = "LOWBAT"
|
||||
|
||||
|
||||
EVENT_KEYPRESS = "homematic.keypress"
|
||||
EVENT_IMPULSE = "homematic.impulse"
|
||||
EVENT_ERROR = "homematic.error"
|
||||
|
||||
SERVICE_VIRTUALKEY = "virtualkey"
|
||||
SERVICE_RECONNECT = "reconnect"
|
||||
SERVICE_SET_VARIABLE_VALUE = "set_variable_value"
|
||||
SERVICE_SET_DEVICE_VALUE = "set_device_value"
|
||||
SERVICE_SET_INSTALL_MODE = "set_install_mode"
|
||||
SERVICE_PUT_PARAMSET = "put_paramset"
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
"Switch",
|
||||
"SwitchPowermeter",
|
||||
"IOSwitch",
|
||||
"IPSwitch",
|
||||
"RFSiren",
|
||||
"IPSwitchPowermeter",
|
||||
"HMWIOSwitch",
|
||||
"Rain",
|
||||
"EcoLogic",
|
||||
"IPKeySwitchPowermeter",
|
||||
"IPGarage",
|
||||
"IPKeySwitch",
|
||||
"IPKeySwitchLevel",
|
||||
"IPMultiIO",
|
||||
],
|
||||
DISCOVER_LIGHTS: [
|
||||
"Dimmer",
|
||||
"KeyDimmer",
|
||||
"IPKeyDimmer",
|
||||
"IPDimmer",
|
||||
"ColorEffectLight",
|
||||
"IPKeySwitchLevel",
|
||||
],
|
||||
DISCOVER_SENSORS: [
|
||||
"SwitchPowermeter",
|
||||
"Motion",
|
||||
"MotionV2",
|
||||
"RemoteMotion",
|
||||
"MotionIP",
|
||||
"ThermostatWall",
|
||||
"AreaThermostat",
|
||||
"RotaryHandleSensor",
|
||||
"WaterSensor",
|
||||
"PowermeterGas",
|
||||
"LuxSensor",
|
||||
"WeatherSensor",
|
||||
"WeatherStation",
|
||||
"ThermostatWall2",
|
||||
"TemperatureDiffSensor",
|
||||
"TemperatureSensor",
|
||||
"CO2Sensor",
|
||||
"IPSwitchPowermeter",
|
||||
"HMWIOSwitch",
|
||||
"FillingLevel",
|
||||
"ValveDrive",
|
||||
"EcoLogic",
|
||||
"IPThermostatWall",
|
||||
"IPSmoke",
|
||||
"RFSiren",
|
||||
"PresenceIP",
|
||||
"IPAreaThermostat",
|
||||
"IPWeatherSensor",
|
||||
"RotaryHandleSensorIP",
|
||||
"IPPassageSensor",
|
||||
"IPKeySwitchPowermeter",
|
||||
"IPThermostatWall230V",
|
||||
"IPWeatherSensorPlus",
|
||||
"IPWeatherSensorBasic",
|
||||
"IPBrightnessSensor",
|
||||
"IPGarage",
|
||||
"UniversalSensor",
|
||||
"MotionIPV2",
|
||||
"IPMultiIO",
|
||||
"IPThermostatWall2",
|
||||
],
|
||||
DISCOVER_CLIMATE: [
|
||||
"Thermostat",
|
||||
"ThermostatWall",
|
||||
"MAXThermostat",
|
||||
"ThermostatWall2",
|
||||
"MAXWallThermostat",
|
||||
"IPThermostat",
|
||||
"IPThermostatWall",
|
||||
"ThermostatGroup",
|
||||
"IPThermostatWall230V",
|
||||
"IPThermostatWall2",
|
||||
],
|
||||
DISCOVER_BINARY_SENSORS: [
|
||||
"ShutterContact",
|
||||
"Smoke",
|
||||
"SmokeV2",
|
||||
"Motion",
|
||||
"MotionV2",
|
||||
"MotionIP",
|
||||
"RemoteMotion",
|
||||
"WeatherSensor",
|
||||
"TiltSensor",
|
||||
"IPShutterContact",
|
||||
"HMWIOSwitch",
|
||||
"MaxShutterContact",
|
||||
"Rain",
|
||||
"WiredSensor",
|
||||
"PresenceIP",
|
||||
"IPWeatherSensor",
|
||||
"IPPassageSensor",
|
||||
"SmartwareMotion",
|
||||
"IPWeatherSensorPlus",
|
||||
"MotionIPV2",
|
||||
"WaterIP",
|
||||
"IPMultiIO",
|
||||
"TiltIP",
|
||||
"IPShutterContactSabotage",
|
||||
],
|
||||
DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"],
|
||||
DISCOVER_LOCKS: ["KeyMatic"],
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"]
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
|
||||
"ACTUAL_TEMPERATURE": [
|
||||
"IPAreaThermostat",
|
||||
"IPWeatherSensor",
|
||||
"IPWeatherSensorPlus",
|
||||
"IPWeatherSensorBasic",
|
||||
"IPThermostatWall",
|
||||
"IPThermostatWall2",
|
||||
]
|
||||
}
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
"LOWBAT": ["battery", {0: "High", 1: "Low"}],
|
||||
"LOW_BAT": ["battery", {0: "High", 1: "Low"}],
|
||||
"ERROR": ["error", {0: "No"}],
|
||||
"ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}],
|
||||
"SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}],
|
||||
"RSSI_PEER": ["rssi_peer", {}],
|
||||
"RSSI_DEVICE": ["rssi_device", {}],
|
||||
"VALVE_STATE": ["valve", {}],
|
||||
"LEVEL": ["level", {}],
|
||||
"BATTERY_STATE": ["battery", {}],
|
||||
"CONTROL_MODE": [
|
||||
"mode",
|
||||
{0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"},
|
||||
],
|
||||
"POWER": ["power", {}],
|
||||
"CURRENT": ["current", {}],
|
||||
"VOLTAGE": ["voltage", {}],
|
||||
"OPERATING_VOLTAGE": ["voltage", {}],
|
||||
"WORKING": ["working", {0: "No", 1: "Yes"}],
|
||||
"STATE_UNCERTAIN": ["state_uncertain", {}],
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
"PRESS_SHORT",
|
||||
"PRESS_LONG",
|
||||
"PRESS_CONT",
|
||||
"PRESS_LONG_RELEASE",
|
||||
"PRESS",
|
||||
]
|
||||
|
||||
HM_IMPULSE_EVENTS = ["SEQUENCE_OK"]
|
||||
|
||||
CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False]
|
||||
|
||||
DATA_HOMEMATIC = "homematic"
|
||||
DATA_STORE = "homematic_store"
|
||||
DATA_CONF = "homematic_conf"
|
||||
|
||||
CONF_INTERFACES = "interfaces"
|
||||
CONF_LOCAL_IP = "local_ip"
|
||||
CONF_LOCAL_PORT = "local_port"
|
||||
CONF_PORT = "port"
|
||||
CONF_PATH = "path"
|
||||
CONF_CALLBACK_IP = "callback_ip"
|
||||
CONF_CALLBACK_PORT = "callback_port"
|
||||
CONF_RESOLVENAMES = "resolvenames"
|
||||
CONF_JSONPORT = "jsonport"
|
||||
CONF_VARIABLES = "variables"
|
||||
CONF_DEVICES = "devices"
|
||||
CONF_PRIMARY = "primary"
|
||||
|
||||
DEFAULT_LOCAL_IP = "0.0.0.0"
|
||||
DEFAULT_LOCAL_PORT = 0
|
||||
DEFAULT_RESOLVENAMES = False
|
||||
@@ -775,277 +612,3 @@ def _device_from_servicecall(hass, service):
|
||||
for devices in hass.data[DATA_HOMEMATIC].devices.values():
|
||||
if address in devices:
|
||||
return devices[address]
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""The HomeMatic hub. (CCU2/HomeGear)."""
|
||||
|
||||
def __init__(self, hass, homematic, name):
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
|
||||
self._homematic = homematic
|
||||
self._variables = {}
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
# Load data
|
||||
self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB)
|
||||
self.hass.add_job(self._update_hub, None)
|
||||
|
||||
self.hass.helpers.event.track_time_interval(
|
||||
self._update_variables, SCAN_INTERVAL_VARIABLES
|
||||
)
|
||||
self.hass.add_job(self._update_variables, None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. HomeMatic Hub object updates variables."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = self._variables.copy()
|
||||
return attr
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:gradient"
|
||||
|
||||
def _update_hub(self, now):
|
||||
"""Retrieve latest state."""
|
||||
service_message = self._homematic.getServiceMessages(self._name)
|
||||
state = None if service_message is None else len(service_message)
|
||||
|
||||
# state have change?
|
||||
if self._state != state:
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_variables(self, now):
|
||||
"""Retrieve all variable data and update hmvariable states."""
|
||||
variables = self._homematic.getAllSystemVariables(self._name)
|
||||
if variables is None:
|
||||
return
|
||||
|
||||
state_change = False
|
||||
for key, value in variables.items():
|
||||
if key in self._variables and value == self._variables[key]:
|
||||
continue
|
||||
|
||||
state_change = True
|
||||
self._variables.update({key: value})
|
||||
|
||||
if state_change:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set_variable(self, name, value):
|
||||
"""Set variable value on CCU/Homegear."""
|
||||
if name not in self._variables:
|
||||
_LOGGER.error("Variable %s not found on %s", name, self.name)
|
||||
return
|
||||
old_value = self._variables.get(name)
|
||||
if isinstance(old_value, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
self._homematic.setSystemVariable(self.name, name, value)
|
||||
|
||||
self._variables.update({name: value})
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The HomeMatic device base object."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._interface = config.get(ATTR_INTERFACE)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._unique_id = config.get(ATTR_UNIQUE_ID)
|
||||
self._data = {}
|
||||
self._homematic = None
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set parameter to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Load data init callbacks."""
|
||||
await self.hass.async_add_job(self.link_homematic)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID. HomeMatic entity IDs are unique by default."""
|
||||
return self._unique_id.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# Generate a dictionary with attributes
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attribute and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# Static attributes
|
||||
attr["id"] = self._hmdevice.ADDRESS
|
||||
attr["interface"] = self._interface
|
||||
|
||||
return attr
|
||||
|
||||
def link_homematic(self):
|
||||
"""Connect to HomeMatic."""
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Initialize
|
||||
self._homematic = self.hass.data[DATA_HOMEMATIC]
|
||||
self._hmdevice = self._homematic.devices[self._interface][self._address]
|
||||
self._connected = True
|
||||
|
||||
try:
|
||||
# Initialize datapoints of this object
|
||||
self._init_data()
|
||||
self._load_data_from_hm()
|
||||
|
||||
# Link events from pyhomematic
|
||||
self._subscribe_homematic_events()
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s", self._address, str(err))
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value)
|
||||
has_changed = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
has_changed = True
|
||||
|
||||
# Availability has changed
|
||||
if self.available != (not self._hmdevice.UNREACH):
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
has_changed = True
|
||||
|
||||
# If it has changed data point, update Home Assistant
|
||||
if has_changed:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = set()
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (
|
||||
self._hmdevice.SENSORNODE,
|
||||
self._hmdevice.BINARYNODE,
|
||||
self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.WRITENODE,
|
||||
self._hmdevice.EVENTNODE,
|
||||
self._hmdevice.ACTIONNODE,
|
||||
):
|
||||
for node, channels in metadata.items():
|
||||
# Data is needed for this instance
|
||||
if node in self._data:
|
||||
# chan is current channel
|
||||
if len(channels) == 1:
|
||||
channel = channels[0]
|
||||
else:
|
||||
channel = self._channel
|
||||
|
||||
# Prepare for subscription
|
||||
try:
|
||||
channels_to_sub.add(int(channel))
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER.error("Invalid channel in metadata from %s", self._name)
|
||||
|
||||
# Set callbacks
|
||||
for channel in channels_to_sub:
|
||||
_LOGGER.debug("Subscribe channel %d from %s", channel, self._name)
|
||||
self._hmdevice.setEventCallback(
|
||||
callback=self._hm_event_callback, bequeath=False, channel=channel
|
||||
)
|
||||
|
||||
def _load_data_from_hm(self):
|
||||
"""Load first value from pyhomematic."""
|
||||
if not self._connected:
|
||||
return False
|
||||
|
||||
# Read data from pyhomematic
|
||||
for metadata, funct in (
|
||||
(self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData),
|
||||
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
|
||||
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
||||
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData),
|
||||
):
|
||||
for node in metadata:
|
||||
if metadata[node] and node in self._data:
|
||||
self._data[node] = funct(name=node, channel=self._channel)
|
||||
|
||||
return True
|
||||
|
||||
def _hm_set_state(self, value):
|
||||
"""Set data to main datapoint."""
|
||||
if self._state in self._data:
|
||||
self._data[self._state] = value
|
||||
|
||||
def _hm_get_state(self):
|
||||
"""Get data from main datapoint."""
|
||||
if self._state in self._data:
|
||||
return self._data[self._state]
|
||||
return None
|
||||
|
||||
def _init_data(self):
|
||||
"""Generate a data dict (self._data) from the HomeMatic metadata."""
|
||||
# Add all attributes to data dictionary
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: STATE_UNKNOWN})
|
||||
|
||||
# Initialize device specific data
|
||||
self._init_data_struct()
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dictionary from the HomeMatic device metadata."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -9,9 +9,9 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASS_SMOKE,
|
||||
BinarySensorDevice,
|
||||
)
|
||||
from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,6 +29,7 @@ SENSOR_TYPES_CLASS = {
|
||||
"SmokeV2": DEVICE_CLASS_SMOKE,
|
||||
"TiltSensor": None,
|
||||
"WeatherSensor": None,
|
||||
"IPContact": DEVICE_CLASS_OPENING,
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
else:
|
||||
devices.append(HMBinarySensor(conf))
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
|
||||
@@ -14,7 +14,8 @@ from homeassistant.components.climate.const import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
new_device = HMThermostat(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMThermostat(HMDevice, ClimateDevice):
|
||||
|
||||
212
homeassistant/components/homematic/const.py
Normal file
212
homeassistant/components/homematic/const.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Constants for the homematic component."""
|
||||
|
||||
DOMAIN = "homematic"
|
||||
|
||||
DISCOVER_SWITCHES = "homematic.switch"
|
||||
DISCOVER_LIGHTS = "homematic.light"
|
||||
DISCOVER_SENSORS = "homematic.sensor"
|
||||
DISCOVER_BINARY_SENSORS = "homematic.binary_sensor"
|
||||
DISCOVER_COVER = "homematic.cover"
|
||||
DISCOVER_CLIMATE = "homematic.climate"
|
||||
DISCOVER_LOCKS = "homematic.locks"
|
||||
DISCOVER_BATTERY = "homematic.battery"
|
||||
|
||||
ATTR_DISCOVER_DEVICES = "devices"
|
||||
ATTR_PARAM = "param"
|
||||
ATTR_CHANNEL = "channel"
|
||||
ATTR_ADDRESS = "address"
|
||||
ATTR_VALUE = "value"
|
||||
ATTR_VALUE_TYPE = "value_type"
|
||||
ATTR_INTERFACE = "interface"
|
||||
ATTR_ERRORCODE = "error"
|
||||
ATTR_MESSAGE = "message"
|
||||
ATTR_TIME = "time"
|
||||
ATTR_UNIQUE_ID = "unique_id"
|
||||
ATTR_PARAMSET_KEY = "paramset_key"
|
||||
ATTR_PARAMSET = "paramset"
|
||||
ATTR_DISCOVERY_TYPE = "discovery_type"
|
||||
ATTR_LOW_BAT = "LOW_BAT"
|
||||
ATTR_LOWBAT = "LOWBAT"
|
||||
|
||||
EVENT_KEYPRESS = "homematic.keypress"
|
||||
EVENT_IMPULSE = "homematic.impulse"
|
||||
EVENT_ERROR = "homematic.error"
|
||||
|
||||
SERVICE_VIRTUALKEY = "virtualkey"
|
||||
SERVICE_RECONNECT = "reconnect"
|
||||
SERVICE_SET_VARIABLE_VALUE = "set_variable_value"
|
||||
SERVICE_SET_DEVICE_VALUE = "set_device_value"
|
||||
SERVICE_SET_INSTALL_MODE = "set_install_mode"
|
||||
SERVICE_PUT_PARAMSET = "put_paramset"
|
||||
|
||||
HM_DEVICE_TYPES = {
|
||||
DISCOVER_SWITCHES: [
|
||||
"Switch",
|
||||
"SwitchPowermeter",
|
||||
"IOSwitch",
|
||||
"IPSwitch",
|
||||
"RFSiren",
|
||||
"IPSwitchPowermeter",
|
||||
"HMWIOSwitch",
|
||||
"Rain",
|
||||
"EcoLogic",
|
||||
"IPKeySwitchPowermeter",
|
||||
"IPGarage",
|
||||
"IPKeySwitch",
|
||||
"IPKeySwitchLevel",
|
||||
"IPMultiIO",
|
||||
],
|
||||
DISCOVER_LIGHTS: [
|
||||
"Dimmer",
|
||||
"KeyDimmer",
|
||||
"IPKeyDimmer",
|
||||
"IPDimmer",
|
||||
"ColorEffectLight",
|
||||
"IPKeySwitchLevel",
|
||||
],
|
||||
DISCOVER_SENSORS: [
|
||||
"SwitchPowermeter",
|
||||
"Motion",
|
||||
"MotionV2",
|
||||
"RemoteMotion",
|
||||
"MotionIP",
|
||||
"ThermostatWall",
|
||||
"AreaThermostat",
|
||||
"RotaryHandleSensor",
|
||||
"WaterSensor",
|
||||
"PowermeterGas",
|
||||
"LuxSensor",
|
||||
"WeatherSensor",
|
||||
"WeatherStation",
|
||||
"ThermostatWall2",
|
||||
"TemperatureDiffSensor",
|
||||
"TemperatureSensor",
|
||||
"CO2Sensor",
|
||||
"IPSwitchPowermeter",
|
||||
"HMWIOSwitch",
|
||||
"FillingLevel",
|
||||
"ValveDrive",
|
||||
"EcoLogic",
|
||||
"IPThermostatWall",
|
||||
"IPSmoke",
|
||||
"RFSiren",
|
||||
"PresenceIP",
|
||||
"IPAreaThermostat",
|
||||
"IPWeatherSensor",
|
||||
"RotaryHandleSensorIP",
|
||||
"IPPassageSensor",
|
||||
"IPKeySwitchPowermeter",
|
||||
"IPThermostatWall230V",
|
||||
"IPWeatherSensorPlus",
|
||||
"IPWeatherSensorBasic",
|
||||
"IPBrightnessSensor",
|
||||
"IPGarage",
|
||||
"UniversalSensor",
|
||||
"MotionIPV2",
|
||||
"IPMultiIO",
|
||||
"IPThermostatWall2",
|
||||
],
|
||||
DISCOVER_CLIMATE: [
|
||||
"Thermostat",
|
||||
"ThermostatWall",
|
||||
"MAXThermostat",
|
||||
"ThermostatWall2",
|
||||
"MAXWallThermostat",
|
||||
"IPThermostat",
|
||||
"IPThermostatWall",
|
||||
"ThermostatGroup",
|
||||
"IPThermostatWall230V",
|
||||
"IPThermostatWall2",
|
||||
],
|
||||
DISCOVER_BINARY_SENSORS: [
|
||||
"ShutterContact",
|
||||
"Smoke",
|
||||
"SmokeV2",
|
||||
"Motion",
|
||||
"MotionV2",
|
||||
"MotionIP",
|
||||
"RemoteMotion",
|
||||
"WeatherSensor",
|
||||
"TiltSensor",
|
||||
"IPShutterContact",
|
||||
"HMWIOSwitch",
|
||||
"MaxShutterContact",
|
||||
"Rain",
|
||||
"WiredSensor",
|
||||
"PresenceIP",
|
||||
"IPWeatherSensor",
|
||||
"IPPassageSensor",
|
||||
"SmartwareMotion",
|
||||
"IPWeatherSensorPlus",
|
||||
"MotionIPV2",
|
||||
"WaterIP",
|
||||
"IPMultiIO",
|
||||
"TiltIP",
|
||||
"IPShutterContactSabotage",
|
||||
"IPContact",
|
||||
],
|
||||
DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"],
|
||||
DISCOVER_LOCKS: ["KeyMatic"],
|
||||
}
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"]
|
||||
|
||||
HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = {
|
||||
"ACTUAL_TEMPERATURE": [
|
||||
"IPAreaThermostat",
|
||||
"IPWeatherSensor",
|
||||
"IPWeatherSensorPlus",
|
||||
"IPWeatherSensorBasic",
|
||||
"IPThermostatWall",
|
||||
"IPThermostatWall2",
|
||||
]
|
||||
}
|
||||
|
||||
HM_ATTRIBUTE_SUPPORT = {
|
||||
"LOWBAT": ["battery", {0: "High", 1: "Low"}],
|
||||
"LOW_BAT": ["battery", {0: "High", 1: "Low"}],
|
||||
"ERROR": ["error", {0: "No"}],
|
||||
"ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}],
|
||||
"SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}],
|
||||
"RSSI_PEER": ["rssi_peer", {}],
|
||||
"RSSI_DEVICE": ["rssi_device", {}],
|
||||
"VALVE_STATE": ["valve", {}],
|
||||
"LEVEL": ["level", {}],
|
||||
"BATTERY_STATE": ["battery", {}],
|
||||
"CONTROL_MODE": [
|
||||
"mode",
|
||||
{0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"},
|
||||
],
|
||||
"POWER": ["power", {}],
|
||||
"CURRENT": ["current", {}],
|
||||
"VOLTAGE": ["voltage", {}],
|
||||
"OPERATING_VOLTAGE": ["voltage", {}],
|
||||
"WORKING": ["working", {0: "No", 1: "Yes"}],
|
||||
"STATE_UNCERTAIN": ["state_uncertain", {}],
|
||||
}
|
||||
|
||||
HM_PRESS_EVENTS = [
|
||||
"PRESS_SHORT",
|
||||
"PRESS_LONG",
|
||||
"PRESS_CONT",
|
||||
"PRESS_LONG_RELEASE",
|
||||
"PRESS",
|
||||
]
|
||||
|
||||
HM_IMPULSE_EVENTS = ["SEQUENCE_OK"]
|
||||
|
||||
CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False]
|
||||
|
||||
DATA_HOMEMATIC = "homematic"
|
||||
DATA_STORE = "homematic_store"
|
||||
DATA_CONF = "homematic_conf"
|
||||
|
||||
CONF_INTERFACES = "interfaces"
|
||||
CONF_LOCAL_IP = "local_ip"
|
||||
CONF_LOCAL_PORT = "local_port"
|
||||
CONF_PORT = "port"
|
||||
CONF_PATH = "path"
|
||||
CONF_CALLBACK_IP = "callback_ip"
|
||||
CONF_CALLBACK_PORT = "callback_port"
|
||||
CONF_RESOLVENAMES = "resolvenames"
|
||||
CONF_JSONPORT = "jsonport"
|
||||
@@ -6,9 +6,9 @@ from homeassistant.components.cover import (
|
||||
ATTR_TILT_POSITION,
|
||||
CoverDevice,
|
||||
)
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
new_device = HMCover(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMCover(HMDevice, CoverDevice):
|
||||
@@ -68,9 +68,9 @@ class HMCover(HMDevice, CoverDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dictionary (self._data) from metadata."""
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
self._data.update({self._state: None})
|
||||
if "LEVEL_2" in self._hmdevice.WRITENODE:
|
||||
self._data.update({"LEVEL_2": STATE_UNKNOWN})
|
||||
self._data.update({"LEVEL_2": None})
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
|
||||
297
homeassistant/components/homematic/entity.py
Normal file
297
homeassistant/components/homematic/entity.py
Normal file
@@ -0,0 +1,297 @@
|
||||
"""Homematic base entity."""
|
||||
from abc import abstractmethod
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_INTERFACE,
|
||||
ATTR_PARAM,
|
||||
ATTR_UNIQUE_ID,
|
||||
DATA_HOMEMATIC,
|
||||
DOMAIN,
|
||||
HM_ATTRIBUTE_SUPPORT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL_HUB = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
|
||||
|
||||
|
||||
class HMDevice(Entity):
|
||||
"""The HomeMatic device base object."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._interface = config.get(ATTR_INTERFACE)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._unique_id = config.get(ATTR_UNIQUE_ID)
|
||||
self._data = {}
|
||||
self._homematic = None
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
||||
# Set parameter to uppercase
|
||||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Load data init callbacks."""
|
||||
await self.hass.async_add_job(self._subscribe_homematic_events)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID. HomeMatic entity IDs are unique by default."""
|
||||
return self._unique_id.replace(" ", "_")
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true if device is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return device specific state attributes."""
|
||||
attr = {}
|
||||
|
||||
# Generate a dictionary with attributes
|
||||
for node, data in HM_ATTRIBUTE_SUPPORT.items():
|
||||
# Is an attribute and exists for this object
|
||||
if node in self._data:
|
||||
value = data[1].get(self._data[node], self._data[node])
|
||||
attr[data[0]] = value
|
||||
|
||||
# Static attributes
|
||||
attr["id"] = self._hmdevice.ADDRESS
|
||||
attr["interface"] = self._interface
|
||||
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
"""Connect to HomeMatic init values."""
|
||||
if self._connected:
|
||||
return True
|
||||
|
||||
# Initialize
|
||||
self._homematic = self.hass.data[DATA_HOMEMATIC]
|
||||
self._hmdevice = self._homematic.devices[self._interface][self._address]
|
||||
self._connected = True
|
||||
|
||||
try:
|
||||
# Initialize datapoints of this object
|
||||
self._init_data()
|
||||
self._load_data_from_hm()
|
||||
|
||||
# Link events from pyhomematic
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
self._connected = False
|
||||
_LOGGER.error("Exception while linking %s: %s", self._address, str(err))
|
||||
|
||||
def _hm_event_callback(self, device, caller, attribute, value):
|
||||
"""Handle all pyhomematic device events."""
|
||||
_LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value)
|
||||
has_changed = False
|
||||
|
||||
# Is data needed for this instance?
|
||||
if attribute in self._data:
|
||||
# Did data change?
|
||||
if self._data[attribute] != value:
|
||||
self._data[attribute] = value
|
||||
has_changed = True
|
||||
|
||||
# Availability has changed
|
||||
if self.available != (not self._hmdevice.UNREACH):
|
||||
self._available = not self._hmdevice.UNREACH
|
||||
has_changed = True
|
||||
|
||||
# If it has changed data point, update Home Assistant
|
||||
if has_changed:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _subscribe_homematic_events(self):
|
||||
"""Subscribe all required events to handle job."""
|
||||
channels_to_sub = set()
|
||||
|
||||
# Push data to channels_to_sub from hmdevice metadata
|
||||
for metadata in (
|
||||
self._hmdevice.SENSORNODE,
|
||||
self._hmdevice.BINARYNODE,
|
||||
self._hmdevice.ATTRIBUTENODE,
|
||||
self._hmdevice.WRITENODE,
|
||||
self._hmdevice.EVENTNODE,
|
||||
self._hmdevice.ACTIONNODE,
|
||||
):
|
||||
for node, channels in metadata.items():
|
||||
# Data is needed for this instance
|
||||
if node in self._data:
|
||||
# chan is current channel
|
||||
if len(channels) == 1:
|
||||
channel = channels[0]
|
||||
else:
|
||||
channel = self._channel
|
||||
|
||||
# Prepare for subscription
|
||||
try:
|
||||
channels_to_sub.add(int(channel))
|
||||
except (ValueError, TypeError):
|
||||
_LOGGER.error("Invalid channel in metadata from %s", self._name)
|
||||
|
||||
# Set callbacks
|
||||
for channel in channels_to_sub:
|
||||
_LOGGER.debug("Subscribe channel %d from %s", channel, self._name)
|
||||
self._hmdevice.setEventCallback(
|
||||
callback=self._hm_event_callback, bequeath=False, channel=channel
|
||||
)
|
||||
|
||||
def _load_data_from_hm(self):
|
||||
"""Load first value from pyhomematic."""
|
||||
if not self._connected:
|
||||
return False
|
||||
|
||||
# Read data from pyhomematic
|
||||
for metadata, funct in (
|
||||
(self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData),
|
||||
(self._hmdevice.WRITENODE, self._hmdevice.getWriteData),
|
||||
(self._hmdevice.SENSORNODE, self._hmdevice.getSensorData),
|
||||
(self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData),
|
||||
):
|
||||
for node in metadata:
|
||||
if metadata[node] and node in self._data:
|
||||
self._data[node] = funct(name=node, channel=self._channel)
|
||||
|
||||
return True
|
||||
|
||||
def _hm_set_state(self, value):
|
||||
"""Set data to main datapoint."""
|
||||
if self._state in self._data:
|
||||
self._data[self._state] = value
|
||||
|
||||
def _hm_get_state(self):
|
||||
"""Get data from main datapoint."""
|
||||
if self._state in self._data:
|
||||
return self._data[self._state]
|
||||
return None
|
||||
|
||||
def _init_data(self):
|
||||
"""Generate a data dict (self._data) from the HomeMatic metadata."""
|
||||
# Add all attributes to data dictionary
|
||||
for data_note in self._hmdevice.ATTRIBUTENODE:
|
||||
self._data.update({data_note: None})
|
||||
|
||||
# Initialize device specific data
|
||||
self._init_data_struct()
|
||||
|
||||
@abstractmethod
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dictionary from the HomeMatic device metadata."""
|
||||
|
||||
|
||||
class HMHub(Entity):
|
||||
"""The HomeMatic hub. (CCU2/HomeGear)."""
|
||||
|
||||
def __init__(self, hass, homematic, name):
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
|
||||
self._homematic = homematic
|
||||
self._variables = {}
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
# Load data
|
||||
self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB)
|
||||
self.hass.add_job(self._update_hub, None)
|
||||
|
||||
self.hass.helpers.event.track_time_interval(
|
||||
self._update_variables, SCAN_INTERVAL_VARIABLES
|
||||
)
|
||||
self.hass.add_job(self._update_variables, None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. HomeMatic Hub object updates variables."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the entity."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attr = self._variables.copy()
|
||||
return attr
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to use in the frontend, if any."""
|
||||
return "mdi:gradient"
|
||||
|
||||
def _update_hub(self, now):
|
||||
"""Retrieve latest state."""
|
||||
service_message = self._homematic.getServiceMessages(self._name)
|
||||
state = None if service_message is None else len(service_message)
|
||||
|
||||
# state have change?
|
||||
if self._state != state:
|
||||
self._state = state
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_variables(self, now):
|
||||
"""Retrieve all variable data and update hmvariable states."""
|
||||
variables = self._homematic.getAllSystemVariables(self._name)
|
||||
if variables is None:
|
||||
return
|
||||
|
||||
state_change = False
|
||||
for key, value in variables.items():
|
||||
if key in self._variables and value == self._variables[key]:
|
||||
continue
|
||||
|
||||
state_change = True
|
||||
self._variables.update({key: value})
|
||||
|
||||
if state_change:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set_variable(self, name, value):
|
||||
"""Set variable value on CCU/Homegear."""
|
||||
if name not in self._variables:
|
||||
_LOGGER.error("Variable %s not found on %s", name, self.name)
|
||||
return
|
||||
old_value = self._variables.get(name)
|
||||
if isinstance(old_value, bool):
|
||||
value = cv.boolean(value)
|
||||
else:
|
||||
value = float(value)
|
||||
self._homematic.setSystemVariable(self.name, name, value)
|
||||
|
||||
self._variables.update({name: value})
|
||||
self.schedule_update_ha_state()
|
||||
@@ -12,7 +12,8 @@ from homeassistant.components.light import (
|
||||
Light,
|
||||
)
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
new_device = HMLight(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMLight(HMDevice, Light):
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import SUPPORT_OPEN, LockDevice
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
devices.append(HMLock(conf))
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMLock(HMDevice, LockDevice):
|
||||
@@ -44,7 +44,7 @@ class HMLock(HMDevice, LockDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
self._state = "STATE"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
self._data.update({self._state: None})
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "homematic",
|
||||
"name": "Homematic",
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematic",
|
||||
"requirements": ["pyhomematic==0.1.62"],
|
||||
"requirements": ["pyhomematic==0.1.63"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@pvizeli", "@danielperna84"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.notify import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.template as template_helper
|
||||
|
||||
from . import (
|
||||
from .const import (
|
||||
ATTR_ADDRESS,
|
||||
ATTR_CHANNEL,
|
||||
ATTR_INTERFACE,
|
||||
@@ -22,6 +22,7 @@ from . import (
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
|
||||
|
||||
@@ -8,10 +8,10 @@ from homeassistant.const import (
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENERGY_WATT_HOUR,
|
||||
POWER_WATT,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
new_device = HMSensor(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMSensor(HMDevice):
|
||||
@@ -117,6 +117,6 @@ class HMSensor(HMDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dictionary (self._data) from metadata."""
|
||||
if self._state:
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
self._data.update({self._state: None})
|
||||
else:
|
||||
_LOGGER.critical("Unable to initialize sensor: %s", self._name)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import logging
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
from . import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from .const import ATTR_DISCOVER_DEVICES
|
||||
from .entity import HMDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
new_device = HMSwitch(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_entities(devices)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class HMSwitch(HMDevice, SwitchDevice):
|
||||
@@ -55,8 +55,8 @@ class HMSwitch(HMDevice, SwitchDevice):
|
||||
def _init_data_struct(self):
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
self._state = "STATE"
|
||||
self._data.update({self._state: STATE_UNKNOWN})
|
||||
self._data.update({self._state: None})
|
||||
|
||||
# Need sensor values for SwitchPowermeter
|
||||
for node in self._hmdevice.SENSORNODE:
|
||||
self._data.update({node: STATE_UNKNOWN})
|
||||
self._data.update({node: None})
|
||||
|
||||
@@ -13,7 +13,7 @@ from homematicip.aio.device import (
|
||||
AsyncPrintedCircuitBoardSwitch2,
|
||||
AsyncPrintedCircuitBoardSwitchBattery,
|
||||
)
|
||||
from homematicip.aio.group import AsyncSwitchingGroup
|
||||
from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -67,7 +67,7 @@ async def async_setup_entry(
|
||||
entities.append(HomematicipMultiSwitch(hap, device, channel))
|
||||
|
||||
for group in hap.home.groups:
|
||||
if isinstance(group, AsyncSwitchingGroup):
|
||||
if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)):
|
||||
entities.append(HomematicipGroupSwitch(hap, group))
|
||||
|
||||
if entities:
|
||||
|
||||
@@ -36,6 +36,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(
|
||||
CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS
|
||||
): cv.boolean,
|
||||
vol.Optional("filename"): str,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -44,7 +45,13 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BRIDGES): vol.All(
|
||||
cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.All(
|
||||
cv.deprecated("filename", invalidation_version="0.106.0"),
|
||||
BRIDGE_CONFIG_SCHEMA,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -69,7 +76,7 @@ async def async_setup(hass, config):
|
||||
bridges = conf[CONF_BRIDGES]
|
||||
|
||||
configured_hosts = set(
|
||||
entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
)
|
||||
|
||||
for bridge_conf in bridges:
|
||||
|
||||
@@ -297,10 +297,11 @@ class IcloudAccount:
|
||||
self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
|
||||
|
||||
self._family_members_fullname = {}
|
||||
for prs_id, member in user_info["membersInfo"].items():
|
||||
self._family_members_fullname[
|
||||
prs_id
|
||||
] = f"{member['firstName']} {member['lastName']}"
|
||||
if user_info.get("membersInfo") is not None:
|
||||
for prs_id, member in user_info["membersInfo"].items():
|
||||
self._family_members_fullname[
|
||||
prs_id
|
||||
] = f"{member['firstName']} {member['lastName']}"
|
||||
|
||||
self._devices = {}
|
||||
self.update_devices()
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/kef",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@basnijholt"],
|
||||
"requirements": ["aiokef==0.2.2", "getmac==0.8.1"]
|
||||
"requirements": ["aiokef==0.2.5", "getmac==0.8.1"]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ DEFAULT_INVERSE_SPEAKER_MODE = False
|
||||
DOMAIN = "kef"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]}
|
||||
SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"]
|
||||
|
||||
@@ -167,7 +167,7 @@ class MikrotikClient:
|
||||
|
||||
def get_hostname(self):
|
||||
"""Return device host name."""
|
||||
data = self.command(MIKROTIK_SERVICES[IDENTITY])
|
||||
data = list(self.command(MIKROTIK_SERVICES[IDENTITY]))
|
||||
return data[0][NAME] if data else None
|
||||
|
||||
def connected(self):
|
||||
|
||||
@@ -133,8 +133,8 @@ class MpdDevice(MediaPlayerDevice):
|
||||
self._status = self._client.status()
|
||||
self._currentsong = self._client.currentsong()
|
||||
|
||||
position = self._status["time"]
|
||||
if self._media_position != position:
|
||||
position = self._status.get("time")
|
||||
if position is not None and self._media_position != position:
|
||||
self._media_position_updated_at = dt_util.utcnow()
|
||||
self._media_position = position
|
||||
|
||||
|
||||
@@ -39,16 +39,18 @@ class MSTeamsNotificationService(BaseNotificationService):
|
||||
def __init__(self, webhook_url):
|
||||
"""Initialize the service."""
|
||||
self._webhook_url = webhook_url
|
||||
self.teams_message = pymsteams.connectorcard(self._webhook_url)
|
||||
|
||||
def send_message(self, message=None, **kwargs):
|
||||
"""Send a message to the webhook."""
|
||||
|
||||
teams_message = pymsteams.connectorcard(self._webhook_url)
|
||||
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
|
||||
self.teams_message.title(title)
|
||||
teams_message.title(title)
|
||||
|
||||
self.teams_message.text(message)
|
||||
teams_message.text(message)
|
||||
|
||||
if data is not None:
|
||||
file_url = data.get(ATTR_FILE_URL)
|
||||
@@ -60,8 +62,8 @@ class MSTeamsNotificationService(BaseNotificationService):
|
||||
|
||||
message_section = pymsteams.cardsection()
|
||||
message_section.addImage(file_url)
|
||||
self.teams_message.addSection(message_section)
|
||||
teams_message.addSection(message_section)
|
||||
try:
|
||||
self.teams_message.send()
|
||||
teams_message.send()
|
||||
except RuntimeError as err:
|
||||
_LOGGER.error("Could not send notification. Error: %s", err)
|
||||
|
||||
@@ -4,15 +4,17 @@ from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from oauthlib.oauth2 import AccessDeniedError
|
||||
from ring_doorbell import Auth, Ring
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -22,16 +24,8 @@ ATTRIBUTION = "Data provided by Ring.com"
|
||||
NOTIFICATION_ID = "ring_notification"
|
||||
NOTIFICATION_TITLE = "Ring Setup"
|
||||
|
||||
DATA_RING_DOORBELLS = "ring_doorbells"
|
||||
DATA_RING_STICKUP_CAMS = "ring_stickup_cams"
|
||||
DATA_RING_CHIMES = "ring_chimes"
|
||||
DATA_TRACK_INTERVAL = "ring_track_interval"
|
||||
|
||||
DOMAIN = "ring"
|
||||
DEFAULT_ENTITY_NAMESPACE = "ring"
|
||||
SIGNAL_UPDATE_RING = "ring_update"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera")
|
||||
|
||||
@@ -88,51 +82,60 @@ async def async_setup_entry(hass, entry):
|
||||
),
|
||||
).result()
|
||||
|
||||
auth = Auth(entry.data["token"], token_updater)
|
||||
auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater)
|
||||
ring = Ring(auth)
|
||||
|
||||
await hass.async_add_executor_job(finish_setup_entry, hass, ring)
|
||||
try:
|
||||
await hass.async_add_executor_job(ring.update_data)
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Access token is no longer valid. Please set up Ring again")
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"api": ring,
|
||||
"devices": ring.devices(),
|
||||
"device_data": GlobalDataUpdater(
|
||||
hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1)
|
||||
),
|
||||
"dings_data": GlobalDataUpdater(
|
||||
hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10)
|
||||
),
|
||||
"history_data": DeviceDataUpdater(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
ring,
|
||||
lambda device: device.history(limit=10),
|
||||
timedelta(minutes=1),
|
||||
),
|
||||
"health_data": DeviceDataUpdater(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
ring,
|
||||
lambda device: device.update_health_data(),
|
||||
timedelta(minutes=1),
|
||||
),
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
if hass.services.has_service(DOMAIN, "update"):
|
||||
return True
|
||||
|
||||
|
||||
def finish_setup_entry(hass, ring):
|
||||
"""Finish setting up entry."""
|
||||
devices = ring.devices
|
||||
hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"]
|
||||
hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"]
|
||||
hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"]
|
||||
|
||||
ring_devices = chimes + doorbells + stickup_cams
|
||||
|
||||
def service_hub_refresh(service):
|
||||
hub_refresh()
|
||||
|
||||
def timer_hub_refresh(event_time):
|
||||
hub_refresh()
|
||||
|
||||
def hub_refresh():
|
||||
"""Call ring to refresh information."""
|
||||
_LOGGER.debug("Updating Ring Hub component")
|
||||
|
||||
for camera in ring_devices:
|
||||
_LOGGER.debug("Updating camera %s", camera.name)
|
||||
camera.update()
|
||||
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_RING)
|
||||
async def async_refresh_all(_):
|
||||
"""Refresh all ring data."""
|
||||
for info in hass.data[DOMAIN].values():
|
||||
await info["device_data"].async_refresh_all()
|
||||
await info["dings_data"].async_refresh_all()
|
||||
await hass.async_add_executor_job(info["history_data"].refresh_all)
|
||||
await hass.async_add_executor_job(info["health_data"].refresh_all)
|
||||
|
||||
# register service
|
||||
hass.services.register(DOMAIN, "update", service_hub_refresh)
|
||||
hass.services.async_register(DOMAIN, "update", async_refresh_all)
|
||||
|
||||
# register scan interval for ring
|
||||
hass.data[DATA_TRACK_INTERVAL] = track_time_interval(
|
||||
hass, timer_hub_refresh, SCAN_INTERVAL
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
@@ -148,13 +151,146 @@ async def async_unload_entry(hass, entry):
|
||||
if not unload_ok:
|
||||
return False
|
||||
|
||||
await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL])
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
if len(hass.data[DOMAIN]) != 0:
|
||||
return True
|
||||
|
||||
# Last entry unloaded, clean up service
|
||||
hass.services.async_remove(DOMAIN, "update")
|
||||
|
||||
hass.data.pop(DATA_RING_DOORBELLS)
|
||||
hass.data.pop(DATA_RING_STICKUP_CAMS)
|
||||
hass.data.pop(DATA_RING_CHIMES)
|
||||
hass.data.pop(DATA_TRACK_INTERVAL)
|
||||
return True
|
||||
|
||||
return unload_ok
|
||||
|
||||
class GlobalDataUpdater:
|
||||
"""Data storage for single API endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry_id: str,
|
||||
ring: Ring,
|
||||
update_method: str,
|
||||
update_interval: timedelta,
|
||||
):
|
||||
"""Initialize global data updater."""
|
||||
self.hass = hass
|
||||
self.config_entry_id = config_entry_id
|
||||
self.ring = ring
|
||||
self.update_method = update_method
|
||||
self.update_interval = update_interval
|
||||
self.listeners = []
|
||||
self._unsub_interval = None
|
||||
|
||||
@callback
|
||||
def async_add_listener(self, update_callback):
|
||||
"""Listen for data updates."""
|
||||
# This is the first listener, set up interval.
|
||||
if not self.listeners:
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self.hass, self.async_refresh_all, self.update_interval
|
||||
)
|
||||
|
||||
self.listeners.append(update_callback)
|
||||
|
||||
@callback
|
||||
def async_remove_listener(self, update_callback):
|
||||
"""Remove data update."""
|
||||
self.listeners.remove(update_callback)
|
||||
|
||||
if not self.listeners:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
async def async_refresh_all(self, _now: Optional[int] = None) -> None:
|
||||
"""Time to update."""
|
||||
if not self.listeners:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
getattr(self.ring, self.update_method)
|
||||
)
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Ring access token is no longer valid. Set up Ring again")
|
||||
await self.hass.config_entries.async_unload(self.config_entry_id)
|
||||
return
|
||||
|
||||
for update_callback in self.listeners:
|
||||
update_callback()
|
||||
|
||||
|
||||
class DeviceDataUpdater:
|
||||
"""Data storage for device data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry_id: str,
|
||||
ring: Ring,
|
||||
update_method: str,
|
||||
update_interval: timedelta,
|
||||
):
|
||||
"""Initialize device data updater."""
|
||||
self.hass = hass
|
||||
self.config_entry_id = config_entry_id
|
||||
self.ring = ring
|
||||
self.update_method = update_method
|
||||
self.update_interval = update_interval
|
||||
self.devices = {}
|
||||
self._unsub_interval = None
|
||||
|
||||
async def async_track_device(self, device, update_callback):
|
||||
"""Track a device."""
|
||||
if not self.devices:
|
||||
self._unsub_interval = async_track_time_interval(
|
||||
self.hass, self.refresh_all, self.update_interval
|
||||
)
|
||||
|
||||
if device.device_id not in self.devices:
|
||||
self.devices[device.device_id] = {
|
||||
"device": device,
|
||||
"update_callbacks": [update_callback],
|
||||
"data": None,
|
||||
}
|
||||
# Store task so that other concurrent requests can wait for us to finish and
|
||||
# data be available.
|
||||
self.devices[device.device_id]["task"] = asyncio.current_task()
|
||||
self.devices[device.device_id][
|
||||
"data"
|
||||
] = await self.hass.async_add_executor_job(self.update_method, device)
|
||||
self.devices[device.device_id].pop("task")
|
||||
else:
|
||||
self.devices[device.device_id]["update_callbacks"].append(update_callback)
|
||||
# If someone is currently fetching data as part of the initialization, wait for them
|
||||
if "task" in self.devices[device.device_id]:
|
||||
await self.devices[device.device_id]["task"]
|
||||
|
||||
update_callback(self.devices[device.device_id]["data"])
|
||||
|
||||
@callback
|
||||
def async_untrack_device(self, device, update_callback):
|
||||
"""Untrack a device."""
|
||||
self.devices[device.device_id]["update_callbacks"].remove(update_callback)
|
||||
|
||||
if not self.devices[device.device_id]["update_callbacks"]:
|
||||
self.devices.pop(device.device_id)
|
||||
|
||||
if not self.devices:
|
||||
self._unsub_interval()
|
||||
self._unsub_interval = None
|
||||
|
||||
def refresh_all(self, _=None):
|
||||
"""Refresh all registered devices."""
|
||||
for info in self.devices.values():
|
||||
try:
|
||||
data = info["data"] = self.update_method(info["device"])
|
||||
except AccessDeniedError:
|
||||
_LOGGER.error("Ring access token is no longer valid. Set up Ring again")
|
||||
self.hass.add_job(
|
||||
self.hass.config_entries.async_unload(self.config_entry_id)
|
||||
)
|
||||
return
|
||||
|
||||
for update_callback in info["update_callbacks"]:
|
||||
self.hass.loop.call_soon_threadsafe(update_callback, data)
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,44 +14,80 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# Sensor types: Name, category, device_class
|
||||
SENSOR_TYPES = {
|
||||
"ding": ["Ding", ["doorbell"], "occupancy"],
|
||||
"motion": ["Motion", ["doorbell", "stickup_cams"], "motion"],
|
||||
"ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"],
|
||||
"motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Ring binary sensors from a config entry."""
|
||||
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
|
||||
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
|
||||
ring = hass.data[DOMAIN][config_entry.entry_id]["api"]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
sensors = []
|
||||
for device in ring_doorbells: # ring.doorbells is doing I/O
|
||||
|
||||
for device_type in ("doorbots", "authorized_doorbots", "stickup_cams"):
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
if "doorbell" in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingBinarySensor(hass, device, sensor_type))
|
||||
if device_type not in SENSOR_TYPES[sensor_type][1]:
|
||||
continue
|
||||
|
||||
for device in ring_stickup_cams: # ring.stickup_cams is doing I/O
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
if "stickup_cams" in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingBinarySensor(hass, device, sensor_type))
|
||||
for device in devices[device_type]:
|
||||
sensors.append(
|
||||
RingBinarySensor(config_entry.entry_id, ring, device, sensor_type)
|
||||
)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class RingBinarySensor(BinarySensorDevice):
|
||||
class RingBinarySensor(RingEntityMixin, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Ring device."""
|
||||
|
||||
def __init__(self, hass, data, sensor_type):
|
||||
_active_alert = None
|
||||
|
||||
def __init__(self, config_entry_id, ring, device, sensor_type):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__()
|
||||
super().__init__(config_entry_id, device)
|
||||
self._ring = ring
|
||||
self._sensor_type = sensor_type
|
||||
self._data = data
|
||||
self._name = "{0} {1}".format(
|
||||
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]
|
||||
self._device.name, SENSOR_TYPES.get(sensor_type)[0]
|
||||
)
|
||||
self._device_class = SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
self._device_class = SENSOR_TYPES.get(sensor_type)[2]
|
||||
self._state = None
|
||||
self._unique_id = f"{self._data.id}-{self._sensor_type}"
|
||||
self._unique_id = f"{device.id}-{sensor_type}"
|
||||
self._update_alert()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback)
|
||||
self._dings_update_callback()
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self.ring_objects["dings_data"].async_remove_listener(
|
||||
self._dings_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _dings_update_callback(self):
|
||||
"""Call update method."""
|
||||
self._update_alert()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_alert(self):
|
||||
"""Update active alert."""
|
||||
self._active_alert = next(
|
||||
(
|
||||
alert
|
||||
for alert in self._ring.active_alerts()
|
||||
if alert["kind"] == self._sensor_type
|
||||
and alert["doorbot_id"] == self._device.id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -60,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self._state
|
||||
return self._active_alert is not None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -72,39 +109,17 @@ class RingBinarySensor(BinarySensorDevice):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._data.id)},
|
||||
"sw_version": self._data.firmware,
|
||||
"name": self._data.name,
|
||||
"model": self._data.kind,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
attrs = super().device_state_attributes
|
||||
|
||||
attrs["timezone"] = self._data.timezone
|
||||
if self._active_alert is None:
|
||||
return attrs
|
||||
|
||||
if self._data.alert and self._data.alert_expires_at:
|
||||
attrs["expires_at"] = self._data.alert_expires_at
|
||||
attrs["state"] = self._data.alert.get("state")
|
||||
attrs["state"] = self._active_alert["state"]
|
||||
attrs["expires_at"] = datetime.fromtimestamp(
|
||||
self._active_alert.get("now") + self._active_alert.get("expires_in")
|
||||
).isoformat()
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
self._data.check_alerts()
|
||||
|
||||
if self._data.alert:
|
||||
if self._sensor_type == self._data.alert.get(
|
||||
"kind"
|
||||
) and self._data.account_id == self._data.alert.get("doorbot_id"):
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
"""This component provides support to the Ring Door Bell camera."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from itertools import chain
|
||||
import logging
|
||||
|
||||
from haffmpeg.camera import CameraMjpeg
|
||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
||||
import requests
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import (
|
||||
ATTRIBUTION,
|
||||
DATA_RING_DOORBELLS,
|
||||
DATA_RING_STICKUP_CAMS,
|
||||
DOMAIN,
|
||||
SIGNAL_UPDATE_RING,
|
||||
)
|
||||
from . import ATTRIBUTION, DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
FORCE_REFRESH_INTERVAL = timedelta(minutes=45)
|
||||
|
||||
@@ -29,53 +25,63 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a Ring Door Bell and StickUp Camera."""
|
||||
ring_doorbell = hass.data[DATA_RING_DOORBELLS]
|
||||
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
cams = []
|
||||
for camera in ring_doorbell + ring_stickup_cams:
|
||||
for camera in chain(
|
||||
devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"]
|
||||
):
|
||||
if not camera.has_subscription:
|
||||
continue
|
||||
|
||||
camera = await hass.async_add_executor_job(RingCam, hass, camera)
|
||||
cams.append(camera)
|
||||
cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera))
|
||||
|
||||
async_add_entities(cams, True)
|
||||
async_add_entities(cams)
|
||||
|
||||
|
||||
class RingCam(Camera):
|
||||
class RingCam(RingEntityMixin, Camera):
|
||||
"""An implementation of a Ring Door Bell camera."""
|
||||
|
||||
def __init__(self, hass, camera):
|
||||
def __init__(self, config_entry_id, ffmpeg, device):
|
||||
"""Initialize a Ring Door Bell camera."""
|
||||
super().__init__()
|
||||
self._camera = camera
|
||||
self._hass = hass
|
||||
self._name = self._camera.name
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._last_video_id = self._camera.last_recording_id
|
||||
self._video_url = self._camera.recording_url(self._last_video_id)
|
||||
super().__init__(config_entry_id, device)
|
||||
|
||||
self._name = self._device.name
|
||||
self._ffmpeg = ffmpeg
|
||||
self._last_event = None
|
||||
self._last_video_id = None
|
||||
self._video_url = None
|
||||
self._utcnow = dt_util.utcnow()
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
self._disp_disconnect = None
|
||||
self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._disp_disconnect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects["history_data"].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["history_data"].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
def _history_update_callback(self, history_data):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
_LOGGER.debug("Updating Ring camera %s (callback)", self.name)
|
||||
if history_data:
|
||||
self._last_event = history_data[0]
|
||||
self.async_schedule_update_ha_state(True)
|
||||
else:
|
||||
self._last_event = None
|
||||
self._last_video_id = None
|
||||
self._video_url = None
|
||||
self._expires_at = self._utcnow
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -85,32 +91,19 @@ class RingCam(Camera):
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._camera.id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._camera.id)},
|
||||
"sw_version": self._camera.firmware,
|
||||
"name": self._camera.name,
|
||||
"model": self._camera.kind,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
return self._device.id
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
"timezone": self._camera.timezone,
|
||||
"video_url": self._video_url,
|
||||
"last_video_id": self._last_video_id,
|
||||
}
|
||||
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
|
||||
ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
|
||||
|
||||
if self._video_url is None:
|
||||
@@ -123,7 +116,6 @@ class RingCam(Camera):
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
|
||||
if self._video_url is None:
|
||||
return
|
||||
|
||||
@@ -141,34 +133,31 @@ class RingCam(Camera):
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via the hub."""
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
async def async_update(self):
|
||||
"""Update camera entity and refresh attributes."""
|
||||
_LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url")
|
||||
|
||||
self._utcnow = dt_util.utcnow()
|
||||
|
||||
try:
|
||||
last_event = self._camera.history(limit=1)[0]
|
||||
except (IndexError, TypeError):
|
||||
if self._last_event is None:
|
||||
return
|
||||
|
||||
last_recording_id = last_event["id"]
|
||||
video_status = last_event["recording"]["status"]
|
||||
if self._last_event["recording"]["status"] != "ready":
|
||||
return
|
||||
|
||||
if video_status == "ready" and (
|
||||
self._last_video_id != last_recording_id or self._utcnow >= self._expires_at
|
||||
if (
|
||||
self._last_video_id == self._last_event["id"]
|
||||
and self._utcnow <= self._expires_at
|
||||
):
|
||||
return
|
||||
|
||||
video_url = self._camera.recording_url(last_recording_id)
|
||||
if video_url:
|
||||
_LOGGER.info("Ring DoorBell properties refreshed")
|
||||
try:
|
||||
video_url = await self.hass.async_add_executor_job(
|
||||
self._device.recording_url, self._last_event["id"]
|
||||
)
|
||||
except requests.Timeout:
|
||||
_LOGGER.warning(
|
||||
"Time out fetching recording url for camera %s", self.entity_id
|
||||
)
|
||||
video_url = None
|
||||
|
||||
# update attributes if new video or if URL has expired
|
||||
self._last_video_id = last_recording_id
|
||||
self._video_url = video_url
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
if video_url:
|
||||
self._last_video_id = self._last_event["id"]
|
||||
self._video_url = video_url
|
||||
self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow
|
||||
|
||||
@@ -5,7 +5,7 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError
|
||||
from ring_doorbell import Auth
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant import config_entries, const, core, exceptions
|
||||
|
||||
from . import DOMAIN # pylint: disable=unused-import
|
||||
|
||||
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
auth = Auth()
|
||||
auth = Auth(f"HomeAssistant/{const.__version__}")
|
||||
|
||||
try:
|
||||
token = await hass.async_add_executor_job(
|
||||
@@ -39,9 +39,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
|
||||
53
homeassistant/components/ring/entity.py
Normal file
53
homeassistant/components/ring/entity.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Base class for Ring entity."""
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import ATTRIBUTION, DOMAIN
|
||||
|
||||
|
||||
class RingEntityMixin:
|
||||
"""Base implementation for Ring device."""
|
||||
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__()
|
||||
self._config_entry_id = config_entry_id
|
||||
self._device = device
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.ring_objects["device_data"].async_add_listener(self._update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
self.ring_objects["device_data"].async_remove_listener(self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def ring_objects(self):
|
||||
"""Return the Ring API objects."""
|
||||
return self.hass.data[DOMAIN][self._config_entry_id]
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via the hub."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {ATTR_ATTRIBUTION: ATTRIBUTION}
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.device_id)},
|
||||
"name": self._device.name,
|
||||
"model": self._device.model,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
@@ -4,10 +4,10 @@ import logging
|
||||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,44 +25,35 @@ OFF_STATE = "off"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the lights for the Ring devices."""
|
||||
cameras = hass.data[DATA_RING_STICKUP_CAMS]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
lights = []
|
||||
|
||||
for device in cameras:
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("light"):
|
||||
lights.append(RingLight(device))
|
||||
lights.append(RingLight(config_entry.entry_id, device))
|
||||
|
||||
async_add_entities(lights, True)
|
||||
async_add_entities(lights)
|
||||
|
||||
|
||||
class RingLight(Light):
|
||||
class RingLight(RingEntityMixin, Light):
|
||||
"""Creates a switch to turn the ring cameras light on and off."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize the light."""
|
||||
self._device = device
|
||||
self._unique_id = self._device.id
|
||||
self._light_on = False
|
||||
super().__init__(config_entry_id, device)
|
||||
self._unique_id = device.id
|
||||
self._light_on = device.lights == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
self._disp_disconnect = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._disp_disconnect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
_LOGGER.debug("Updating Ring light %s (callback)", self.name)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
self._light_on = self._device.lights == ON_STATE
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -74,33 +65,17 @@ class RingLight(Light):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update controlled via the hub."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""If the switch is currently on or off."""
|
||||
return self._light_on
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.id)},
|
||||
"sw_version": self._device.firmware,
|
||||
"name": self._device.name,
|
||||
"model": self._device.kind,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
def _set_light(self, new_state):
|
||||
"""Update light state, and causes Home Assistant to correctly update."""
|
||||
self._device.lights = new_state
|
||||
self._light_on = new_state == ON_STATE
|
||||
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the light on for 30 seconds."""
|
||||
@@ -109,11 +84,3 @@ class RingLight(Light):
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._set_light(OFF_STATE)
|
||||
|
||||
def update(self):
|
||||
"""Update current state of the light."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
_LOGGER.debug("Skipping update...")
|
||||
return
|
||||
|
||||
self._light_on = self._device.lights == ON_STATE
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"domain": "ring",
|
||||
"name": "Ring",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ring",
|
||||
"requirements": ["ring_doorbell==0.5.0"],
|
||||
"requirements": ["ring_doorbell==0.6.0"],
|
||||
"dependencies": ["ffmpeg"],
|
||||
"codeowners": [],
|
||||
"codeowners": ["@balloob"],
|
||||
"config_flow": true
|
||||
}
|
||||
|
||||
@@ -1,126 +1,60 @@
|
||||
"""This component provides HA sensor support for Ring Door Bell/Chimes."""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
from . import (
|
||||
ATTRIBUTION,
|
||||
DATA_RING_CHIMES,
|
||||
DATA_RING_DOORBELLS,
|
||||
DATA_RING_STICKUP_CAMS,
|
||||
DOMAIN,
|
||||
SIGNAL_UPDATE_RING,
|
||||
)
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Sensor types: Name, category, units, icon, kind
|
||||
SENSOR_TYPES = {
|
||||
"battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None],
|
||||
"last_activity": [
|
||||
"Last Activity",
|
||||
["doorbell", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
None,
|
||||
],
|
||||
"last_ding": ["Last Ding", ["doorbell"], None, "history", "ding"],
|
||||
"last_motion": [
|
||||
"Last Motion",
|
||||
["doorbell", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
"motion",
|
||||
],
|
||||
"volume": [
|
||||
"Volume",
|
||||
["chime", "doorbell", "stickup_cams"],
|
||||
None,
|
||||
"bell-ring",
|
||||
None,
|
||||
],
|
||||
"wifi_signal_category": [
|
||||
"WiFi Signal Category",
|
||||
["chime", "doorbell", "stickup_cams"],
|
||||
None,
|
||||
"wifi",
|
||||
None,
|
||||
],
|
||||
"wifi_signal_strength": [
|
||||
"WiFi Signal Strength",
|
||||
["chime", "doorbell", "stickup_cams"],
|
||||
"dBm",
|
||||
"wifi",
|
||||
None,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up a sensor for a Ring device."""
|
||||
ring_chimes = hass.data[DATA_RING_CHIMES]
|
||||
ring_doorbells = hass.data[DATA_RING_DOORBELLS]
|
||||
ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
|
||||
# Makes a ton of requests. We will make this a config entry option in the future
|
||||
wifi_enabled = False
|
||||
|
||||
sensors = []
|
||||
for device in ring_chimes:
|
||||
|
||||
for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"):
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
if "chime" in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingSensor(hass, device, sensor_type))
|
||||
if device_type not in SENSOR_TYPES[sensor_type][1]:
|
||||
continue
|
||||
|
||||
for device in ring_doorbells:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
if "doorbell" in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingSensor(hass, device, sensor_type))
|
||||
if not wifi_enabled and sensor_type.startswith("wifi_"):
|
||||
continue
|
||||
|
||||
for device in ring_stickup_cams:
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
if "stickup_cams" in SENSOR_TYPES[sensor_type][1]:
|
||||
sensors.append(RingSensor(hass, device, sensor_type))
|
||||
for device in devices[device_type]:
|
||||
if device_type == "battery" and device.battery_life is None:
|
||||
continue
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
sensors.append(
|
||||
SENSOR_TYPES[sensor_type][6](
|
||||
config_entry.entry_id, device, sensor_type
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class RingSensor(Entity):
|
||||
class RingSensor(RingEntityMixin, Entity):
|
||||
"""A sensor implementation for Ring device."""
|
||||
|
||||
def __init__(self, hass, data, sensor_type):
|
||||
def __init__(self, config_entry_id, device, sensor_type):
|
||||
"""Initialize a sensor for Ring device."""
|
||||
super().__init__()
|
||||
super().__init__(config_entry_id, device)
|
||||
self._sensor_type = sensor_type
|
||||
self._data = data
|
||||
self._extra = None
|
||||
self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3])
|
||||
self._kind = SENSOR_TYPES.get(self._sensor_type)[4]
|
||||
self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3])
|
||||
self._kind = SENSOR_TYPES.get(sensor_type)[4]
|
||||
self._name = "{0} {1}".format(
|
||||
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0]
|
||||
self._device.name, SENSOR_TYPES.get(sensor_type)[0]
|
||||
)
|
||||
self._state = None
|
||||
self._tz = str(hass.config.time_zone)
|
||||
self._unique_id = f"{self._data.id}-{self._sensor_type}"
|
||||
self._disp_disconnect = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._disp_disconnect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
||||
)
|
||||
await self.hass.async_add_executor_job(self._data.update)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
self._unique_id = f"{device.id}-{sensor_type}"
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@@ -135,7 +69,11 @@ class RingSensor(Entity):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
if self._sensor_type == "volume":
|
||||
return self._device.volume
|
||||
|
||||
if self._sensor_type == "battery":
|
||||
return self._device.battery_life
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@@ -143,39 +81,16 @@ class RingSensor(Entity):
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._data.id)},
|
||||
"sw_version": self._data.firmware,
|
||||
"name": self._data.name,
|
||||
"model": self._data.kind,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
|
||||
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||
attrs["timezone"] = self._data.timezone
|
||||
attrs["wifi_name"] = self._data.wifi_name
|
||||
|
||||
if self._extra and self._sensor_type.startswith("last_"):
|
||||
attrs["created_at"] = self._extra["created_at"]
|
||||
attrs["answered"] = self._extra["answered"]
|
||||
attrs["recording_status"] = self._extra["recording"]["status"]
|
||||
attrs["category"] = self._extra["kind"]
|
||||
|
||||
return attrs
|
||||
def device_class(self):
|
||||
"""Return sensor device class."""
|
||||
return SENSOR_TYPES[self._sensor_type][5]
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
if self._sensor_type == "battery" and self._state is not None:
|
||||
if self._sensor_type == "battery" and self._device.battery_life is not None:
|
||||
return icon_for_battery_level(
|
||||
battery_level=int(self._state), charging=False
|
||||
battery_level=self._device.battery_life, charging=False
|
||||
)
|
||||
return self._icon
|
||||
|
||||
@@ -184,29 +99,168 @@ class RingSensor(Entity):
|
||||
"""Return the units of measurement."""
|
||||
return SENSOR_TYPES.get(self._sensor_type)[2]
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Updating data from %s sensor", self._name)
|
||||
|
||||
if self._sensor_type == "volume":
|
||||
self._state = self._data.volume
|
||||
class HealthDataRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on health data."""
|
||||
|
||||
if self._sensor_type == "battery":
|
||||
self._state = self._data.battery_life
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if self._sensor_type.startswith("last_"):
|
||||
history = self._data.history(
|
||||
limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True
|
||||
)
|
||||
if history:
|
||||
self._extra = history[0]
|
||||
created_at = self._extra["created_at"]
|
||||
self._state = "{0:0>2}:{1:0>2}".format(
|
||||
created_at.hour, created_at.minute
|
||||
)
|
||||
await self.ring_objects["health_data"].async_track_device(
|
||||
self._device, self._health_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["health_data"].async_untrack_device(
|
||||
self._device, self._health_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _health_update_callback(self, _health_data):
|
||||
"""Call update method."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._sensor_type == "wifi_signal_category":
|
||||
self._state = self._data.wifi_signal_category
|
||||
return self._device.wifi_signal_category
|
||||
|
||||
if self._sensor_type == "wifi_signal_strength":
|
||||
self._state = self._data.wifi_signal_strength
|
||||
return self._device.wifi_signal_strength
|
||||
|
||||
|
||||
class HistoryRingSensor(RingSensor):
|
||||
"""Ring sensor that relies on history data."""
|
||||
|
||||
_latest_event = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
await self.ring_objects["history_data"].async_track_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self.ring_objects["history_data"].async_untrack_device(
|
||||
self._device, self._history_update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _history_update_callback(self, history_data):
|
||||
"""Call update method."""
|
||||
if not history_data:
|
||||
return
|
||||
|
||||
found = None
|
||||
if self._kind is None:
|
||||
found = history_data[0]
|
||||
else:
|
||||
for entry in history_data:
|
||||
if entry["kind"] == self._kind:
|
||||
found = entry
|
||||
break
|
||||
|
||||
if not found:
|
||||
return
|
||||
|
||||
self._latest_event = found
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
if self._latest_event is None:
|
||||
return None
|
||||
|
||||
return self._latest_event["created_at"].isoformat()
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = super().device_state_attributes
|
||||
|
||||
if self._latest_event:
|
||||
attrs["created_at"] = self._latest_event["created_at"]
|
||||
attrs["answered"] = self._latest_event["answered"]
|
||||
attrs["recording_status"] = self._latest_event["recording"]["status"]
|
||||
attrs["category"] = self._latest_event["kind"]
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
# Sensor types: Name, category, units, icon, kind, device_class, class
|
||||
SENSOR_TYPES = {
|
||||
"battery": [
|
||||
"Battery",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
"%",
|
||||
None,
|
||||
None,
|
||||
"battery",
|
||||
RingSensor,
|
||||
],
|
||||
"last_activity": [
|
||||
"Last Activity",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
None,
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"last_ding": [
|
||||
"Last Ding",
|
||||
["doorbots", "authorized_doorbots"],
|
||||
None,
|
||||
"history",
|
||||
"ding",
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"last_motion": [
|
||||
"Last Motion",
|
||||
["doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"history",
|
||||
"motion",
|
||||
"timestamp",
|
||||
HistoryRingSensor,
|
||||
],
|
||||
"volume": [
|
||||
"Volume",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"bell-ring",
|
||||
None,
|
||||
None,
|
||||
RingSensor,
|
||||
],
|
||||
"wifi_signal_category": [
|
||||
"WiFi Signal Category",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
None,
|
||||
"wifi",
|
||||
None,
|
||||
None,
|
||||
HealthDataRingSensor,
|
||||
],
|
||||
"wifi_signal_strength": [
|
||||
"WiFi Signal Strength",
|
||||
["chimes", "doorbots", "authorized_doorbots", "stickup_cams"],
|
||||
"dBm",
|
||||
"wifi",
|
||||
None,
|
||||
"signal_strength",
|
||||
HealthDataRingSensor,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import logging
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING
|
||||
from . import DOMAIN
|
||||
from .entity import RingEntityMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -24,42 +24,24 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Create the switches for the Ring devices."""
|
||||
cameras = hass.data[DATA_RING_STICKUP_CAMS]
|
||||
devices = hass.data[DOMAIN][config_entry.entry_id]["devices"]
|
||||
switches = []
|
||||
for device in cameras:
|
||||
|
||||
for device in devices["stickup_cams"]:
|
||||
if device.has_capability("siren"):
|
||||
switches.append(SirenSwitch(device))
|
||||
switches.append(SirenSwitch(config_entry.entry_id, device))
|
||||
|
||||
async_add_entities(switches, True)
|
||||
async_add_entities(switches)
|
||||
|
||||
|
||||
class BaseRingSwitch(SwitchDevice):
|
||||
class BaseRingSwitch(RingEntityMixin, SwitchDevice):
|
||||
"""Represents a switch for controlling an aspect of a ring device."""
|
||||
|
||||
def __init__(self, device, device_type):
|
||||
def __init__(self, config_entry_id, device, device_type):
|
||||
"""Initialize the switch."""
|
||||
self._device = device
|
||||
super().__init__(config_entry_id, device)
|
||||
self._device_type = device_type
|
||||
self._unique_id = f"{self._device.id}-{self._device_type}"
|
||||
self._disp_disconnect = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self._disp_disconnect = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_RING, self._update_callback
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Disconnect callbacks."""
|
||||
if self._disp_disconnect:
|
||||
self._disp_disconnect()
|
||||
self._disp_disconnect = None
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
_LOGGER.debug("Updating Ring sensor %s (callback)", self.name)
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@@ -71,31 +53,24 @@ class BaseRingSwitch(SwitchDevice):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Update controlled via the hub."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.id)},
|
||||
"sw_version": self._device.firmware,
|
||||
"name": self._device.name,
|
||||
"model": self._device.kind,
|
||||
"manufacturer": "Ring",
|
||||
}
|
||||
|
||||
|
||||
class SirenSwitch(BaseRingSwitch):
|
||||
"""Creates a switch to turn the ring cameras siren on and off."""
|
||||
|
||||
def __init__(self, device):
|
||||
def __init__(self, config_entry_id, device):
|
||||
"""Initialize the switch for a device with a siren."""
|
||||
super().__init__(device, "siren")
|
||||
super().__init__(config_entry_id, device, "siren")
|
||||
self._no_updates_until = dt_util.utcnow()
|
||||
self._siren_on = False
|
||||
self._siren_on = device.siren > 0
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
"""Call update method."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
return
|
||||
|
||||
self._siren_on = self._device.siren > 0
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _set_switch(self, new_state):
|
||||
"""Update switch state, and causes Home Assistant to correctly update."""
|
||||
@@ -121,10 +96,3 @@ class SirenSwitch(BaseRingSwitch):
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return SIREN_ICON
|
||||
|
||||
def update(self):
|
||||
"""Update state of the siren."""
|
||||
if self._no_updates_until > dt_util.utcnow():
|
||||
_LOGGER.debug("Skipping update...")
|
||||
return
|
||||
self._siren_on = self._device.siren > 0
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Tesla",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla",
|
||||
"requirements": ["teslajsonpy==0.2.2"],
|
||||
"requirements": ["teslajsonpy==0.2.3"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@zabuldon", "@alandtse"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "webostv",
|
||||
"name": "LG webOS Smart TV",
|
||||
"documentation": "https://www.home-assistant.io/integrations/webostv",
|
||||
"requirements": ["aiopylgtv==0.2.4"],
|
||||
"requirements": ["aiopylgtv==0.2.7"],
|
||||
"dependencies": ["configurator"],
|
||||
"codeowners": ["@bendavid"]
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice):
|
||||
partial_match_channel_id = None
|
||||
perfect_match_channel_id = None
|
||||
|
||||
for channel in self._client.get_channels():
|
||||
for channel in await self._client.get_channels():
|
||||
if media_id == channel["channelNumber"]:
|
||||
perfect_match_channel_id = channel["channelId"]
|
||||
continue
|
||||
|
||||
@@ -56,6 +56,7 @@ CHANNEL_HUMIDITY = "humidity"
|
||||
CHANNEL_IAS_WD = "ias_wd"
|
||||
CHANNEL_ILLUMINANCE = "illuminance"
|
||||
CHANNEL_LEVEL = ATTR_LEVEL
|
||||
CHANNEL_MULTISTATE_INPUT = "multistate_input"
|
||||
CHANNEL_OCCUPANCY = "occupancy"
|
||||
CHANNEL_ON_OFF = "on_off"
|
||||
CHANNEL_POWER_CONFIGURATION = "power"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"requirements": [
|
||||
"bellows-homeassistant==0.12.0",
|
||||
"zha-quirks==0.0.30",
|
||||
"zha-quirks==0.0.31",
|
||||
"zigpy-deconz==0.7.0",
|
||||
"zigpy-homeassistant==0.12.0",
|
||||
"zigpy-xbee-homeassistant==0.8.0",
|
||||
|
||||
@@ -26,6 +26,7 @@ from .core.const import (
|
||||
CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||
CHANNEL_HUMIDITY,
|
||||
CHANNEL_ILLUMINANCE,
|
||||
CHANNEL_MULTISTATE_INPUT,
|
||||
CHANNEL_POWER_CONFIGURATION,
|
||||
CHANNEL_PRESSURE,
|
||||
CHANNEL_SMARTENERGY_METERING,
|
||||
@@ -227,6 +228,18 @@ class ElectricalMeasurement(Sensor):
|
||||
return round(value * self._channel.multiplier / self._channel.divisor)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_MULTISTATE_INPUT)
|
||||
class Text(Sensor):
|
||||
"""Sensor that displays string values."""
|
||||
|
||||
_device_class = None
|
||||
_unit = None
|
||||
|
||||
def formatter(self, value) -> str:
|
||||
"""Return string value."""
|
||||
return value
|
||||
|
||||
|
||||
@STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER)
|
||||
@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY)
|
||||
class Humidity(Sensor):
|
||||
|
||||
@@ -79,14 +79,12 @@ CONF_REFRESH_DELAY = "delay"
|
||||
CONF_DEVICE_CONFIG = "device_config"
|
||||
CONF_DEVICE_CONFIG_GLOB = "device_config_glob"
|
||||
CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain"
|
||||
CONF_TILT_OPEN_POSITION = "tilt_open_position"
|
||||
|
||||
DEFAULT_CONF_IGNORED = False
|
||||
DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False
|
||||
DEFAULT_CONF_INVERT_PERCENT = False
|
||||
DEFAULT_CONF_REFRESH_VALUE = False
|
||||
DEFAULT_CONF_REFRESH_DELAY = 5
|
||||
DEFAULT_CONF_TILT_OPEN_POSITION = 50
|
||||
|
||||
SUPPORTED_PLATFORMS = [
|
||||
"binary_sensor",
|
||||
@@ -216,9 +214,6 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema(
|
||||
vol.Optional(
|
||||
CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY
|
||||
): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_TILT_OPEN_POSITION, default=DEFAULT_CONF_TILT_OPEN_POSITION
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
ATTR_TILT_POSITION,
|
||||
DOMAIN,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
@@ -15,13 +14,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from . import (
|
||||
CONF_INVERT_OPENCLOSE_BUTTONS,
|
||||
CONF_INVERT_PERCENT,
|
||||
CONF_TILT_OPEN_POSITION,
|
||||
ZWaveDeviceEntity,
|
||||
workaround,
|
||||
)
|
||||
from .const import (
|
||||
COMMAND_CLASS_BARRIER_OPERATOR,
|
||||
COMMAND_CLASS_MANUFACTURER_PROPRIETARY,
|
||||
COMMAND_CLASS_SWITCH_BINARY,
|
||||
COMMAND_CLASS_SWITCH_MULTILEVEL,
|
||||
DATA_NETWORK,
|
||||
@@ -32,23 +29,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE
|
||||
|
||||
|
||||
def _to_hex_str(id_in_bytes):
|
||||
"""Convert a two byte value to a hex string.
|
||||
|
||||
Example: 0x1234 --> '0x1234'
|
||||
"""
|
||||
return f"0x{id_in_bytes:04x}"
|
||||
|
||||
|
||||
# For some reason node.manufacturer_id is of type string. So we need to convert
|
||||
# the values.
|
||||
FIBARO = _to_hex_str(workaround.FIBARO)
|
||||
FIBARO222_SHUTTERS = [
|
||||
_to_hex_str(workaround.FGR222_SHUTTER2),
|
||||
_to_hex_str(workaround.FGRM222_SHUTTER2),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old method of setting up Z-Wave covers."""
|
||||
pass
|
||||
@@ -73,17 +53,6 @@ def get_device(hass, values, node_config, **kwargs):
|
||||
values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL
|
||||
and values.primary.index == 0
|
||||
):
|
||||
if (
|
||||
values.primary.node.manufacturer_id == FIBARO
|
||||
and values.primary.node.product_type in FIBARO222_SHUTTERS
|
||||
):
|
||||
return FibaroFGRM222(
|
||||
hass,
|
||||
values,
|
||||
invert_buttons,
|
||||
invert_percent,
|
||||
node_config.get(CONF_TILT_OPEN_POSITION),
|
||||
)
|
||||
return ZwaveRollershutter(hass, values, invert_buttons, invert_percent)
|
||||
if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY:
|
||||
return ZwaveGarageDoorSwitch(values)
|
||||
@@ -243,116 +212,3 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the garage door."""
|
||||
self.values.primary.data = "Opened"
|
||||
|
||||
|
||||
class FibaroFGRM222(ZwaveRollershutter):
|
||||
"""Implementation of proprietary features for Fibaro FGR-222 / FGRM-222.
|
||||
|
||||
This adds support for the tilt feature for the ventian blind mode.
|
||||
To enable this you need to configure the devices to use the venetian blind
|
||||
mode and to enable the proprietary command class:
|
||||
* Set "3: Reports type to Blind position reports sent"
|
||||
to value "the main controller using Fibaro Command Class"
|
||||
* Set "10: Roller Shutter operating modes"
|
||||
to value "2 - Venetian Blind Mode, with positioning"
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, hass, values, invert_buttons, invert_percent, open_tilt_position: int
|
||||
):
|
||||
"""Initialize the FGRM-222."""
|
||||
self._value_blinds = None
|
||||
self._value_tilt = None
|
||||
self._has_tilt_mode = False # type: bool
|
||||
self._open_tilt_position = 50 # type: int
|
||||
if open_tilt_position is not None:
|
||||
self._open_tilt_position = open_tilt_position
|
||||
super().__init__(hass, values, invert_buttons, invert_percent)
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self) -> int:
|
||||
"""Get the tilt of the blinds.
|
||||
|
||||
Saturate values <5 and >94 so that it's easier to detect the end
|
||||
positions in automations.
|
||||
"""
|
||||
if not self._has_tilt_mode:
|
||||
return None
|
||||
if self._value_tilt.data <= 5:
|
||||
return 0
|
||||
if self._value_tilt.data >= 95:
|
||||
return 100
|
||||
return self._value_tilt.data
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if not self._has_tilt_mode:
|
||||
_LOGGER.error("Can't set cover tilt as device is not yet set up.")
|
||||
else:
|
||||
# Limit the range to [0-99], as this what that the ZWave command
|
||||
# accepts.
|
||||
tilt_position = max(0, min(99, kwargs.get(ATTR_TILT_POSITION)))
|
||||
_LOGGER.debug("setting tilt to %d", tilt_position)
|
||||
self._value_tilt.data = tilt_position
|
||||
|
||||
def open_cover_tilt(self, **kwargs):
|
||||
"""Set slats to horizontal position."""
|
||||
self.set_cover_tilt_position(tilt_position=self._open_tilt_position)
|
||||
|
||||
def close_cover_tilt(self, **kwargs):
|
||||
"""Close the slats."""
|
||||
self.set_cover_tilt_position(tilt_position=0)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the roller shutter to a specific position.
|
||||
|
||||
If the venetian blinds mode is not activated, fall back to
|
||||
the behavior of the parent class.
|
||||
"""
|
||||
if not self._has_tilt_mode:
|
||||
super().set_cover_position(**kwargs)
|
||||
else:
|
||||
_LOGGER.debug("Setting cover position to %s", kwargs.get(ATTR_POSITION))
|
||||
self._value_blinds.data = kwargs.get(ATTR_POSITION)
|
||||
|
||||
def _configure_values(self):
|
||||
"""Get the value objects from the node."""
|
||||
for value in self.node.get_values(
|
||||
class_id=COMMAND_CLASS_MANUFACTURER_PROPRIETARY
|
||||
).values():
|
||||
if value is None:
|
||||
continue
|
||||
if value.index == 0:
|
||||
self._value_blinds = value
|
||||
elif value.index == 1:
|
||||
self._value_tilt = value
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Undefined index %d for this command class", value.index
|
||||
)
|
||||
|
||||
if self._value_tilt is not None:
|
||||
# We reached here because the user has configured the Fibaro to
|
||||
# report using the MANUFACTURER_PROPRIETARY command class. The only
|
||||
# reason for the user to configure this way is if tilt support is
|
||||
# needed (aka venetian blind mode). Therefore, turn it on.
|
||||
#
|
||||
# Note: This is safe to do even if the user has accidentally set
|
||||
# this configuration parameter, or configuration parameter 10 to
|
||||
# something other than venetian blind mode. The controller will just
|
||||
# ignore potential tilt settings sent from Home Assistant in this
|
||||
# case.
|
||||
self._has_tilt_mode = True
|
||||
_LOGGER.info(
|
||||
"Zwave node %s is a Fibaro FGR-222/FGRM-222 with tilt support.",
|
||||
self.node_id,
|
||||
)
|
||||
|
||||
def update_properties(self):
|
||||
"""React on properties being updated."""
|
||||
if not self._has_tilt_mode:
|
||||
self._configure_values()
|
||||
if self._value_blinds is not None:
|
||||
self._current_position = self._value_blinds.data
|
||||
else:
|
||||
super().update_properties()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 104
|
||||
PATCH_VERSION = "0b4"
|
||||
PATCH_VERSION = "2"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
||||
@@ -259,10 +259,21 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta):
|
||||
"""
|
||||
return self.async_create_entry(title=self.flow_impl.name, data=data)
|
||||
|
||||
async def async_step_discovery(self, user_input: dict = None) -> dict:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self.async_set_unique_id(self.DOMAIN)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
assert self.hass is not None
|
||||
if self.hass.config_entries.async_entries(self.DOMAIN):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return await self.async_step_pick_implementation()
|
||||
|
||||
async_step_user = async_step_pick_implementation
|
||||
async_step_ssdp = async_step_pick_implementation
|
||||
async_step_zeroconf = async_step_pick_implementation
|
||||
async_step_homekit = async_step_pick_implementation
|
||||
async_step_ssdp = async_step_discovery
|
||||
async_step_zeroconf = async_step_discovery
|
||||
async_step_homekit = async_step_discovery
|
||||
|
||||
@classmethod
|
||||
def async_register_implementation(
|
||||
|
||||
@@ -146,7 +146,8 @@ class EntityPlatform:
|
||||
warn_task = hass.loop.call_later(
|
||||
SLOW_SETUP_WARNING,
|
||||
logger.warning,
|
||||
"Setup of platform %s is taking over %s seconds.",
|
||||
"Setup of %s platform %s is taking over %s seconds.",
|
||||
self.domain,
|
||||
self.platform_name,
|
||||
SLOW_SETUP_WARNING,
|
||||
)
|
||||
@@ -354,6 +355,7 @@ class EntityPlatform:
|
||||
capabilities=entity.capability_attributes,
|
||||
supported_features=entity.supported_features,
|
||||
device_class=entity.device_class,
|
||||
unit_of_measurement=entity.unit_of_measurement,
|
||||
)
|
||||
|
||||
entity.registry_entry = entry
|
||||
|
||||
@@ -18,6 +18,7 @@ import attr
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
@@ -77,6 +78,7 @@ class RegistryEntry:
|
||||
capabilities: Optional[Dict[str, Any]] = attr.ib(default=None)
|
||||
supported_features: int = attr.ib(default=0)
|
||||
device_class: Optional[str] = attr.ib(default=None)
|
||||
unit_of_measurement: Optional[str] = attr.ib(default=None)
|
||||
domain = attr.ib(type=str, init=False, repr=False)
|
||||
|
||||
@domain.default
|
||||
@@ -164,6 +166,7 @@ class EntityRegistry:
|
||||
capabilities: Optional[Dict[str, Any]] = None,
|
||||
supported_features: Optional[int] = None,
|
||||
device_class: Optional[str] = None,
|
||||
unit_of_measurement: Optional[str] = None,
|
||||
) -> RegistryEntry:
|
||||
"""Get entity. Create if it doesn't exist."""
|
||||
config_entry_id = None
|
||||
@@ -180,6 +183,7 @@ class EntityRegistry:
|
||||
capabilities=capabilities or _UNDEF,
|
||||
supported_features=supported_features or _UNDEF,
|
||||
device_class=device_class or _UNDEF,
|
||||
unit_of_measurement=unit_of_measurement or _UNDEF,
|
||||
# When we changed our slugify algorithm, we invalidated some
|
||||
# stored entity IDs with either a __ or ending in _.
|
||||
# Fix introduced in 0.86 (Jan 23, 2019). Next line can be
|
||||
@@ -210,6 +214,7 @@ class EntityRegistry:
|
||||
capabilities=capabilities,
|
||||
supported_features=supported_features or 0,
|
||||
device_class=device_class,
|
||||
unit_of_measurement=unit_of_measurement,
|
||||
)
|
||||
self.entities[entity_id] = entity
|
||||
_LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id)
|
||||
@@ -279,6 +284,7 @@ class EntityRegistry:
|
||||
capabilities=_UNDEF,
|
||||
supported_features=_UNDEF,
|
||||
device_class=_UNDEF,
|
||||
unit_of_measurement=_UNDEF,
|
||||
):
|
||||
"""Private facing update properties method."""
|
||||
old = self.entities[entity_id]
|
||||
@@ -293,6 +299,7 @@ class EntityRegistry:
|
||||
("capabilities", capabilities),
|
||||
("supported_features", supported_features),
|
||||
("device_class", device_class),
|
||||
("unit_of_measurement", unit_of_measurement),
|
||||
):
|
||||
if value is not _UNDEF and value != getattr(old, attr_name):
|
||||
changes[attr_name] = value
|
||||
@@ -369,6 +376,7 @@ class EntityRegistry:
|
||||
capabilities=entity.get("capabilities") or {},
|
||||
supported_features=entity.get("supported_features", 0),
|
||||
device_class=entity.get("device_class"),
|
||||
unit_of_measurement=entity.get("unit_of_measurement"),
|
||||
)
|
||||
|
||||
self.entities = entities
|
||||
@@ -395,6 +403,7 @@ class EntityRegistry:
|
||||
"capabilities": entry.capabilities,
|
||||
"supported_features": entry.supported_features,
|
||||
"device_class": entry.device_class,
|
||||
"unit_of_measurement": entry.unit_of_measurement,
|
||||
}
|
||||
for entry in self.entities.values()
|
||||
]
|
||||
@@ -499,6 +508,9 @@ def async_setup_entity_restore(
|
||||
if entry.device_class is not None:
|
||||
attrs[ATTR_DEVICE_CLASS] = entry.device_class
|
||||
|
||||
if entry.unit_of_measurement is not None:
|
||||
attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement
|
||||
|
||||
states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs)
|
||||
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states)
|
||||
|
||||
@@ -11,7 +11,7 @@ cryptography==2.8
|
||||
defusedxml==0.6.0
|
||||
distro==1.4.0
|
||||
hass-nabucasa==0.31
|
||||
home-assistant-frontend==20200108.0
|
||||
home-assistant-frontend==20200108.2
|
||||
importlib-metadata==1.3.0
|
||||
jinja2>=2.10.3
|
||||
netdisco==2.6.0
|
||||
|
||||
@@ -172,7 +172,7 @@ aioimaplib==0.7.15
|
||||
aiokafka==0.5.1
|
||||
|
||||
# homeassistant.components.kef
|
||||
aiokef==0.2.2
|
||||
aiokef==0.2.5
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx==0.6.7
|
||||
@@ -190,7 +190,7 @@ aionotion==1.1.0
|
||||
aiopvapi==1.6.14
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiopylgtv==0.2.4
|
||||
aiopylgtv==0.2.7
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==2019.4.26
|
||||
@@ -477,7 +477,7 @@ eliqonline==1.2.2
|
||||
elkm1-lib==0.7.15
|
||||
|
||||
# homeassistant.components.emulated_roku
|
||||
emulated_roku==0.1.8
|
||||
emulated_roku==0.1.9
|
||||
|
||||
# homeassistant.components.enocean
|
||||
enocean==0.50
|
||||
@@ -679,7 +679,7 @@ hole==0.5.0
|
||||
holidays==0.9.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200108.0
|
||||
home-assistant-frontend==20200108.2
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.7
|
||||
@@ -1194,7 +1194,7 @@ pydaikin==1.6.1
|
||||
pydanfossair==0.1.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==67
|
||||
pydeconz==68
|
||||
|
||||
# homeassistant.components.delijn
|
||||
pydelijn==0.5.1
|
||||
@@ -1288,7 +1288,7 @@ pyhik==0.2.5
|
||||
pyhiveapi==0.2.19.3
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.62
|
||||
pyhomematic==0.1.63
|
||||
|
||||
# homeassistant.components.homeworks
|
||||
pyhomeworks==0.0.6
|
||||
@@ -1753,7 +1753,7 @@ rfk101py==0.0.1
|
||||
rflink==0.0.50
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring_doorbell==0.5.0
|
||||
ring_doorbell==0.6.0
|
||||
|
||||
# homeassistant.components.fleetgo
|
||||
ritassist==0.9.2
|
||||
@@ -1957,7 +1957,7 @@ temperusb==1.5.3
|
||||
# tensorflow==1.13.2
|
||||
|
||||
# homeassistant.components.tesla
|
||||
teslajsonpy==0.2.2
|
||||
teslajsonpy==0.2.3
|
||||
|
||||
# homeassistant.components.thermoworks_smoke
|
||||
thermoworks_smoke==0.1.8
|
||||
@@ -2113,7 +2113,7 @@ zengge==0.2
|
||||
zeroconf==0.24.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.30
|
||||
zha-quirks==0.0.31
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
|
||||
@@ -69,7 +69,7 @@ aiohue==1.10.1
|
||||
aionotion==1.1.0
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiopylgtv==0.2.4
|
||||
aiopylgtv==0.2.7
|
||||
|
||||
# homeassistant.components.switcher_kis
|
||||
aioswitcher==2019.4.26
|
||||
@@ -171,7 +171,7 @@ eebrightbox==0.0.4
|
||||
elgato==0.2.0
|
||||
|
||||
# homeassistant.components.emulated_roku
|
||||
emulated_roku==0.1.8
|
||||
emulated_roku==0.1.9
|
||||
|
||||
# homeassistant.components.season
|
||||
ephem==3.7.7.0
|
||||
@@ -244,7 +244,7 @@ hole==0.5.0
|
||||
holidays==0.9.12
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200108.0
|
||||
home-assistant-frontend==20200108.2
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.7
|
||||
@@ -414,7 +414,7 @@ pycoolmasternet==0.0.4
|
||||
pydaikin==1.6.1
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==67
|
||||
pydeconz==68
|
||||
|
||||
# homeassistant.components.zwave
|
||||
pydispatcher==2.0.5
|
||||
@@ -442,7 +442,7 @@ pyhaversion==3.1.0
|
||||
pyheos==0.6.0
|
||||
|
||||
# homeassistant.components.homematic
|
||||
pyhomematic==0.1.62
|
||||
pyhomematic==0.1.63
|
||||
|
||||
# homeassistant.components.icloud
|
||||
pyicloud==0.9.1
|
||||
@@ -567,7 +567,7 @@ restrictedpython==5.0
|
||||
rflink==0.0.50
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring_doorbell==0.5.0
|
||||
ring_doorbell==0.6.0
|
||||
|
||||
# homeassistant.components.yamaha
|
||||
rxv==0.6.0
|
||||
@@ -616,7 +616,7 @@ sunwatcher==0.2.1
|
||||
tellduslive==0.10.10
|
||||
|
||||
# homeassistant.components.tesla
|
||||
teslajsonpy==0.2.2
|
||||
teslajsonpy==0.2.3
|
||||
|
||||
# homeassistant.components.toon
|
||||
toonapilib==3.2.4
|
||||
@@ -673,7 +673,7 @@ yahooweather==0.10
|
||||
zeroconf==0.24.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.30
|
||||
zha-quirks==0.0.31
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.7.0
|
||||
|
||||
@@ -922,6 +922,11 @@ class MockEntity(entity.Entity):
|
||||
"""Info how device should be classified."""
|
||||
return self._handle("device_class")
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Info on the units the entity state is in."""
|
||||
return self._handle("unit_of_measurement")
|
||||
|
||||
@property
|
||||
def capability_attributes(self):
|
||||
"""Info about capabilities."""
|
||||
|
||||
@@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None):
|
||||
for capability in capabilities:
|
||||
if instance and capability["instance"] == instance:
|
||||
return capability
|
||||
elif capability["interface"] == capability_name:
|
||||
if not instance and capability["interface"] == capability_name:
|
||||
return capability
|
||||
|
||||
return None
|
||||
@@ -1427,6 +1427,36 @@ async def test_cover_position_range(hass):
|
||||
assert supported_range["maximumValue"] == 100
|
||||
assert supported_range["precision"] == 1
|
||||
|
||||
# Assert for Position Semantics
|
||||
position_semantics = range_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in position_action_mappings
|
||||
|
||||
position_state_mappings = position_semantics["stateMappings"]
|
||||
assert position_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in position_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in position_state_mappings
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.RangeController",
|
||||
"SetRangeValue",
|
||||
@@ -2454,16 +2484,37 @@ async def test_cover_position_mode(hass):
|
||||
},
|
||||
} in supported_modes
|
||||
|
||||
semantics = mode_capability["semantics"]
|
||||
assert semantics is not None
|
||||
# Assert for Position Semantics
|
||||
position_semantics = mode_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
action_mappings = semantics["actionMappings"]
|
||||
assert action_mappings is not None
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetMode", "payload": {"mode": "position.closed"}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetMode", "payload": {"mode": "position.open"}},
|
||||
} in position_action_mappings
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
position_state_mappings = position_semantics["stateMappings"]
|
||||
assert position_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": "position.closed",
|
||||
} in position_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"value": "position.open",
|
||||
} in position_state_mappings
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
@@ -2477,7 +2528,7 @@ async def test_cover_position_mode(hass):
|
||||
assert properties["namespace"] == "Alexa.ModeController"
|
||||
assert properties["value"] == "position.closed"
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
@@ -2491,7 +2542,7 @@ async def test_cover_position_mode(hass):
|
||||
assert properties["namespace"] == "Alexa.ModeController"
|
||||
assert properties["value"] == "position.open"
|
||||
|
||||
call, msg = await assert_request_calls_service(
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"cover#test_mode",
|
||||
@@ -2611,7 +2662,7 @@ async def test_cover_tilt_position_range(hass):
|
||||
|
||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||
assert range_capability is not None
|
||||
assert range_capability["instance"] == "cover.tilt_position"
|
||||
assert range_capability["instance"] == "cover.tilt"
|
||||
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
@@ -2629,7 +2680,7 @@ async def test_cover_tilt_position_range(hass):
|
||||
"cover.set_cover_tilt_position",
|
||||
hass,
|
||||
payload={"rangeValue": "50"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
assert call.data["position"] == 50
|
||||
|
||||
@@ -2640,7 +2691,7 @@ async def test_cover_tilt_position_range(hass):
|
||||
"cover.close_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": "0"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
@@ -2654,7 +2705,7 @@ async def test_cover_tilt_position_range(hass):
|
||||
"cover.open_cover_tilt",
|
||||
hass,
|
||||
payload={"rangeValue": "100"},
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
properties = msg["context"]["properties"][0]
|
||||
assert properties["name"] == "rangeValue"
|
||||
@@ -2670,12 +2721,12 @@ async def test_cover_tilt_position_range(hass):
|
||||
False,
|
||||
"cover.set_cover_tilt_position",
|
||||
"tilt_position",
|
||||
instance="cover.tilt_position",
|
||||
instance="cover.tilt",
|
||||
)
|
||||
|
||||
|
||||
async def test_cover_semantics(hass):
|
||||
"""Test cover discovery and semantics."""
|
||||
async def test_cover_semantics_position_and_tilt(hass):
|
||||
"""Test cover discovery and semantics with position and tilt support."""
|
||||
device = (
|
||||
"cover.test_semantics",
|
||||
"open",
|
||||
@@ -2697,50 +2748,57 @@ async def test_cover_semantics(hass):
|
||||
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||
)
|
||||
|
||||
for range_instance in ("cover.position", "cover.tilt_position"):
|
||||
range_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", range_instance
|
||||
)
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
# Assert for Position Semantics
|
||||
position_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", "cover.position"
|
||||
)
|
||||
position_semantics = position_capability["semantics"]
|
||||
assert position_semantics is not None
|
||||
|
||||
action_mappings = semantics["actionMappings"]
|
||||
assert action_mappings is not None
|
||||
if range_instance == "cover.position":
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
elif range_instance == "cover.position":
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
position_action_mappings = position_semantics["actionMappings"]
|
||||
assert position_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Lower"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in position_action_mappings
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in state_mappings
|
||||
# Assert for Tilt Semantics
|
||||
tilt_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", "cover.tilt"
|
||||
)
|
||||
tilt_semantics = tilt_capability["semantics"]
|
||||
assert tilt_semantics is not None
|
||||
tilt_action_mappings = tilt_semantics["actionMappings"]
|
||||
assert tilt_action_mappings is not None
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Close"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||
} in tilt_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in tilt_action_mappings
|
||||
|
||||
tilt_state_mappings = tilt_semantics["stateMappings"]
|
||||
assert tilt_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in tilt_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in tilt_state_mappings
|
||||
|
||||
|
||||
async def test_input_number(hass):
|
||||
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import deconz
|
||||
from homeassistant.components.deconz.const import CONF_BRIDGEID
|
||||
from homeassistant.components.deconz.const import CONF_BRIDGE_ID
|
||||
|
||||
from .test_gateway import BRIDGEID, setup_deconz_integration
|
||||
|
||||
@@ -91,7 +91,7 @@ async def test_configure_service_with_field(hass):
|
||||
|
||||
data = {
|
||||
deconz.services.SERVICE_FIELD: "/light/2",
|
||||
CONF_BRIDGEID: BRIDGEID,
|
||||
CONF_BRIDGE_ID: BRIDGEID,
|
||||
deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ async def test_service_refresh_devices(hass):
|
||||
"""Test that service can refresh devices."""
|
||||
gateway = await setup_deconz_integration(hass)
|
||||
|
||||
data = {CONF_BRIDGEID: BRIDGEID}
|
||||
data = {CONF_BRIDGE_ID: BRIDGEID}
|
||||
|
||||
with patch(
|
||||
"pydeconz.DeconzSession.request",
|
||||
|
||||
@@ -177,6 +177,25 @@ async def test_light_color_temperature(hass, hk_driver, cls, events):
|
||||
assert events[-1].data[ATTR_VALUE] == "color temperature at 250"
|
||||
|
||||
|
||||
async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, events):
|
||||
"""Test light with color temperature and rgb color not exposing temperature."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_ON,
|
||||
{
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP | SUPPORT_COLOR,
|
||||
ATTR_COLOR_TEMP: 190,
|
||||
ATTR_HS_COLOR: (260, 90),
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None)
|
||||
|
||||
assert not hasattr(acc, "char_color_temperature")
|
||||
|
||||
|
||||
async def test_light_rgb_color(hass, hk_driver, cls, events):
|
||||
"""Test light with rgb_color."""
|
||||
entity_id = "light.demo"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Test different accessory types: Sensors."""
|
||||
from homeassistant.components.homekit import get_accessory
|
||||
from homeassistant.components.homekit.const import (
|
||||
PROP_CELSIUS,
|
||||
THRESHOLD_CO,
|
||||
@@ -17,6 +18,7 @@ from homeassistant.components.homekit.type_sensors import (
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
STATE_OFF,
|
||||
@@ -25,6 +27,8 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.helpers import entity_registry
|
||||
|
||||
|
||||
async def test_temperature(hass, hk_driver):
|
||||
@@ -262,3 +266,34 @@ async def test_binary_device_classes(hass, hk_driver):
|
||||
acc = BinarySensor(hass, hk_driver, "Binary Sensor", entity_id, 2, None)
|
||||
assert acc.get_service(service).display_name == service
|
||||
assert acc.char_detected.display_name == char
|
||||
|
||||
|
||||
async def test_sensor_restore(hass, hk_driver, events):
|
||||
"""Test setting up an entity from state in the event registry."""
|
||||
hass.state = CoreState.not_running
|
||||
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
|
||||
registry.async_get_or_create(
|
||||
"sensor",
|
||||
"generic",
|
||||
"1234",
|
||||
suggested_object_id="temperature",
|
||||
device_class="temperature",
|
||||
)
|
||||
registry.async_get_or_create(
|
||||
"sensor",
|
||||
"generic",
|
||||
"12345",
|
||||
suggested_object_id="humidity",
|
||||
device_class="humidity",
|
||||
unit_of_measurement="%",
|
||||
)
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
acc = get_accessory(hass, hk_driver, hass.states.get("sensor.temperature"), 2, {})
|
||||
assert acc.category == 10
|
||||
|
||||
acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 2, {})
|
||||
assert acc.category == 10
|
||||
|
||||
@@ -29,11 +29,14 @@ async def test_setup_defined_hosts_known_auth(hass):
|
||||
hue.DOMAIN,
|
||||
{
|
||||
hue.DOMAIN: {
|
||||
hue.CONF_BRIDGES: {
|
||||
hue.CONF_HOST: "0.0.0.0",
|
||||
hue.CONF_ALLOW_HUE_GROUPS: False,
|
||||
hue.CONF_ALLOW_UNREACHABLE: True,
|
||||
}
|
||||
hue.CONF_BRIDGES: [
|
||||
{
|
||||
hue.CONF_HOST: "0.0.0.0",
|
||||
hue.CONF_ALLOW_HUE_GROUPS: False,
|
||||
hue.CONF_ALLOW_UNREACHABLE: True,
|
||||
},
|
||||
{hue.CONF_HOST: "1.1.1.1", "filename": "bla"},
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -41,7 +44,7 @@ async def test_setup_defined_hosts_known_auth(hass):
|
||||
)
|
||||
|
||||
# Flow started for discovered bridge
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
assert len(hass.config_entries.flow.async_progress()) == 1
|
||||
|
||||
# Config stored for domain.
|
||||
assert hass.data[hue.DATA_CONFIGS] == {
|
||||
@@ -49,7 +52,13 @@ async def test_setup_defined_hosts_known_auth(hass):
|
||||
hue.CONF_HOST: "0.0.0.0",
|
||||
hue.CONF_ALLOW_HUE_GROUPS: False,
|
||||
hue.CONF_ALLOW_UNREACHABLE: True,
|
||||
}
|
||||
},
|
||||
"1.1.1.1": {
|
||||
hue.CONF_HOST: "1.1.1.1",
|
||||
hue.CONF_ALLOW_HUE_GROUPS: True,
|
||||
hue.CONF_ALLOW_UNREACHABLE: False,
|
||||
"filename": "bla",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Configuration for Ring tests."""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import requests_mock
|
||||
|
||||
@@ -33,17 +35,19 @@ def requests_mock_fixture():
|
||||
)
|
||||
# Mocks the response for getting the history of a device
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/history",
|
||||
re.compile(
|
||||
r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history"
|
||||
),
|
||||
text=load_fixture("ring_doorbots.json"),
|
||||
)
|
||||
# Mocks the response for getting the health of a device
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/health",
|
||||
re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"),
|
||||
text=load_fixture("ring_doorboot_health_attrs.json"),
|
||||
)
|
||||
# Mocks the response for getting a chimes health
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/chimes/999999/health",
|
||||
re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"),
|
||||
text=load_fixture("ring_chime_health_attrs.json"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,87 +1,31 @@
|
||||
"""The tests for the Ring binary sensor platform."""
|
||||
from asyncio import run_coroutine_threadsafe
|
||||
import unittest
|
||||
from time import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components import ring as base_ring
|
||||
from homeassistant.components.ring import binary_sensor as ring
|
||||
|
||||
from tests.common import get_test_home_assistant, load_fixture, mock_storage
|
||||
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
|
||||
from .common import setup_platform
|
||||
|
||||
|
||||
class TestRingBinarySensorSetup(unittest.TestCase):
|
||||
"""Test the Ring Binary Sensor platform."""
|
||||
async def test_binary_sensor(hass, requests_mock):
|
||||
"""Test the Ring binary sensors."""
|
||||
with patch(
|
||||
"ring_doorbell.Ring.active_alerts",
|
||||
return_value=[
|
||||
{
|
||||
"kind": "motion",
|
||||
"doorbot_id": 987654,
|
||||
"state": "ringing",
|
||||
"now": time(),
|
||||
"expires_in": 180,
|
||||
}
|
||||
],
|
||||
):
|
||||
await setup_platform(hass, "binary_sensor")
|
||||
|
||||
DEVICES = []
|
||||
motion_state = hass.states.get("binary_sensor.front_door_motion")
|
||||
assert motion_state is not None
|
||||
assert motion_state.state == "on"
|
||||
assert motion_state.attributes["device_class"] == "motion"
|
||||
|
||||
def add_entities(self, devices, action):
|
||||
"""Mock add devices."""
|
||||
for device in devices:
|
||||
self.DEVICES.append(device)
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize values for this testcase class."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.config = {
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"monitored_conditions": ["ding", "motion"],
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_binary_sensor(self, mock):
|
||||
"""Test the Ring sensor class and methods."""
|
||||
mock.post(
|
||||
"https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json")
|
||||
)
|
||||
mock.post(
|
||||
"https://api.ring.com/clients_api/session",
|
||||
text=load_fixture("ring_session.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/ring_devices",
|
||||
text=load_fixture("ring_devices.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/dings/active",
|
||||
text=load_fixture("ring_ding_active.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/health",
|
||||
text=load_fixture("ring_doorboot_health_attrs.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/chimes/999999/health",
|
||||
text=load_fixture("ring_chime_health_attrs.json"),
|
||||
)
|
||||
|
||||
with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []):
|
||||
run_coroutine_threadsafe(
|
||||
base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop
|
||||
).result()
|
||||
run_coroutine_threadsafe(
|
||||
self.hass.async_block_till_done(), self.hass.loop
|
||||
).result()
|
||||
run_coroutine_threadsafe(
|
||||
ring.async_setup_entry(self.hass, None, self.add_entities),
|
||||
self.hass.loop,
|
||||
).result()
|
||||
|
||||
for device in self.DEVICES:
|
||||
device.update()
|
||||
if device.name == "Front Door Ding":
|
||||
assert "on" == device.state
|
||||
assert "America/New_York" == device.device_state_attributes["timezone"]
|
||||
elif device.name == "Front Door Motion":
|
||||
assert "off" == device.state
|
||||
assert "motion" == device.device_class
|
||||
|
||||
assert device.entity_picture is None
|
||||
assert ATTRIBUTION == device.device_state_attributes["attribution"]
|
||||
ding_state = hass.states.get("binary_sensor.front_door_ding")
|
||||
assert ding_state is not None
|
||||
assert ding_state.state == "off"
|
||||
|
||||
@@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock):
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
entry = entity_registry.async_get("light.front_light")
|
||||
assert entry.unique_id == "aacdef123"
|
||||
assert entry.unique_id == 765432
|
||||
|
||||
entry = entity_registry.async_get("light.internal_light")
|
||||
assert entry.unique_id == "aacdef124"
|
||||
assert entry.unique_id == 345678
|
||||
|
||||
|
||||
async def test_light_off_reports_correctly(hass, requests_mock):
|
||||
@@ -42,7 +42,7 @@ async def test_light_can_be_turned_on(hass, requests_mock):
|
||||
|
||||
# Mocks the response for turning a light on
|
||||
requests_mock.put(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on",
|
||||
"https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on",
|
||||
text=load_fixture("ring_doorbot_siren_on_response.json"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,122 +1,46 @@
|
||||
"""The tests for the Ring sensor platform."""
|
||||
from asyncio import run_coroutine_threadsafe
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from .common import setup_platform
|
||||
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components import ring as base_ring
|
||||
import homeassistant.components.ring.sensor as ring
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
from tests.common import get_test_home_assistant, load_fixture, mock_storage
|
||||
from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG
|
||||
WIFI_ENABLED = False
|
||||
|
||||
|
||||
class TestRingSensorSetup(unittest.TestCase):
|
||||
"""Test the Ring platform."""
|
||||
async def test_sensor(hass, requests_mock):
|
||||
"""Test the Ring sensors."""
|
||||
await setup_platform(hass, "sensor")
|
||||
|
||||
DEVICES = []
|
||||
front_battery_state = hass.states.get("sensor.front_battery")
|
||||
assert front_battery_state is not None
|
||||
assert front_battery_state.state == "80"
|
||||
|
||||
def add_entities(self, devices, action):
|
||||
"""Mock add devices."""
|
||||
for device in devices:
|
||||
self.DEVICES.append(device)
|
||||
front_door_battery_state = hass.states.get("sensor.front_door_battery")
|
||||
assert front_door_battery_state is not None
|
||||
assert front_door_battery_state.state == "100"
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize values for this testcase class."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.config = {
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"monitored_conditions": [
|
||||
"battery",
|
||||
"last_activity",
|
||||
"last_ding",
|
||||
"last_motion",
|
||||
"volume",
|
||||
"wifi_signal_category",
|
||||
"wifi_signal_strength",
|
||||
],
|
||||
}
|
||||
downstairs_volume_state = hass.states.get("sensor.downstairs_volume")
|
||||
assert downstairs_volume_state is not None
|
||||
assert downstairs_volume_state.state == "2"
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity")
|
||||
assert front_door_last_activity_state is not None
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_sensor(self, mock):
|
||||
"""Test the Ring sensor class and methods."""
|
||||
mock.post(
|
||||
"https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json")
|
||||
)
|
||||
mock.post(
|
||||
"https://api.ring.com/clients_api/session",
|
||||
text=load_fixture("ring_session.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/ring_devices",
|
||||
text=load_fixture("ring_devices.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/history",
|
||||
text=load_fixture("ring_doorbots.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/health",
|
||||
text=load_fixture("ring_doorboot_health_attrs.json"),
|
||||
)
|
||||
mock.get(
|
||||
"https://api.ring.com/clients_api/chimes/999999/health",
|
||||
text=load_fixture("ring_chime_health_attrs.json"),
|
||||
)
|
||||
downstairs_wifi_signal_strength_state = hass.states.get(
|
||||
"sensor.downstairs_wifi_signal_strength"
|
||||
)
|
||||
|
||||
with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []):
|
||||
run_coroutine_threadsafe(
|
||||
base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop
|
||||
).result()
|
||||
run_coroutine_threadsafe(
|
||||
self.hass.async_block_till_done(), self.hass.loop
|
||||
).result()
|
||||
run_coroutine_threadsafe(
|
||||
ring.async_setup_entry(self.hass, None, self.add_entities),
|
||||
self.hass.loop,
|
||||
).result()
|
||||
if not WIFI_ENABLED:
|
||||
return
|
||||
|
||||
for device in self.DEVICES:
|
||||
# Mimick add to hass
|
||||
device.hass = self.hass
|
||||
run_coroutine_threadsafe(
|
||||
device.async_added_to_hass(), self.hass.loop,
|
||||
).result()
|
||||
assert downstairs_wifi_signal_strength_state is not None
|
||||
assert downstairs_wifi_signal_strength_state.state == "-39"
|
||||
|
||||
# Entity update data from ring data
|
||||
device.update()
|
||||
if device.name == "Front Battery":
|
||||
expected_icon = icon_for_battery_level(
|
||||
battery_level=int(device.state), charging=False
|
||||
)
|
||||
assert device.icon == expected_icon
|
||||
assert 80 == device.state
|
||||
if device.name == "Front Door Battery":
|
||||
assert 100 == device.state
|
||||
if device.name == "Downstairs Volume":
|
||||
assert 2 == device.state
|
||||
assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"]
|
||||
assert "mdi:bell-ring" == device.icon
|
||||
if device.name == "Front Door Last Activity":
|
||||
assert not device.device_state_attributes["answered"]
|
||||
assert "America/New_York" == device.device_state_attributes["timezone"]
|
||||
front_door_wifi_signal_category_state = hass.states.get(
|
||||
"sensor.front_door_wifi_signal_category"
|
||||
)
|
||||
assert front_door_wifi_signal_category_state is not None
|
||||
assert front_door_wifi_signal_category_state.state == "good"
|
||||
|
||||
if device.name == "Downstairs WiFi Signal Strength":
|
||||
assert -39 == device.state
|
||||
|
||||
if device.name == "Front Door WiFi Signal Category":
|
||||
assert "good" == device.state
|
||||
|
||||
if device.name == "Front Door WiFi Signal Strength":
|
||||
assert -58 == device.state
|
||||
|
||||
assert device.entity_picture is None
|
||||
assert ATTRIBUTION == device.device_state_attributes["attribution"]
|
||||
assert not device.should_poll
|
||||
front_door_wifi_signal_strength_state = hass.states.get(
|
||||
"sensor.front_door_wifi_signal_strength"
|
||||
)
|
||||
assert front_door_wifi_signal_strength_state is not None
|
||||
assert front_door_wifi_signal_strength_state.state == "-58"
|
||||
|
||||
@@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock):
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
entry = entity_registry.async_get("switch.front_siren")
|
||||
assert entry.unique_id == "aacdef123-siren"
|
||||
assert entry.unique_id == "765432-siren"
|
||||
|
||||
entry = entity_registry.async_get("switch.internal_siren")
|
||||
assert entry.unique_id == "aacdef124-siren"
|
||||
assert entry.unique_id == "345678-siren"
|
||||
|
||||
|
||||
async def test_siren_off_reports_correctly(hass, requests_mock):
|
||||
@@ -43,7 +43,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock):
|
||||
|
||||
# Mocks the response for turning a siren on
|
||||
requests_mock.put(
|
||||
"https://api.ring.com/clients_api/doorbots/987652/siren_on",
|
||||
"https://api.ring.com/clients_api/doorbots/765432/siren_on",
|
||||
text=load_fixture("ring_doorbot_siren_on_response.json"),
|
||||
)
|
||||
|
||||
|
||||
8
tests/fixtures/ring_devices.json
vendored
8
tests/fixtures/ring_devices.json
vendored
@@ -9,7 +9,7 @@
|
||||
"do_not_disturb": {"seconds_left": 0},
|
||||
"features": {"ringtones_enabled": true},
|
||||
"firmware_version": "1.2.3",
|
||||
"id": 999999,
|
||||
"id": 123456,
|
||||
"kind": "chime",
|
||||
"latitude": 12.000000,
|
||||
"longitude": -70.12345,
|
||||
@@ -42,7 +42,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.4.26",
|
||||
"id": 987652,
|
||||
"id": 987654,
|
||||
"kind": "lpd_v1",
|
||||
"latitude": 12.000000,
|
||||
"longitude": -70.12345,
|
||||
@@ -93,7 +93,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.9.3",
|
||||
"id": 987652,
|
||||
"id": 765432,
|
||||
"kind": "hp_cam_v1",
|
||||
"latitude": 12.000000,
|
||||
"led_status": "off",
|
||||
@@ -231,7 +231,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.9.3",
|
||||
"id": 987652,
|
||||
"id": 345678,
|
||||
"kind": "hp_cam_v1",
|
||||
"latitude": 12.000000,
|
||||
"led_status": "on",
|
||||
|
||||
8
tests/fixtures/ring_devices_updated.json
vendored
8
tests/fixtures/ring_devices_updated.json
vendored
@@ -9,7 +9,7 @@
|
||||
"do_not_disturb": {"seconds_left": 0},
|
||||
"features": {"ringtones_enabled": true},
|
||||
"firmware_version": "1.2.3",
|
||||
"id": 999999,
|
||||
"id": 123456,
|
||||
"kind": "chime",
|
||||
"latitude": 12.000000,
|
||||
"longitude": -70.12345,
|
||||
@@ -42,7 +42,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.4.26",
|
||||
"id": 987652,
|
||||
"id": 987654,
|
||||
"kind": "lpd_v1",
|
||||
"latitude": 12.000000,
|
||||
"longitude": -70.12345,
|
||||
@@ -93,7 +93,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.9.3",
|
||||
"id": 987652,
|
||||
"id": 765432,
|
||||
"kind": "hp_cam_v1",
|
||||
"latitude": 12.000000,
|
||||
"led_status": "on",
|
||||
@@ -231,7 +231,7 @@
|
||||
"shadow_correction_enabled": false,
|
||||
"show_recordings": true},
|
||||
"firmware_version": "1.9.3",
|
||||
"id": 987652,
|
||||
"id": 345678,
|
||||
"kind": "hp_cam_v1",
|
||||
"latitude": 12.000000,
|
||||
"led_status": "off",
|
||||
|
||||
@@ -122,6 +122,64 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl):
|
||||
assert result["reason"] == "authorize_url_timeout"
|
||||
|
||||
|
||||
async def test_step_discovery(hass, flow_handler, local_impl):
|
||||
"""Check flow triggers from discovery."""
|
||||
hass.config.api.base_url = "https://example.com"
|
||||
flow_handler.async_register_implementation(hass, local_impl)
|
||||
config_entry_oauth2_flow.async_register_implementation(
|
||||
hass, TEST_DOMAIN, MockOAuth2Implementation()
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pick_implementation"
|
||||
|
||||
|
||||
async def test_abort_discovered_multiple(hass, flow_handler, local_impl):
|
||||
"""Test if aborts when discovered multiple times."""
|
||||
hass.config.api.base_url = "https://example.com"
|
||||
flow_handler.async_register_implementation(hass, local_impl)
|
||||
config_entry_oauth2_flow.async_register_implementation(
|
||||
hass, TEST_DOMAIN, MockOAuth2Implementation()
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "pick_implementation"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl):
|
||||
"""Test if abort discovery when entries exists."""
|
||||
hass.config.api.base_url = "https://example.com"
|
||||
flow_handler.async_register_implementation(hass, local_impl)
|
||||
config_entry_oauth2_flow.async_register_implementation(
|
||||
hass, TEST_DOMAIN, MockOAuth2Implementation()
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(domain=TEST_DOMAIN, data={},)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_full_flow(
|
||||
hass, flow_handler, local_impl, aiohttp_client, aioclient_mock
|
||||
):
|
||||
|
||||
@@ -804,6 +804,7 @@ async def test_entity_info_added_to_entity_registry(hass):
|
||||
capability_attributes={"max": 100},
|
||||
supported_features=5,
|
||||
device_class="mock-device-class",
|
||||
unit_of_measurement="%",
|
||||
)
|
||||
|
||||
await component.async_add_entities([entity_default])
|
||||
@@ -815,6 +816,7 @@ async def test_entity_info_added_to_entity_registry(hass):
|
||||
assert entry_default.capabilities == {"max": 100}
|
||||
assert entry_default.supported_features == 5
|
||||
assert entry_default.device_class == "mock-device-class"
|
||||
assert entry_default.unit_of_measurement == "%"
|
||||
|
||||
|
||||
async def test_override_restored_entities(hass):
|
||||
|
||||
Reference in New Issue
Block a user