mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 11:47:06 +00:00
commit
7ef7d1dfd0
@ -1073,6 +1073,15 @@ class AlexaSecurityPanelController(AlexaCapability):
|
|||||||
class AlexaModeController(AlexaCapability):
|
class AlexaModeController(AlexaCapability):
|
||||||
"""Implements Alexa.ModeController.
|
"""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
|
https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -1183,28 +1192,38 @@ class AlexaModeController(AlexaCapability):
|
|||||||
|
|
||||||
def semantics(self):
|
def semantics(self):
|
||||||
"""Build and return semantics object."""
|
"""Build and return semantics object."""
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||||
|
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||||
self._semantics = AlexaSemantics()
|
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(
|
self._semantics.add_action_to_directive(
|
||||||
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
lower_labels,
|
||||||
"SetMode",
|
"SetMode",
|
||||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||||
)
|
)
|
||||||
self._semantics.add_action_to_directive(
|
self._semantics.add_action_to_directive(
|
||||||
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
raise_labels,
|
||||||
"SetMode",
|
"SetMode",
|
||||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
{"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 self._semantics.serialize_semantics()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -1213,6 +1232,15 @@ class AlexaModeController(AlexaCapability):
|
|||||||
class AlexaRangeController(AlexaCapability):
|
class AlexaRangeController(AlexaCapability):
|
||||||
"""Implements Alexa.RangeController.
|
"""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
|
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}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||||
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION)
|
||||||
|
|
||||||
# Input Number Value
|
# Input Number Value
|
||||||
@ -1321,10 +1349,10 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
)
|
)
|
||||||
return self._resource.serialize_capability_resources()
|
return self._resource.serialize_capability_resources()
|
||||||
|
|
||||||
# Cover Tilt Position Resources
|
# Cover Tilt Resources
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||||
self._resource = AlexaPresetResource(
|
self._resource = AlexaPresetResource(
|
||||||
["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING],
|
["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION],
|
||||||
min_value=0,
|
min_value=0,
|
||||||
max_value=100,
|
max_value=100,
|
||||||
precision=1,
|
precision=1,
|
||||||
@ -1358,24 +1386,35 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
|
|
||||||
def semantics(self):
|
def semantics(self):
|
||||||
"""Build and return semantics object."""
|
"""Build and return semantics object."""
|
||||||
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
|
|
||||||
# Cover Position
|
# Cover Position
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||||
|
lower_labels = [AlexaSemantics.ACTION_LOWER]
|
||||||
|
raise_labels = [AlexaSemantics.ACTION_RAISE]
|
||||||
self._semantics = AlexaSemantics()
|
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(
|
self._semantics.add_action_to_directive(
|
||||||
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||||
)
|
)
|
||||||
self._semantics.add_action_to_directive(
|
self._semantics.add_action_to_directive(
|
||||||
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
|
raise_labels, "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
|
|
||||||
)
|
)
|
||||||
return self._semantics.serialize_semantics()
|
return self._semantics.serialize_semantics()
|
||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt
|
||||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
if self.instance == f"{cover.DOMAIN}.tilt":
|
||||||
self._semantics = AlexaSemantics()
|
self._semantics = AlexaSemantics()
|
||||||
self._semantics.add_action_to_directive(
|
self._semantics.add_action_to_directive(
|
||||||
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
|
||||||
@ -1395,6 +1434,15 @@ class AlexaRangeController(AlexaCapability):
|
|||||||
class AlexaToggleController(AlexaCapability):
|
class AlexaToggleController(AlexaCapability):
|
||||||
"""Implements Alexa.ToggleController.
|
"""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
|
https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -404,9 +404,7 @@ class CoverCapabilities(AlexaEntity):
|
|||||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||||
)
|
)
|
||||||
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||||
yield AlexaRangeController(
|
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
|
||||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
|
||||||
)
|
|
||||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||||
yield Alexa(self.hass)
|
yield Alexa(self.hass)
|
||||||
|
|
||||||
|
@ -1118,8 +1118,8 @@ async def async_api_set_range(hass, config, directive, context):
|
|||||||
service = cover.SERVICE_SET_COVER_POSITION
|
service = cover.SERVICE_SET_COVER_POSITION
|
||||||
data[cover.ATTR_POSITION] = range_value
|
data[cover.ATTR_POSITION] = range_value
|
||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
elif instance == f"{cover.DOMAIN}.tilt":
|
||||||
range_value = int(range_value)
|
range_value = int(range_value)
|
||||||
if range_value == 0:
|
if range_value == 0:
|
||||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
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)
|
100, max(0, range_delta + current)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cover Tilt Position
|
# Cover Tilt
|
||||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
elif instance == f"{cover.DOMAIN}.tilt":
|
||||||
range_delta = int(range_delta)
|
range_delta = int(range_delta)
|
||||||
service = SERVICE_SET_COVER_TILT_POSITION
|
service = SERVICE_SET_COVER_TILT_POSITION
|
||||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||||
|
@ -190,7 +190,12 @@ class AlexaGlobalCatalog:
|
|||||||
|
|
||||||
|
|
||||||
class AlexaCapabilityResource:
|
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
|
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 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
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""Support for deCONZ devices."""
|
"""Support for deCONZ devices."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import _UNDEF
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||||
|
|
||||||
from .config_flow import get_master_gateway
|
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 .gateway import DeconzGateway
|
||||||
from .services import async_setup_services, async_unload_services
|
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
|
# 0.104 introduced config entry unique id, this makes upgrading possible
|
||||||
if config_entry.unique_id is None:
|
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(
|
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
|
hass.data[DOMAIN][config_entry.unique_id] = gateway
|
||||||
|
@ -54,11 +54,13 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
|
|||||||
"""Representation of a deCONZ binary sensor."""
|
"""Representation of a deCONZ binary sensor."""
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Update the sensor's state."""
|
||||||
changed = set(self._device.changed_keys)
|
if ignore_update:
|
||||||
|
return
|
||||||
|
|
||||||
keys = {"on", "reachable", "state"}
|
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()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers import aiohttp_client
|
|||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALLOW_CLIP_SENSOR,
|
CONF_ALLOW_CLIP_SENSOR,
|
||||||
CONF_ALLOW_DECONZ_GROUPS,
|
CONF_ALLOW_DECONZ_GROUPS,
|
||||||
CONF_BRIDGEID,
|
CONF_BRIDGE_ID,
|
||||||
DEFAULT_ALLOW_CLIP_SENSOR,
|
DEFAULT_ALLOW_CLIP_SENSOR,
|
||||||
DEFAULT_ALLOW_DECONZ_GROUPS,
|
DEFAULT_ALLOW_DECONZ_GROUPS,
|
||||||
DEFAULT_PORT,
|
DEFAULT_PORT,
|
||||||
@ -74,7 +74,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
for bridge in self.bridges:
|
for bridge in self.bridges:
|
||||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||||
self.bridge_id = bridge[CONF_BRIDGEID]
|
self.bridge_id = bridge[CONF_BRIDGE_ID]
|
||||||
self.deconz_config = {
|
self.deconz_config = {
|
||||||
CONF_HOST: bridge[CONF_HOST],
|
CONF_HOST: bridge[CONF_HOST],
|
||||||
CONF_PORT: bridge[CONF_PORT],
|
CONF_PORT: bridge[CONF_PORT],
|
||||||
|
@ -5,7 +5,8 @@ _LOGGER = logging.getLogger(__package__)
|
|||||||
|
|
||||||
DOMAIN = "deconz"
|
DOMAIN = "deconz"
|
||||||
|
|
||||||
CONF_BRIDGEID = "bridgeid"
|
CONF_BRIDGE_ID = "bridgeid"
|
||||||
|
CONF_GROUP_ID_BASE = "group_id_base"
|
||||||
|
|
||||||
DEFAULT_PORT = 80
|
DEFAULT_PORT = 80
|
||||||
DEFAULT_ALLOW_CLIP_SENSOR = False
|
DEFAULT_ALLOW_CLIP_SENSOR = False
|
||||||
|
@ -91,8 +91,11 @@ class DeconzDevice(DeconzBase, Entity):
|
|||||||
unsub_dispatcher()
|
unsub_dispatcher()
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Update the device's state."""
|
||||||
|
if ignore_update:
|
||||||
|
return
|
||||||
|
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -39,17 +39,21 @@ class DeconzEvent(DeconzBase):
|
|||||||
self._device = None
|
self._device = None
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Fire the event if reason is that state is updated."""
|
||||||
if "state" in self._device.changed_keys:
|
if ignore_update or "state" not in self._device.changed_keys:
|
||||||
data = {
|
return
|
||||||
CONF_ID: self.event_id,
|
|
||||||
CONF_UNIQUE_ID: self.serial,
|
data = {
|
||||||
CONF_EVENT: self._device.state,
|
CONF_ID: self.event_id,
|
||||||
}
|
CONF_UNIQUE_ID: self.serial,
|
||||||
if self._device.gesture:
|
CONF_EVENT: self._device.state,
|
||||||
data[CONF_GESTURE] = self._device.gesture
|
}
|
||||||
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
|
|
||||||
|
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):
|
async def async_update_device_registry(self):
|
||||||
"""Update device registry."""
|
"""Update device registry."""
|
||||||
|
@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_GROUP_ID_BASE,
|
||||||
COVER_TYPES,
|
COVER_TYPES,
|
||||||
DOMAIN as DECONZ_DOMAIN,
|
DOMAIN as DECONZ_DOMAIN,
|
||||||
NEW_GROUP,
|
NEW_GROUP,
|
||||||
@ -205,7 +206,11 @@ class DeconzGroup(DeconzLight):
|
|||||||
"""Set up group and create an unique id."""
|
"""Set up group and create an unique id."""
|
||||||
super().__init__(device, gateway)
|
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
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
"name": "deCONZ",
|
"name": "deCONZ",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||||
"requirements": ["pydeconz==67"],
|
"requirements": [
|
||||||
|
"pydeconz==68"
|
||||||
|
],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "Royal Philips Electronics"
|
"manufacturer": "Royal Philips Electronics"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"codeowners": ["@kane610"],
|
"codeowners": [
|
||||||
|
"@kane610"
|
||||||
|
],
|
||||||
"quality_scale": "platinum"
|
"quality_scale": "platinum"
|
||||||
}
|
}
|
@ -97,11 +97,13 @@ class DeconzSensor(DeconzDevice):
|
|||||||
"""Representation of a deCONZ sensor."""
|
"""Representation of a deCONZ sensor."""
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Update the sensor's state."""
|
||||||
changed = set(self._device.changed_keys)
|
if ignore_update:
|
||||||
|
return
|
||||||
|
|
||||||
keys = {"on", "reachable", "state"}
|
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()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -155,11 +157,13 @@ class DeconzBattery(DeconzDevice):
|
|||||||
"""Battery class for when a device is only represented as an event."""
|
"""Battery class for when a device is only represented as an event."""
|
||||||
|
|
||||||
@callback
|
@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."""
|
"""Update the battery's state, if needed."""
|
||||||
changed = set(self._device.changed_keys)
|
if ignore_update:
|
||||||
|
return
|
||||||
|
|
||||||
keys = {"battery", "reachable"}
|
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()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -217,7 +221,7 @@ class DeconzSensorStateTracker:
|
|||||||
self.sensor = None
|
self.sensor = None
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_callback(self):
|
def async_update_callback(self, ignore_update=False):
|
||||||
"""Sensor state updated."""
|
"""Sensor state updated."""
|
||||||
if "battery" in self.sensor.changed_keys:
|
if "battery" in self.sensor.changed_keys:
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
|
@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
from .config_flow import get_master_gateway
|
from .config_flow import get_master_gateway
|
||||||
from .const import (
|
from .const import (
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
CONF_BRIDGEID,
|
CONF_BRIDGE_ID,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
NEW_GROUP,
|
NEW_GROUP,
|
||||||
NEW_LIGHT,
|
NEW_LIGHT,
|
||||||
@ -27,14 +27,14 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
|
|||||||
vol.Optional(SERVICE_ENTITY): cv.entity_id,
|
vol.Optional(SERVICE_ENTITY): cv.entity_id,
|
||||||
vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
|
vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
|
||||||
vol.Required(SERVICE_DATA): dict,
|
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),
|
cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
|
||||||
)
|
)
|
||||||
|
|
||||||
SERVICE_DEVICE_REFRESH = "device_refresh"
|
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):
|
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:
|
See Dresden Elektroniks REST API documentation for details:
|
||||||
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
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, "")
|
field = data.get(SERVICE_FIELD, "")
|
||||||
entity_id = data.get(SERVICE_ENTITY)
|
entity_id = data.get(SERVICE_ENTITY)
|
||||||
data = data[SERVICE_DATA]
|
data = data[SERVICE_DATA]
|
||||||
@ -119,15 +119,15 @@ async def async_configure_service(hass, data):
|
|||||||
async def async_refresh_devices_service(hass, data):
|
async def async_refresh_devices_service(hass, data):
|
||||||
"""Refresh available devices from deCONZ."""
|
"""Refresh available devices from deCONZ."""
|
||||||
gateway = get_master_gateway(hass)
|
gateway = get_master_gateway(hass)
|
||||||
if CONF_BRIDGEID in data:
|
if CONF_BRIDGE_ID in data:
|
||||||
gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]]
|
gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]]
|
||||||
|
|
||||||
groups = set(gateway.api.groups.keys())
|
groups = set(gateway.api.groups.keys())
|
||||||
lights = set(gateway.api.lights.keys())
|
lights = set(gateway.api.lights.keys())
|
||||||
scenes = set(gateway.api.scenes.keys())
|
scenes = set(gateway.api.scenes.keys())
|
||||||
sensors = set(gateway.api.sensors.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(
|
gateway.async_add_device_callback(
|
||||||
NEW_GROUP,
|
NEW_GROUP,
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": [
|
"requirements": ["home-assistant-frontend==20200108.2"],
|
||||||
"home-assistant-frontend==20200108.0"
|
|
||||||
],
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
"auth",
|
"auth",
|
||||||
@ -14,8 +12,6 @@
|
|||||||
"system_log",
|
"system_log",
|
||||||
"websocket_api"
|
"websocket_api"
|
||||||
],
|
],
|
||||||
"codeowners": [
|
"codeowners": ["@home-assistant/frontend"],
|
||||||
"@home-assistant/frontend"
|
|
||||||
],
|
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ from homematicip.aio.device import (
|
|||||||
AsyncPrintedCircuitBoardSwitch2,
|
AsyncPrintedCircuitBoardSwitch2,
|
||||||
AsyncPrintedCircuitBoardSwitchBattery,
|
AsyncPrintedCircuitBoardSwitchBattery,
|
||||||
)
|
)
|
||||||
from homematicip.aio.group import AsyncSwitchingGroup
|
from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -67,7 +67,7 @@ async def async_setup_entry(
|
|||||||
entities.append(HomematicipMultiSwitch(hap, device, channel))
|
entities.append(HomematicipMultiSwitch(hap, device, channel))
|
||||||
|
|
||||||
for group in hap.home.groups:
|
for group in hap.home.groups:
|
||||||
if isinstance(group, AsyncSwitchingGroup):
|
if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)):
|
||||||
entities.append(HomematicipGroupSwitch(hap, group))
|
entities.append(HomematicipGroupSwitch(hap, group))
|
||||||
|
|
||||||
if entities:
|
if entities:
|
||||||
|
@ -36,6 +36,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS
|
CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS
|
||||||
): cv.boolean,
|
): cv.boolean,
|
||||||
|
vol.Optional("filename"): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,8 +47,10 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_BRIDGES): vol.All(
|
vol.Optional(CONF_BRIDGES): vol.All(
|
||||||
cv.ensure_list,
|
cv.ensure_list,
|
||||||
[
|
[
|
||||||
cv.deprecated("filename", invalidation_version="0.106.0"),
|
vol.All(
|
||||||
vol.All(BRIDGE_CONFIG_SCHEMA),
|
cv.deprecated("filename", invalidation_version="0.106.0"),
|
||||||
|
BRIDGE_CONFIG_SCHEMA,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import logging
|
|||||||
|
|
||||||
from haffmpeg.camera import CameraMjpeg
|
from haffmpeg.camera import CameraMjpeg
|
||||||
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
from haffmpeg.tools import IMAGE_JPEG, ImageFrame
|
||||||
|
import requests
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera
|
from homeassistant.components.camera import Camera
|
||||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||||
@ -146,9 +147,15 @@ class RingCam(RingEntityMixin, Camera):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
video_url = await self.hass.async_add_executor_job(
|
try:
|
||||||
self._device.recording_url, self._last_event["id"]
|
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
|
||||||
|
|
||||||
if video_url:
|
if video_url:
|
||||||
self._last_video_id = self._last_event["id"]
|
self._last_video_id = self._last_event["id"]
|
||||||
|
@ -56,6 +56,7 @@ CHANNEL_HUMIDITY = "humidity"
|
|||||||
CHANNEL_IAS_WD = "ias_wd"
|
CHANNEL_IAS_WD = "ias_wd"
|
||||||
CHANNEL_ILLUMINANCE = "illuminance"
|
CHANNEL_ILLUMINANCE = "illuminance"
|
||||||
CHANNEL_LEVEL = ATTR_LEVEL
|
CHANNEL_LEVEL = ATTR_LEVEL
|
||||||
|
CHANNEL_MULTISTATE_INPUT = "multistate_input"
|
||||||
CHANNEL_OCCUPANCY = "occupancy"
|
CHANNEL_OCCUPANCY = "occupancy"
|
||||||
CHANNEL_ON_OFF = "on_off"
|
CHANNEL_ON_OFF = "on_off"
|
||||||
CHANNEL_POWER_CONFIGURATION = "power"
|
CHANNEL_POWER_CONFIGURATION = "power"
|
||||||
|
@ -26,6 +26,7 @@ from .core.const import (
|
|||||||
CHANNEL_ELECTRICAL_MEASUREMENT,
|
CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||||
CHANNEL_HUMIDITY,
|
CHANNEL_HUMIDITY,
|
||||||
CHANNEL_ILLUMINANCE,
|
CHANNEL_ILLUMINANCE,
|
||||||
|
CHANNEL_MULTISTATE_INPUT,
|
||||||
CHANNEL_POWER_CONFIGURATION,
|
CHANNEL_POWER_CONFIGURATION,
|
||||||
CHANNEL_PRESSURE,
|
CHANNEL_PRESSURE,
|
||||||
CHANNEL_SMARTENERGY_METERING,
|
CHANNEL_SMARTENERGY_METERING,
|
||||||
@ -227,6 +228,18 @@ class ElectricalMeasurement(Sensor):
|
|||||||
return round(value * self._channel.multiplier / self._channel.divisor)
|
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(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER)
|
||||||
@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY)
|
@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY)
|
||||||
class Humidity(Sensor):
|
class Humidity(Sensor):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 104
|
MINOR_VERSION = 104
|
||||||
PATCH_VERSION = "1"
|
PATCH_VERSION = "2"
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||||
|
@ -11,7 +11,7 @@ cryptography==2.8
|
|||||||
defusedxml==0.6.0
|
defusedxml==0.6.0
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
hass-nabucasa==0.31
|
hass-nabucasa==0.31
|
||||||
home-assistant-frontend==20200108.0
|
home-assistant-frontend==20200108.2
|
||||||
importlib-metadata==1.3.0
|
importlib-metadata==1.3.0
|
||||||
jinja2>=2.10.3
|
jinja2>=2.10.3
|
||||||
netdisco==2.6.0
|
netdisco==2.6.0
|
||||||
|
@ -679,7 +679,7 @@ hole==0.5.0
|
|||||||
holidays==0.9.12
|
holidays==0.9.12
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20200108.0
|
home-assistant-frontend==20200108.2
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.7
|
homeassistant-pyozw==0.1.7
|
||||||
@ -1194,7 +1194,7 @@ pydaikin==1.6.1
|
|||||||
pydanfossair==0.1.0
|
pydanfossair==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==67
|
pydeconz==68
|
||||||
|
|
||||||
# homeassistant.components.delijn
|
# homeassistant.components.delijn
|
||||||
pydelijn==0.5.1
|
pydelijn==0.5.1
|
||||||
|
@ -244,7 +244,7 @@ hole==0.5.0
|
|||||||
holidays==0.9.12
|
holidays==0.9.12
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20200108.0
|
home-assistant-frontend==20200108.2
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.7
|
homeassistant-pyozw==0.1.7
|
||||||
@ -414,7 +414,7 @@ pycoolmasternet==0.0.4
|
|||||||
pydaikin==1.6.1
|
pydaikin==1.6.1
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==67
|
pydeconz==68
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
pydispatcher==2.0.5
|
pydispatcher==2.0.5
|
||||||
|
@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None):
|
|||||||
for capability in capabilities:
|
for capability in capabilities:
|
||||||
if instance and capability["instance"] == instance:
|
if instance and capability["instance"] == instance:
|
||||||
return capability
|
return capability
|
||||||
elif capability["interface"] == capability_name:
|
if not instance and capability["interface"] == capability_name:
|
||||||
return capability
|
return capability
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -1427,6 +1427,36 @@ async def test_cover_position_range(hass):
|
|||||||
assert supported_range["maximumValue"] == 100
|
assert supported_range["maximumValue"] == 100
|
||||||
assert supported_range["precision"] == 1
|
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(
|
call, _ = await assert_request_calls_service(
|
||||||
"Alexa.RangeController",
|
"Alexa.RangeController",
|
||||||
"SetRangeValue",
|
"SetRangeValue",
|
||||||
@ -2454,16 +2484,37 @@ async def test_cover_position_mode(hass):
|
|||||||
},
|
},
|
||||||
} in supported_modes
|
} in supported_modes
|
||||||
|
|
||||||
semantics = mode_capability["semantics"]
|
# Assert for Position Semantics
|
||||||
assert semantics is not None
|
position_semantics = mode_capability["semantics"]
|
||||||
|
assert position_semantics is not None
|
||||||
|
|
||||||
action_mappings = semantics["actionMappings"]
|
position_action_mappings = position_semantics["actionMappings"]
|
||||||
assert action_mappings is not None
|
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"]
|
position_state_mappings = position_semantics["stateMappings"]
|
||||||
assert state_mappings is not None
|
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",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"cover#test_mode",
|
"cover#test_mode",
|
||||||
@ -2477,7 +2528,7 @@ async def test_cover_position_mode(hass):
|
|||||||
assert properties["namespace"] == "Alexa.ModeController"
|
assert properties["namespace"] == "Alexa.ModeController"
|
||||||
assert properties["value"] == "position.closed"
|
assert properties["value"] == "position.closed"
|
||||||
|
|
||||||
call, msg = await assert_request_calls_service(
|
_, msg = await assert_request_calls_service(
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"cover#test_mode",
|
"cover#test_mode",
|
||||||
@ -2491,7 +2542,7 @@ async def test_cover_position_mode(hass):
|
|||||||
assert properties["namespace"] == "Alexa.ModeController"
|
assert properties["namespace"] == "Alexa.ModeController"
|
||||||
assert properties["value"] == "position.open"
|
assert properties["value"] == "position.open"
|
||||||
|
|
||||||
call, msg = await assert_request_calls_service(
|
_, msg = await assert_request_calls_service(
|
||||||
"Alexa.ModeController",
|
"Alexa.ModeController",
|
||||||
"SetMode",
|
"SetMode",
|
||||||
"cover#test_mode",
|
"cover#test_mode",
|
||||||
@ -2611,7 +2662,7 @@ async def test_cover_tilt_position_range(hass):
|
|||||||
|
|
||||||
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
range_capability = get_capability(capabilities, "Alexa.RangeController")
|
||||||
assert range_capability is not None
|
assert range_capability is not None
|
||||||
assert range_capability["instance"] == "cover.tilt_position"
|
assert range_capability["instance"] == "cover.tilt"
|
||||||
|
|
||||||
semantics = range_capability["semantics"]
|
semantics = range_capability["semantics"]
|
||||||
assert semantics is not None
|
assert semantics is not None
|
||||||
@ -2629,7 +2680,7 @@ async def test_cover_tilt_position_range(hass):
|
|||||||
"cover.set_cover_tilt_position",
|
"cover.set_cover_tilt_position",
|
||||||
hass,
|
hass,
|
||||||
payload={"rangeValue": "50"},
|
payload={"rangeValue": "50"},
|
||||||
instance="cover.tilt_position",
|
instance="cover.tilt",
|
||||||
)
|
)
|
||||||
assert call.data["position"] == 50
|
assert call.data["position"] == 50
|
||||||
|
|
||||||
@ -2640,7 +2691,7 @@ async def test_cover_tilt_position_range(hass):
|
|||||||
"cover.close_cover_tilt",
|
"cover.close_cover_tilt",
|
||||||
hass,
|
hass,
|
||||||
payload={"rangeValue": "0"},
|
payload={"rangeValue": "0"},
|
||||||
instance="cover.tilt_position",
|
instance="cover.tilt",
|
||||||
)
|
)
|
||||||
properties = msg["context"]["properties"][0]
|
properties = msg["context"]["properties"][0]
|
||||||
assert properties["name"] == "rangeValue"
|
assert properties["name"] == "rangeValue"
|
||||||
@ -2654,7 +2705,7 @@ async def test_cover_tilt_position_range(hass):
|
|||||||
"cover.open_cover_tilt",
|
"cover.open_cover_tilt",
|
||||||
hass,
|
hass,
|
||||||
payload={"rangeValue": "100"},
|
payload={"rangeValue": "100"},
|
||||||
instance="cover.tilt_position",
|
instance="cover.tilt",
|
||||||
)
|
)
|
||||||
properties = msg["context"]["properties"][0]
|
properties = msg["context"]["properties"][0]
|
||||||
assert properties["name"] == "rangeValue"
|
assert properties["name"] == "rangeValue"
|
||||||
@ -2670,12 +2721,12 @@ async def test_cover_tilt_position_range(hass):
|
|||||||
False,
|
False,
|
||||||
"cover.set_cover_tilt_position",
|
"cover.set_cover_tilt_position",
|
||||||
"tilt_position",
|
"tilt_position",
|
||||||
instance="cover.tilt_position",
|
instance="cover.tilt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_cover_semantics(hass):
|
async def test_cover_semantics_position_and_tilt(hass):
|
||||||
"""Test cover discovery and semantics."""
|
"""Test cover discovery and semantics with position and tilt support."""
|
||||||
device = (
|
device = (
|
||||||
"cover.test_semantics",
|
"cover.test_semantics",
|
||||||
"open",
|
"open",
|
||||||
@ -2697,50 +2748,57 @@ async def test_cover_semantics(hass):
|
|||||||
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa"
|
||||||
)
|
)
|
||||||
|
|
||||||
for range_instance in ("cover.position", "cover.tilt_position"):
|
# Assert for Position Semantics
|
||||||
range_capability = get_capability(
|
position_capability = get_capability(
|
||||||
capabilities, "Alexa.RangeController", range_instance
|
capabilities, "Alexa.RangeController", "cover.position"
|
||||||
)
|
)
|
||||||
semantics = range_capability["semantics"]
|
position_semantics = position_capability["semantics"]
|
||||||
assert semantics is not None
|
assert position_semantics is not None
|
||||||
|
|
||||||
action_mappings = semantics["actionMappings"]
|
position_action_mappings = position_semantics["actionMappings"]
|
||||||
assert action_mappings is not None
|
assert position_action_mappings is not None
|
||||||
if range_instance == "cover.position":
|
assert {
|
||||||
assert {
|
"@type": "ActionsToDirective",
|
||||||
"@type": "ActionsToDirective",
|
"actions": ["Alexa.Actions.Lower"],
|
||||||
"actions": ["Alexa.Actions.Lower"],
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
||||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}},
|
} in position_action_mappings
|
||||||
} in action_mappings
|
assert {
|
||||||
assert {
|
"@type": "ActionsToDirective",
|
||||||
"@type": "ActionsToDirective",
|
"actions": ["Alexa.Actions.Raise"],
|
||||||
"actions": ["Alexa.Actions.Raise"],
|
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
} in position_action_mappings
|
||||||
} 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
|
|
||||||
|
|
||||||
state_mappings = semantics["stateMappings"]
|
# Assert for Tilt Semantics
|
||||||
assert state_mappings is not None
|
tilt_capability = get_capability(
|
||||||
assert {
|
capabilities, "Alexa.RangeController", "cover.tilt"
|
||||||
"@type": "StatesToValue",
|
)
|
||||||
"states": ["Alexa.States.Closed"],
|
tilt_semantics = tilt_capability["semantics"]
|
||||||
"value": 0,
|
assert tilt_semantics is not None
|
||||||
} in state_mappings
|
tilt_action_mappings = tilt_semantics["actionMappings"]
|
||||||
assert {
|
assert tilt_action_mappings is not None
|
||||||
"@type": "StatesToRange",
|
assert {
|
||||||
"states": ["Alexa.States.Open"],
|
"@type": "ActionsToDirective",
|
||||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
"actions": ["Alexa.Actions.Close"],
|
||||||
} in state_mappings
|
"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):
|
async def test_input_number(hass):
|
||||||
|
@ -5,7 +5,7 @@ import pytest
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import deconz
|
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
|
from .test_gateway import BRIDGEID, setup_deconz_integration
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ async def test_configure_service_with_field(hass):
|
|||||||
|
|
||||||
data = {
|
data = {
|
||||||
deconz.services.SERVICE_FIELD: "/light/2",
|
deconz.services.SERVICE_FIELD: "/light/2",
|
||||||
CONF_BRIDGEID: BRIDGEID,
|
CONF_BRIDGE_ID: BRIDGEID,
|
||||||
deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
|
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."""
|
"""Test that service can refresh devices."""
|
||||||
gateway = await setup_deconz_integration(hass)
|
gateway = await setup_deconz_integration(hass)
|
||||||
|
|
||||||
data = {CONF_BRIDGEID: BRIDGEID}
|
data = {CONF_BRIDGE_ID: BRIDGEID}
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pydeconz.DeconzSession.request",
|
"pydeconz.DeconzSession.request",
|
||||||
|
@ -29,12 +29,14 @@ async def test_setup_defined_hosts_known_auth(hass):
|
|||||||
hue.DOMAIN,
|
hue.DOMAIN,
|
||||||
{
|
{
|
||||||
hue.DOMAIN: {
|
hue.DOMAIN: {
|
||||||
hue.CONF_BRIDGES: {
|
hue.CONF_BRIDGES: [
|
||||||
hue.CONF_HOST: "0.0.0.0",
|
{
|
||||||
hue.CONF_ALLOW_HUE_GROUPS: False,
|
hue.CONF_HOST: "0.0.0.0",
|
||||||
hue.CONF_ALLOW_UNREACHABLE: True,
|
hue.CONF_ALLOW_HUE_GROUPS: False,
|
||||||
"filename": "bla",
|
hue.CONF_ALLOW_UNREACHABLE: True,
|
||||||
}
|
},
|
||||||
|
{hue.CONF_HOST: "1.1.1.1", "filename": "bla"},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -42,7 +44,7 @@ async def test_setup_defined_hosts_known_auth(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Flow started for discovered bridge
|
# 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.
|
# Config stored for domain.
|
||||||
assert hass.data[hue.DATA_CONFIGS] == {
|
assert hass.data[hue.DATA_CONFIGS] == {
|
||||||
@ -50,8 +52,13 @@ async def test_setup_defined_hosts_known_auth(hass):
|
|||||||
hue.CONF_HOST: "0.0.0.0",
|
hue.CONF_HOST: "0.0.0.0",
|
||||||
hue.CONF_ALLOW_HUE_GROUPS: False,
|
hue.CONF_ALLOW_HUE_GROUPS: False,
|
||||||
hue.CONF_ALLOW_UNREACHABLE: True,
|
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",
|
"filename": "bla",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user