Merge pull request #30925 from home-assistant/rc

0.104.2
This commit is contained in:
Paulus Schoutsen 2020-01-17 16:32:18 -08:00 committed by GitHub
commit 7ef7d1dfd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 332 additions and 158 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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