Bump zwave-js-server-python to 0.21.0 (#47408)

Co-authored-by: Tobias Sauerwein <cgtobi@users.noreply.github.com>
This commit is contained in:
Raman Gupta 2021-03-04 19:15:50 -05:00 committed by GitHub
parent a1faba29f0
commit ee69e93b46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 352 additions and 118 deletions

View File

@ -48,7 +48,8 @@ from .const import (
ZWAVE_JS_EVENT,
)
from .discovery import async_discover_values
from .helpers import get_device_id, get_old_value_id, get_unique_id
from .helpers import get_device_id
from .migrate import async_migrate_discovered_value
from .services import ZWaveServices
CONNECT_TIMEOUT = 10
@ -98,31 +99,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
dev_reg = await device_registry.async_get_registry(hass)
ent_reg = entity_registry.async_get(hass)
@callback
def migrate_entity(platform: str, old_unique_id: str, new_unique_id: str) -> None:
"""Check if entity with old unique ID exists, and if so migrate it to new ID."""
if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id):
LOGGER.debug(
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
entity_id,
old_unique_id,
new_unique_id,
)
try:
ent_reg.async_update_entity(
entity_id,
new_unique_id=new_unique_id,
)
except ValueError:
LOGGER.debug(
(
"Entity %s can't be migrated because the unique ID is taken. "
"Cleaning it up since it is likely no longer valid."
),
entity_id,
)
ent_reg.async_remove(entity_id)
@callback
def async_on_node_ready(node: ZwaveNode) -> None:
"""Handle node ready event."""
@ -136,49 +112,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
LOGGER.debug("Discovered entity: %s", disc_info)
# This migration logic was added in 2021.3 to handle a breaking change to
# the value_id format. Some time in the future, this code block
# (as well as get_old_value_id helper and migrate_entity closure) can be
# removed.
value_ids = [
# 2021.2.* format
get_old_value_id(disc_info.primary_value),
# 2021.3.0b0 format
disc_info.primary_value.value_id,
]
new_unique_id = get_unique_id(
client.driver.controller.home_id,
disc_info.primary_value.value_id,
)
for value_id in value_ids:
old_unique_id = get_unique_id(
client.driver.controller.home_id,
f"{disc_info.primary_value.node.node_id}.{value_id}",
)
# Most entities have the same ID format, but notification binary sensors
# have a state key in their ID so we need to handle them differently
if (
disc_info.platform == "binary_sensor"
and disc_info.platform_hint == "notification"
):
for state_key in disc_info.primary_value.metadata.states:
# ignore idle key (0)
if state_key == "0":
continue
migrate_entity(
disc_info.platform,
f"{old_unique_id}.{state_key}",
f"{new_unique_id}.{state_key}",
)
# Once we've iterated through all state keys, we can move on to the
# next item
continue
migrate_entity(disc_info.platform, old_unique_id, new_unique_id)
# the value_id format. Some time in the future, this call (as well as the
# helper functions) can be removed.
async_migrate_discovered_value(ent_reg, client, disc_info)
async_dispatcher_send(
hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info
)

View File

@ -125,18 +125,10 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity):
)
self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {}
for enum in ThermostatSetpointType:
# Some devices don't include a property key so we need to check for value
# ID's, both with and without the property key
self._setpoint_values[enum] = self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT,
value_property_key=enum.value.key,
value_property_key_name=enum.value.name,
add_to_watched_value_ids=True,
) or self.get_zwave_value(
THERMOSTAT_SETPOINT_PROPERTY,
command_class=CommandClass.THERMOSTAT_SETPOINT,
value_property_key_name=enum.value.name,
add_to_watched_value_ids=True,
)
# Use the first found setpoint value to always determine the temperature unit

View File

@ -169,7 +169,6 @@ class ZWaveBaseEntity(Entity):
command_class: Optional[int] = None,
endpoint: Optional[int] = None,
value_property_key: Optional[int] = None,
value_property_key_name: Optional[str] = None,
add_to_watched_value_ids: bool = True,
check_all_endpoints: bool = False,
) -> Optional[ZwaveValue]:
@ -188,7 +187,6 @@ class ZWaveBaseEntity(Entity):
value_property,
endpoint=endpoint,
property_key=value_property_key,
property_key_name=value_property_key_name,
)
return_value = self.info.node.values.get(value_id)
@ -203,7 +201,6 @@ class ZWaveBaseEntity(Entity):
value_property,
endpoint=endpoint_.index,
property_key=value_property_key,
property_key_name=value_property_key_name,
)
return_value = self.info.node.values.get(value_id)
if return_value:

View File

