Skip to content
Merged

0.104.2 #30925

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 71 additions & 23 deletions homeassistant/components/alexa/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,15 @@ def configuration(self):
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
"""

Expand Down Expand Up @@ -1183,28 +1192,38 @@ def capability_resources(self):

def semantics(self):
"""Build and return semantics object."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)

# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()

# Add open/close semantics if tilt is not supported.
if not supported & cover.SUPPORT_SET_TILT_POSITION:
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
raise_labels.append(AlexaSemantics.ACTION_OPEN)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
)

self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER],
lower_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"},
)
self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE],
raise_labels,
"SetMode",
{"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"},
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED],
f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}",
)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_OPEN],
f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}",
)

return self._semantics.serialize_semantics()

return None
Expand All @@ -1213,6 +1232,15 @@ def semantics(self):
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
"""

Expand Down Expand Up @@ -1268,8 +1296,8 @@ def get_property(self, name):
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
Expand Down Expand Up @@ -1321,10 +1349,10 @@ def capability_resources(self):
)
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,
Expand Down Expand Up @@ -1358,24 +1386,35 @@ def capability_resources(self):

def semantics(self):
"""Build and return semantics object."""
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)

# Cover Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
lower_labels = [AlexaSemantics.ACTION_LOWER]
raise_labels = [AlexaSemantics.ACTION_RAISE]
self._semantics = AlexaSemantics()

# Add open/close semantics if tilt is not supported.
if not supported & cover.SUPPORT_SET_TILT_POSITION:
lower_labels.append(AlexaSemantics.ACTION_CLOSE)
raise_labels.append(AlexaSemantics.ACTION_OPEN)
self._semantics.add_states_to_value(
[AlexaSemantics.STATES_CLOSED], value=0
)
self._semantics.add_states_to_range(
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
)

self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0}
lower_labels, "SetRangeValue", {"rangeValue": 0}
)
self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100}
)
self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0)
self._semantics.add_states_to_range(
[AlexaSemantics.STATES_OPEN], min_value=1, max_value=100
raise_labels, "SetRangeValue", {"rangeValue": 100}
)
return self._semantics.serialize_semantics()

# Cover Tilt Position
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}":
# Cover Tilt
if self.instance == f"{cover.DOMAIN}.tilt":
self._semantics = AlexaSemantics()
self._semantics.add_action_to_directive(
[AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0}
Expand All @@ -1395,6 +1434,15 @@ def semantics(self):
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
"""

Expand Down
4 changes: 1 addition & 3 deletions homeassistant/components/alexa/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,7 @@ def interfaces(self):
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)

Expand Down
8 changes: 4 additions & 4 deletions homeassistant/components/alexa/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion homeassistant/components/alexa/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
"""

Expand Down
11 changes: 9 additions & 2 deletions homeassistant/components/deconz/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions homeassistant/components/deconz/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/deconz/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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,
Expand Down Expand Up @@ -74,7 +74,7 @@ async def async_step_user(self, user_input=None):
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],
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/deconz/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

DOMAIN = "deconz"

CONF_BRIDGEID = "bridgeid"
CONF_BRIDGE_ID = "bridgeid"
CONF_GROUP_ID_BASE = "group_id_base"

DEFAULT_PORT = 80
DEFAULT_ALLOW_CLIP_SENSOR = False
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/deconz/deconz_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,11 @@ async def async_will_remove_from_hass(self) -> None:
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
Expand Down
24 changes: 14 additions & 10 deletions homeassistant/components/deconz/deconz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,21 @@ def async_will_remove_from_hass(self) -> None:
self._device = None

@callback
def async_update_callback(self, force_update=False):
def async_update_callback(self, force_update=False, ignore_update=False):
"""Fire the event if reason is that state is updated."""
if "state" in self._device.changed_keys:
data = {
CONF_ID: self.event_id,
CONF_UNIQUE_ID: self.serial,
CONF_EVENT: self._device.state,
}
if self._device.gesture:
data[CONF_GESTURE] = self._device.gesture
self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)
if ignore_update or "state" not in self._device.changed_keys:
return

data = {
CONF_ID: self.event_id,
CONF_UNIQUE_ID: self.serial,
CONF_EVENT: self._device.state,
}

if self._device.gesture:
data[CONF_GESTURE] = self._device.gesture

self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data)

async def async_update_device_registry(self):
"""Update device registry."""
Expand Down
7 changes: 6 additions & 1 deletion homeassistant/components/deconz/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import homeassistant.util.color as color_util

from .const import (
CONF_GROUP_ID_BASE,
COVER_TYPES,
DOMAIN as DECONZ_DOMAIN,
NEW_GROUP,
Expand Down Expand Up @@ -205,7 +206,11 @@ def __init__(self, device, gateway):
"""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):
Expand Down
10 changes: 7 additions & 3 deletions homeassistant/components/deconz/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Loading