mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 19:57:07 +00:00
commit
7ef7d1dfd0
@ -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,20 +1192,18 @@ 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()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
|
||||
)
|
||||
|
||||
# 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}",
|
||||
@ -1205,6 +1212,18 @@ class AlexaModeController(AlexaCapability):
|
||||
[AlexaSemantics.STATES_OPEN],
|
||||
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels,
|
||||
"SetMode",
|
||||
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
raise_labels,
|
||||
"SetMode",
|
||||
{"mode": 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()
|
||||
self._semantics.add_action_to_directive(
|
||||
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
|
||||
|
||||
# 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_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
|
||||
)
|
||||
|
||||
self._semantics.add_action_to_directive(
|
||||
lower_labels, "SetRangeValue", {"rangeValue": 0}
|
||||
)
|
||||
self._semantics.add_action_to_directive(
|
||||
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
|
||||
"""
|
||||
|
||||
|
@ -404,9 +404,7 @@ class CoverCapabilities(AlexaEntity):
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}"
|
||||
)
|
||||
if supported & cover.SUPPORT_SET_TILT_POSITION:
|
||||
yield AlexaRangeController(
|
||||
self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}"
|
||||
)
|
||||
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.hass)
|
||||
|
||||
|
@ -1118,8 +1118,8 @@ async def async_api_set_range(hass, config, directive, context):
|
||||
service = cover.SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = range_value
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_value = int(range_value)
|
||||
if range_value == 0:
|
||||
service = cover.SERVICE_CLOSE_COVER_TILT
|
||||
@ -1192,8 +1192,8 @@ async def async_api_adjust_range(hass, config, directive, context):
|
||||
100, max(0, range_delta + current)
|
||||
)
|
||||
|
||||
# Cover Tilt Position
|
||||
elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
|
||||
# Cover Tilt
|
||||
elif instance == f"{cover.DOMAIN}.tilt":
|
||||
range_delta = int(range_delta)
|
||||
service = SERVICE_SET_COVER_TILT_POSITION
|
||||
current = entity.attributes.get(cover.ATTR_TILT_POSITION)
|
||||
|
@ -190,7 +190,12 @@ class AlexaGlobalCatalog:
|
||||
|
||||
|
||||
class AlexaCapabilityResource:
|
||||
"""Base class for Alexa capabilityResources, ModeResources, and presetResources objects.
|
||||
"""Base class for Alexa capabilityResources, modeResources, and presetResources objects.
|
||||
|
||||
Resources objects labels must be unique across all modeResources and presetResources within the same device.
|
||||
To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array.
|
||||
You cannot use any words from the following list as friendly names:
|
||||
https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources
|
||||
"""
|
||||
@ -312,6 +317,14 @@ class AlexaSemantics:
|
||||
|
||||
Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController.
|
||||
|
||||
Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has
|
||||
multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail.
|
||||
|
||||
You can support semantics actionMappings on different controllers for the same device, however each controller must
|
||||
support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController,
|
||||
but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported
|
||||
for one interface on the same device.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object
|
||||
"""
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""Support for deCONZ devices."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import _UNDEF
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
from .config_flow import get_master_gateway
|
||||
from .const import CONF_MASTER_GATEWAY, DOMAIN
|
||||
from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN
|
||||
from .gateway import DeconzGateway
|
||||
from .services import async_setup_services, async_unload_services
|
||||
|
||||
@ -37,8 +38,14 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
# 0.104 introduced config entry unique id, this makes upgrading possible
|
||||
if config_entry.unique_id is None:
|
||||
|
||||
new_data = _UNDEF
|
||||
if CONF_BRIDGE_ID in config_entry.data:
|
||||
new_data = dict(config_entry.data)
|
||||
new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID]
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=gateway.api.config.bridgeid
|
||||
config_entry, unique_id=gateway.api.config.bridgeid, data=new_data
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][config_entry.unique_id] = gateway
|
||||
|
@ -54,11 +54,13 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
|
||||
"""Representation of a deCONZ binary sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"on", "reachable", "state"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers import aiohttp_client
|
||||
from .const import (
|
||||
CONF_ALLOW_CLIP_SENSOR,
|
||||
CONF_ALLOW_DECONZ_GROUPS,
|
||||
CONF_BRIDGEID,
|
||||
CONF_BRIDGE_ID,
|
||||
DEFAULT_ALLOW_CLIP_SENSOR,
|
||||
DEFAULT_ALLOW_DECONZ_GROUPS,
|
||||
DEFAULT_PORT,
|
||||
@ -74,7 +74,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
for bridge in self.bridges:
|
||||
if bridge[CONF_HOST] == user_input[CONF_HOST]:
|
||||
self.bridge_id = bridge[CONF_BRIDGEID]
|
||||
self.bridge_id = bridge[CONF_BRIDGE_ID]
|
||||
self.deconz_config = {
|
||||
CONF_HOST: bridge[CONF_HOST],
|
||||
CONF_PORT: bridge[CONF_PORT],
|
||||
|
@ -5,7 +5,8 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "deconz"
|
||||
|
||||
CONF_BRIDGEID = "bridgeid"
|
||||
CONF_BRIDGE_ID = "bridgeid"
|
||||
CONF_GROUP_ID_BASE = "group_id_base"
|
||||
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_ALLOW_CLIP_SENSOR = False
|
||||
|
@ -91,8 +91,11 @@ class DeconzDevice(DeconzBase, Entity):
|
||||
unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the device's state."""
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -39,16 +39,20 @@ 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:
|
||||
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):
|
||||
|
@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import (
|
||||
CONF_GROUP_ID_BASE,
|
||||
COVER_TYPES,
|
||||
DOMAIN as DECONZ_DOMAIN,
|
||||
NEW_GROUP,
|
||||
@ -205,7 +206,11 @@ class DeconzGroup(DeconzLight):
|
||||
"""Set up group and create an unique id."""
|
||||
super().__init__(device, gateway)
|
||||
|
||||
self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}"
|
||||
group_id_base = self.gateway.config_entry.unique_id
|
||||
if CONF_GROUP_ID_BASE in self.gateway.config_entry.data:
|
||||
group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE]
|
||||
|
||||
self._unique_id = f"{group_id_base}-{self._device.deconz_id}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -3,13 +3,17 @@
|
||||
"name": "deCONZ",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/deconz",
|
||||
"requirements": ["pydeconz==67"],
|
||||
"requirements": [
|
||||
"pydeconz==68"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Royal Philips Electronics"
|
||||
}
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@kane610"],
|
||||
"codeowners": [
|
||||
"@kane610"
|
||||
],
|
||||
"quality_scale": "platinum"
|
||||
}
|
@ -97,11 +97,13 @@ class DeconzSensor(DeconzDevice):
|
||||
"""Representation of a deCONZ sensor."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the sensor's state."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"on", "reachable", "state"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@ -155,11 +157,13 @@ class DeconzBattery(DeconzDevice):
|
||||
"""Battery class for when a device is only represented as an event."""
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, force_update=False):
|
||||
def async_update_callback(self, force_update=False, ignore_update=False):
|
||||
"""Update the battery's state, if needed."""
|
||||
changed = set(self._device.changed_keys)
|
||||
if ignore_update:
|
||||
return
|
||||
|
||||
keys = {"battery", "reachable"}
|
||||
if force_update or any(key in changed for key in keys):
|
||||
if force_update or self._device.changed_keys.intersection(keys):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
@ -217,7 +221,7 @@ class DeconzSensorStateTracker:
|
||||
self.sensor = None
|
||||
|
||||
@callback
|
||||
def async_update_callback(self):
|
||||
def async_update_callback(self, ignore_update=False):
|
||||
"""Sensor state updated."""
|
||||
if "battery" in self.sensor.changed_keys:
|
||||
async_dispatcher_send(
|
||||
|
@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from .config_flow import get_master_gateway
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
CONF_BRIDGEID,
|
||||
CONF_BRIDGE_ID,
|
||||
DOMAIN,
|
||||
NEW_GROUP,
|
||||
NEW_LIGHT,
|
||||
@ -27,14 +27,14 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All(
|
||||
vol.Optional(SERVICE_ENTITY): cv.entity_id,
|
||||
vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"),
|
||||
vol.Required(SERVICE_DATA): dict,
|
||||
vol.Optional(CONF_BRIDGEID): str,
|
||||
vol.Optional(CONF_BRIDGE_ID): str,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD),
|
||||
)
|
||||
|
||||
SERVICE_DEVICE_REFRESH = "device_refresh"
|
||||
SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str}))
|
||||
SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str}))
|
||||
|
||||
|
||||
async def async_setup_services(hass):
|
||||
@ -97,7 +97,7 @@ async def async_configure_service(hass, data):
|
||||
See Dresden Elektroniks REST API documentation for details:
|
||||
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
|
||||
"""
|
||||
bridgeid = data.get(CONF_BRIDGEID)
|
||||
bridgeid = data.get(CONF_BRIDGE_ID)
|
||||
field = data.get(SERVICE_FIELD, "")
|
||||
entity_id = data.get(SERVICE_ENTITY)
|
||||
data = data[SERVICE_DATA]
|
||||
@ -119,15 +119,15 @@ async def async_configure_service(hass, data):
|
||||
async def async_refresh_devices_service(hass, data):
|
||||
"""Refresh available devices from deCONZ."""
|
||||
gateway = get_master_gateway(hass)
|
||||
if CONF_BRIDGEID in data:
|
||||
gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]]
|
||||
if CONF_BRIDGE_ID in data:
|
||||
gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]]
|
||||
|
||||
groups = set(gateway.api.groups.keys())
|
||||
lights = set(gateway.api.lights.keys())
|
||||
scenes = set(gateway.api.scenes.keys())
|
||||
sensors = set(gateway.api.sensors.keys())
|
||||
|
||||
await gateway.api.refresh_state()
|
||||
await gateway.api.refresh_state(ignore_update=True)
|
||||
|
||||
gateway.async_add_device_callback(
|
||||
NEW_GROUP,
|
||||
|
@ -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"
|
||||
}
|
@ -13,7 +13,7 @@ from homematicip.aio.device import (
|
||||
AsyncPrintedCircuitBoardSwitch2,
|
||||
AsyncPrintedCircuitBoardSwitchBattery,
|
||||
)
|
||||
from homematicip.aio.group import AsyncSwitchingGroup
|
||||
from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -67,7 +67,7 @@ async def async_setup_entry(
|
||||
entities.append(HomematicipMultiSwitch(hap, device, channel))
|
||||
|
||||
for group in hap.home.groups:
|
||||
if isinstance(group, AsyncSwitchingGroup):
|
||||
if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)):
|
||||
entities.append(HomematicipGroupSwitch(hap, group))
|
||||
|
||||
if entities:
|
||||
|
@ -36,6 +36,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(
|
||||
CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS
|
||||
): cv.boolean,
|
||||
vol.Optional("filename"): str,
|
||||
}
|
||||
)
|
||||
|
||||
@ -46,8 +47,10 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_BRIDGES): vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.All(
|
||||
cv.deprecated("filename", invalidation_version="0.106.0"),
|
||||
vol.All(BRIDGE_CONFIG_SCHEMA),
|
||||
BRIDGE_CONFIG_SCHEMA,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ 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
|
||||
@ -146,9 +147,15 @@ class RingCam(RingEntityMixin, Camera):
|
||||
):
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
if video_url:
|
||||
self._last_video_id = self._last_event["id"]
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 104
|
||||
PATCH_VERSION = "1"
|
||||
PATCH_VERSION = "2"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
# Assert for Position Semantics
|
||||
position_capability = get_capability(
|
||||
capabilities, "Alexa.RangeController", "cover.position"
|
||||
)
|
||||
semantics = range_capability["semantics"]
|
||||
assert semantics is not None
|
||||
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":
|
||||
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 action_mappings
|
||||
} in position_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Raise"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
elif range_instance == "cover.position":
|
||||
} in position_action_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 action_mappings
|
||||
} in tilt_action_mappings
|
||||
assert {
|
||||
"@type": "ActionsToDirective",
|
||||
"actions": ["Alexa.Actions.Open"],
|
||||
"directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}},
|
||||
} in action_mappings
|
||||
} in tilt_action_mappings
|
||||
|
||||
state_mappings = semantics["stateMappings"]
|
||||
assert state_mappings is not None
|
||||
tilt_state_mappings = tilt_semantics["stateMappings"]
|
||||
assert tilt_state_mappings is not None
|
||||
assert {
|
||||
"@type": "StatesToValue",
|
||||
"states": ["Alexa.States.Closed"],
|
||||
"value": 0,
|
||||
} in state_mappings
|
||||
} in tilt_state_mappings
|
||||
assert {
|
||||
"@type": "StatesToRange",
|
||||
"states": ["Alexa.States.Open"],
|
||||
"range": {"minimumValue": 1, "maximumValue": 100},
|
||||
} in state_mappings
|
||||
} in tilt_state_mappings
|
||||
|
||||
|
||||
async def test_input_number(hass):
|
||||
|
@ -5,7 +5,7 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import deconz
|
||||
from homeassistant.components.deconz.const import CONF_BRIDGEID
|
||||
from homeassistant.components.deconz.const import CONF_BRIDGE_ID
|
||||
|
||||
from .test_gateway import BRIDGEID, setup_deconz_integration
|
||||
|
||||
@ -91,7 +91,7 @@ async def test_configure_service_with_field(hass):
|
||||
|
||||
data = {
|
||||
deconz.services.SERVICE_FIELD: "/light/2",
|
||||
CONF_BRIDGEID: BRIDGEID,
|
||||
CONF_BRIDGE_ID: BRIDGEID,
|
||||
deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20},
|
||||
}
|
||||
|
||||
@ -180,7 +180,7 @@ async def test_service_refresh_devices(hass):
|
||||
"""Test that service can refresh devices."""
|
||||
gateway = await setup_deconz_integration(hass)
|
||||
|
||||
data = {CONF_BRIDGEID: BRIDGEID}
|
||||
data = {CONF_BRIDGE_ID: BRIDGEID}
|
||||
|
||||
with patch(
|
||||
"pydeconz.DeconzSession.request",
|
||||
|
@ -29,12 +29,14 @@ async def test_setup_defined_hosts_known_auth(hass):
|
||||
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_ALLOW_UNREACHABLE: True,
|
||||
"filename": "bla",
|
||||
}
|
||||
},
|
||||
{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
|
||||
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] == {
|
||||
@ -50,8 +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",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user