@ -3,7 +3,6 @@ from typing import List, Tuple, cast
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@ -13,16 +12,6 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg
from .const import DATA_CLIENT, DOMAIN
@callback
def get_old_value_id(value: ZwaveValue) -> str:
"""Get old value ID so we can migrate entity unique ID."""
command_class = value.command_class
endpoint = value.endpoint or "00"
property_ = value.property_
property_key_name = value.property_key_name or "00"
return f"{value.node.node_id}-{command_class}-{endpoint}-{property_}-{property_key_name}"
@callback
def get_unique_id(home_id: str, value_id: str) -> str:
"""Get unique ID from home ID and value ID."""

View File

@ -228,7 +228,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"targetColor",
CommandClass.SWITCH_COLOR,
value_property_key=None,
value_property_key_name=None,
)
if combined_color_val and isinstance(combined_color_val.value, dict):
colors_dict = {}
@ -252,7 +251,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"targetColor",
CommandClass.SWITCH_COLOR,
value_property_key=property_key.key,
value_property_key_name=property_key.name,
)
if target_zwave_value is None:
# guard for unsupported color
@ -318,31 +316,26 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.RED.value.key,
value_property_key_name=ColorComponent.RED.value.name,
)
green_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.GREEN.value.key,
value_property_key_name=ColorComponent.GREEN.value.name,
)
blue_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.BLUE.value.key,
value_property_key_name=ColorComponent.BLUE.value.name,
)
ww_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.WARM_WHITE.value.key,
value_property_key_name=ColorComponent.WARM_WHITE.value.name,
)
cw_val = self.get_zwave_value(
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=ColorComponent.COLD_WHITE.value.key,
value_property_key_name=ColorComponent.COLD_WHITE.value.name,
)
# prefer the (new) combined color property
# https://github.com/zwave-js/node-zwave-js/pull/1782
@ -350,7 +343,6 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity):
"currentColor",
CommandClass.SWITCH_COLOR,
value_property_key=None,
value_property_key_name=None,
)
if combined_color_val and isinstance(combined_color_val.value, dict):
multi_color = combined_color_val.value

View File

@ -3,7 +3,7 @@
"name": "Z-Wave JS",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
"requirements": ["zwave-js-server-python==0.20.1"],
"requirements": ["zwave-js-server-python==0.21.0"],
"codeowners": ["@home-assistant/z-wave"],
"dependencies": ["http", "websocket_api"]
}

View File

@ -0,0 +1,113 @@
"""Functions used to migrate unique IDs for Z-Wave JS entities."""
import logging
from typing import List
from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.value import Value as ZwaveValue
from homeassistant.core import callback
from homeassistant.helpers.entity_registry import EntityRegistry
from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .helpers import get_unique_id
_LOGGER = logging.getLogger(__name__)
@callback
def async_migrate_entity(
ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str
) -> None:
"""Check if entity with old unique ID exists, and if so migrate it to new ID."""
if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id):
_LOGGER.debug(
"Migrating entity %s from old unique ID '%s' to new unique ID '%s'",
entity_id,
old_unique_id,
new_unique_id,
)
try:
ent_reg.async_update_entity(
entity_id,
new_unique_id=new_unique_id,
)
except ValueError:
_LOGGER.debug(
(
"Entity %s can't be migrated because the unique ID is taken. "
"Cleaning it up since it is likely no longer valid."
),
entity_id,
)
ent_reg.async_remove(entity_id)
@callback
def async_migrate_discovered_value(
ent_reg: EntityRegistry, client: ZwaveClient, disc_info: ZwaveDiscoveryInfo
) -> None:
"""Migrate unique ID for entity/entities tied to discovered value."""
new_unique_id = get_unique_id(
client.driver.controller.home_id,
disc_info.primary_value.value_id,
)
# 2021.2.*, 2021.3.0b0, and 2021.3.0 formats
for value_id in get_old_value_ids(disc_info.primary_value):
old_unique_id = get_unique_id(
client.driver.controller.home_id,
value_id,
)
# Most entities have the same ID format, but notification binary sensors
# have a state key in their ID so we need to handle them differently
if (
disc_info.platform == "binary_sensor"
and disc_info.platform_hint == "notification"
):
for state_key in disc_info.primary_value.metadata.states:
# ignore idle key (0)
if state_key == "0":
continue
async_migrate_entity(
ent_reg,
disc_info.platform,
f"{old_unique_id}.{state_key}",
f"{new_unique_id}.{state_key}",
)
# Once we've iterated through all state keys, we can move on to the
# next item
continue
async_migrate_entity(ent_reg, disc_info.platform, old_unique_id, new_unique_id)
@callback
def get_old_value_ids(value: ZwaveValue) -> List[str]:
"""Get old value IDs so we can migrate entity unique ID."""
value_ids = []
# Pre 2021.3.0 value ID
command_class = value.command_class
endpoint = value.endpoint or "00"
property_ = value.property_
property_key_name = value.property_key_name or "00"
value_ids.append(
f"{value.node.node_id}.{value.node.node_id}-{command_class}-{endpoint}-"
f"{property_}-{property_key_name}"
)
endpoint = "00" if value.endpoint is None else value.endpoint
property_key = "00" if value.property_key is None else value.property_key
property_key_name = value.property_key_name or "00"
value_id = (
f"{value.node.node_id}-{command_class}-{endpoint}-"
f"{property_}-{property_key}-{property_key_name}"
)
# 2021.3.0b0 and 2021.3.0 value IDs
value_ids.extend([f"{value.node.node_id}.{value_id}", value_id])
return value_ids

