Compare commits

...

42 Commits

Author SHA1 Message Date
Paulus Schoutsen
7ef7d1dfd0 Merge pull request #30925 from home-assistant/rc
0.104.2
2020-01-17 16:32:18 -08:00
Paulus Schoutsen
2c915af348 Bumped version to 0.104.2 2020-01-17 15:48:27 -08:00
Paulus Schoutsen
2b733917a4 Fix hue accepting filename (#30924) 2020-01-17 15:48:21 -08:00
Bram Kragten
353010712f pdated frontend to 20200108.2 (#30921) 2020-01-17 15:47:58 -08:00
Robert Svensson
07a0bc4abe Fix service device refresh calling state update (#30920) 2020-01-17 15:46:10 -08:00
Paulus Schoutsen
f9b48844e6 camera endpoint likes to timeout, catch it. (#30919) 2020-01-17 15:46:10 -08:00
ochlocracy
6053d02e44 Fix Alexa semantics for covers with tilt support. (#30911)
* Fix Alexa semantics for covers with tilt support.

* Clarify wording.

* Korrect grammar.
2020-01-17 15:45:41 -08:00
Robert Svensson
6ac33e5c7b Fix issue with group unique id when normalising bridge id (#30904) 2020-01-17 15:44:09 -08:00
SukramJ
586566e6ab Fix missing switch groups of HomematicIP Cloud (#30903) 2020-01-17 15:44:08 -08:00
David F. Mulcahey
5a46adfebf add multistate back (#30889) 2020-01-17 15:44:07 -08:00
Paulus Schoutsen
09f7a09ce7 Merge pull request #30853 from home-assistant/rc
0.104.1
2020-01-16 12:47:51 -08:00
Aaron Bach
e2e01f5020 Fix sensor type creation with multiple Ambient weather stations (#30850) 2020-01-16 11:38:10 -08:00
Per-Øyvind Bruun
0e1450838e Fix for issue #29822 (#30849) 2020-01-16 11:38:10 -08:00
Paulus Schoutsen
006419b96c Whitelist Frenck for release 2020-01-16 11:36:01 -08:00
Paulus Schoutsen
3f54533e72 Bumped version to 0.104.1 2020-01-16 11:33:54 -08:00
Paulus Schoutsen
cc126761e1 Reinstate and deprecate filename option for hue config (#30846) 2020-01-16 11:33:47 -08:00
Quentame
8fdf68c8d1 Fix iCloud when no family members (issue #30829) (#30836) 2020-01-16 11:33:46 -08:00
Josh Bendavid
6a8582750c Fix play_media in webostv (#30828) 2020-01-16 11:33:45 -08:00
Pascal Vizeli
5b51f740df Fix mpd time issue (#30825)
* Fix mpd time issue

* Update homeassistant/components/mpd/media_player.py

Co-Authored-By: Franck Nijhof <git@frenck.dev>
2020-01-16 11:33:45 -08:00
Paulus Schoutsen
881b35f9d6 Handle no host info in ignored config entries (#30822) 2020-01-16 11:33:44 -08:00
springstan
8f852bd656 Fix setup error of Mikrotik (#30810) 2020-01-16 11:33:43 -08:00
Josh Bendavid
368d04b2a1 update to aiopylgtv 0.2.7 (#30797) 2020-01-16 11:33:42 -08:00
Jan De Luyck
6d83dafff2 Update emulated_roku to 0.1.9 (#30791)
* Update emulated_roku to 0.1.9

* Update requirements_all
2020-01-16 11:33:42 -08:00
Franck Nijhof
4bb319e658 0.104.0 (#30803)
0.104.0
2020-01-15 21:38:22 +01:00
Franck Nijhof
3d9b6332c8 Bumped version to 0.104.0 2020-01-15 20:22:09 +01:00
Jc2k
1a6535ff8b Restore unit_of_measurement from entity registry (#30780)
* Restore unit_of_measurement from entity registry

* Lint fix
2020-01-15 20:17:42 +01:00
Paulus Schoutsen
3fd14ca3cf Refactor Ring data handling (#30777)
* Refactor Ring data handling

* Add async_ to methods
2020-01-15 20:17:36 +01:00
Pascal Vizeli
1c70435df6 Revert #29701 (#30766)
* Revert #29701

* fix format

* Fix lint
2020-01-15 20:17:28 +01:00
Paulus Schoutsen
8dbdf0b0e1 Bumped version to 0.104.0b5 2020-01-14 12:59:50 -08:00
Franck Nijhof
fe9c85aaf1 Fix HomeKit behavior with lights supporting color and temperature (#30756) 2020-01-14 12:59:41 -08:00
Bas Nijholt
9de800ab6a bump aiokef to 0.2.5 which uses locks (#30753) 2020-01-14 12:59:41 -08:00
Pascal Vizeli
f530ea10af Refactor HomeMatic / Fix issue with 0.104/dev (#30752)
* Refactor HomeMatic / Fix issue with 0.104/dev

* Fix lock
2020-01-14 12:59:40 -08:00
Daniel Perna
64224f46ec Update pyhomematic to 0.1.63 (#30594) 2020-01-14 12:59:39 -08:00
Alan Tse
413545bd91 Bump teslajsonpy to 0.2.3 (#30750) 2020-01-14 12:57:34 -08:00
Paulus Schoutsen
f5af77d00c Set default locale for cloud Alexa config (#30749) 2020-01-14 12:57:33 -08:00
Paulus Schoutsen
4bc520c724 Update Ring to 0.6.0 (#30748)
* Update Ring to 0.6.0

* Update sensor tests

* update -> async_update

* Delete temp files

* Address comments

* Final tweaks

* Remove stale print
2020-01-14 12:57:32 -08:00
David F. Mulcahey
8e5f46d5b5 Bump ZHA quirks to 0.0.31 (#30740)
* Bump ZHA quirks version

* update requirements
2020-01-14 12:57:31 -08:00
Josh Bendavid
9fa0779c1f update aiopylgtv to 0.2.6 (#30739) 2020-01-14 12:57:30 -08:00
Josh Bendavid
c138a93454 update aiopylgtv to 0.2.5 (#30702) 2020-01-14 12:57:30 -08:00
Paulus Schoutsen
c6b96f7250 Fix Ring wifi sensors (#30735)
* Fix Ring wifi sensors

* Update before adding
2020-01-14 12:56:01 -08:00
Phil Bruckner
4a8ecb82a8 Revert "Forget auth token when going offline so we can reconnect (#26630)" (#30705)
This reverts commit 2d6d6ba90e.
2020-01-14 12:56:00 -08:00
Franck Nijhof
80f3cb7d79 Fix discovery for oauth2 flow implementations (#30700)
* Fix discovery for oauth2 flow implementations

* Fix user step

* Add tests
2020-01-14 12:55:59 -08:00
85 changed files with 1839 additions and 1514 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
DOMAIN = "ambient_station"
ATTR_LAST_DATA = "last_data"
ATTR_MONITORED_CONDITIONS = "monitored_conditions"
CONF_APP_KEY = "app_key"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"

View File

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

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

View File

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

View File

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

View File

@@ -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"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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",
}

View File

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

View File

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

View File

@@ -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,
],
}

View File

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

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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