View File

@ -2394,4 +2394,4 @@ zigpy==0.32.0
zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.20.1
zwave-js-server-python==0.21.0

View File

@ -1231,4 +1231,4 @@ zigpy-znp==0.4.0
zigpy==0.32.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.20.1
zwave-js-server-python==0.21.0

View File

@ -71,7 +71,7 @@ async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client):
result = msg["result"]
assert len(result) == 61
key = "52-112-0-2-00-00"
key = "52-112-0-2"
assert result[key]["property"] == 2
assert result[key]["metadata"]["type"] == "number"
assert result[key]["configuration_value_type"] == "enumerated"

View File

@ -405,7 +405,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
assert state
assert state.state == HVAC_MODE_HEAT
assert state.attributes[ATTR_TEMPERATURE] == 25
assert state.attributes[ATTR_TEMPERATURE] == 14
assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT]
assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE
@ -432,6 +432,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
"commandClassName": "Thermostat Setpoint",
"property": "setpoint",
"propertyName": "setpoint",
"propertyKey": 1,
"propertyKeyName": "Heating",
"ccVersion": 2,
"metadata": {
@ -441,7 +442,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
"unit": "\u00b0C",
"ccSpecific": {"setpointType": 1},
},
"value": 25,
"value": 14,
}
assert args["value"] == 21.5
@ -459,6 +460,7 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat
"commandClass": 67,
"endpoint": 0,
"property": "setpoint",
"propertyKey": 1,
"propertyKeyName": "Heating",
"propertyName": "setpoint",
"newValue": 23,

View File

@ -160,7 +160,7 @@ async def test_unique_id_migration_dupes(
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00"
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
assert entity_entry.unique_id == new_unique_id
assert ent_reg.async_get(f"{AIR_TEMPERATURE_SENSOR}_1") is None
@ -195,7 +195,7 @@ async def test_unique_id_migration_v1(hass, multisensor_6_state, client, integra
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(AIR_TEMPERATURE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature-00-00"
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Air temperature"
assert entity_entry.unique_id == new_unique_id
@ -228,7 +228,147 @@ async def test_unique_id_migration_v2(hass, multisensor_6_state, client, integra
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00"
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
assert entity_entry.unique_id == new_unique_id
async def test_unique_id_migration_v3(hass, multisensor_6_state, client, integration):
"""Test unique ID is migrated from old format to new (version 3)."""
ent_reg = entity_registry.async_get(hass)
# Migrate version 2
ILLUMINANCE_SENSOR = "sensor.multisensor_6_illuminance"
entity_name = ILLUMINANCE_SENSOR.split(".")[1]
# Create entity RegistryEntry using old unique ID format
old_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance-00-00"
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id,
suggested_object_id=entity_name,
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == ILLUMINANCE_SENSOR
assert entity_entry.unique_id == old_unique_id
# Add a ready node, unique ID should be migrated
node = Node(client, multisensor_6_state)
event = {"node": node}
client.driver.controller.emit("node added", event)
await hass.async_block_till_done()
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(ILLUMINANCE_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-49-0-Illuminance"
assert entity_entry.unique_id == new_unique_id
async def test_unique_id_migration_property_key_v1(
hass, hank_binary_switch_state, client, integration
):
"""Test unique ID with property key is migrated from old format to new (version 1)."""
ent_reg = entity_registry.async_get(hass)
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
entity_name = SENSOR_NAME.split(".")[1]
# Create entity RegistryEntry using old unique ID format
old_unique_id = f"{client.driver.controller.home_id}.32.32-50-00-value-W_Consumed"
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id,
suggested_object_id=entity_name,
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == SENSOR_NAME
assert entity_entry.unique_id == old_unique_id
# Add a ready node, unique ID should be migrated
node = Node(client, hank_binary_switch_state)
event = {"node": node}
client.driver.controller.emit("node added", event)
await hass.async_block_till_done()
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(SENSOR_NAME)
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
assert entity_entry.unique_id == new_unique_id
async def test_unique_id_migration_property_key_v2(
hass, hank_binary_switch_state, client, integration
):
"""Test unique ID with property key is migrated from old format to new (version 2)."""
ent_reg = entity_registry.async_get(hass)
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
entity_name = SENSOR_NAME.split(".")[1]
# Create entity RegistryEntry using old unique ID format
old_unique_id = (
f"{client.driver.controller.home_id}.32.32-50-0-value-66049-W_Consumed"
)
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id,
suggested_object_id=entity_name,
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == SENSOR_NAME
assert entity_entry.unique_id == old_unique_id
# Add a ready node, unique ID should be migrated
node = Node(client, hank_binary_switch_state)
event = {"node": node}
client.driver.controller.emit("node added", event)
await hass.async_block_till_done()
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(SENSOR_NAME)
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
assert entity_entry.unique_id == new_unique_id
async def test_unique_id_migration_property_key_v3(
hass, hank_binary_switch_state, client, integration
):
"""Test unique ID with property key is migrated from old format to new (version 3)."""
ent_reg = entity_registry.async_get(hass)
SENSOR_NAME = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed"
entity_name = SENSOR_NAME.split(".")[1]
# Create entity RegistryEntry using old unique ID format
old_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049-W_Consumed"
entity_entry = ent_reg.async_get_or_create(
"sensor",
DOMAIN,
old_unique_id,
suggested_object_id=entity_name,
config_entry=integration,
original_name=entity_name,
)
assert entity_entry.entity_id == SENSOR_NAME
assert entity_entry.unique_id == old_unique_id
# Add a ready node, unique ID should be migrated
node = Node(client, hank_binary_switch_state)
event = {"node": node}
client.driver.controller.emit("node added", event)
await hass.async_block_till_done()
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(SENSOR_NAME)
new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049"
assert entity_entry.unique_id == new_unique_id
@ -262,7 +402,7 @@ async def test_unique_id_migration_notification_binary_sensor(
# Check that new RegistryEntry is using new unique ID format
entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR)
new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status-Motion sensor status.8"
new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8"
assert entity_entry.unique_id == new_unique_id

View File

@ -4,11 +4,25 @@
"status": 1,
"ready": true,
"deviceClass": {
"basic": {"key": 4, "label":"Routing Slave"},
"generic": {"key": 8, "label":"Thermostat"},
"specific": {"key": 4, "label":"Setpoint Thermostat"},
"mandatorySupportedCCs": [],
"mandatoryControlCCs": []
"basic": {
"key": 4,
"label": "Routing Slave"
},
"generic": {
"key": 8,
"label": "Thermostat"
},
"specific": {
"key": 4,
"label": "Setpoint Thermostat"
},
"mandatorySupportedCCs": [
114,
143,
67,
134
],
"mandatoryControlledCCs": []
},
"isListening": false,
"isFrequentListening": false,
@ -22,6 +36,7 @@
"productType": 5,
"firmwareVersion": "1.1",
"deviceConfig": {
"filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0002/lc-13.json",
"manufacturerId": 2,
"manufacturer": "Danfoss",
"label": "LC-13",
@ -66,19 +81,76 @@
14
],
"interviewAttempts": 1,
"interviewStage": 7,
"commandClasses": [
{
"id": 67,
"name": "Thermostat Setpoint",
"version": 2,
"isSecure": false
},
{
"id": 70,
"name": "Climate Control Schedule",
"version": 1,
"isSecure": false
},
{
"id": 114,
"name": "Manufacturer Specific",
"version": 1,
"isSecure": false
},
{
"id": 117,
"name": "Protection",
"version": 2,
"isSecure": false
},
{
"id": 128,
"name": "Battery",
"version": 1,
"isSecure": false
},
{
"id": 129,
"name": "Clock",
"version": 1,
"isSecure": false
},
{
"id": 132,
"name": "Wake Up",
"version": 1,
"isSecure": false
},
{
"id": 134,
"name": "Version",
"version": 1,
"isSecure": false
},
{
"id": 143,
"name": "Multi Command",
"version": 1,
"isSecure": false
}
],
"endpoints": [
{
"nodeId": 5,
"index": 0
}
],
"commandClasses": [],
"values": [
{
"endpoint": 0,
"commandClass": 67,
"commandClassName": "Thermostat Setpoint",
"property": "setpoint",
"propertyKey": 1,
"propertyName": "setpoint",
"propertyKeyName": "Heating",
"ccVersion": 2,
@ -91,7 +163,7 @@
"setpointType": 1
}
},
"value": 25
"value": 14
},
{
"endpoint": 0,
@ -262,7 +334,7 @@
"unit": "%",
"label": "Battery level"
},
"value": 53
"value": 49
},
{
"endpoint": 0,
@ -361,4 +433,4 @@
]
}
]
}
}

View File

@ -837,6 +837,7 @@
"commandClassName": "Thermostat Setpoint",
"property": "setpoint",
"propertyName": "setpoint",
"propertyKey": 1,
"propertyKeyName": "Heating",
"ccVersion": 3,
"metadata": {