diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index be6d9b698d453e0d5ba0a844ffc34721e804b3f9..62d45a5ae5e1564dbe73ae68113e29bf60753fe4 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -14,12 +14,14 @@ from homeassistant.core import callback class ZwaveDiscoveryInfo: """Info discovered from (primary) ZWave Value to create entity.""" - node: ZwaveNode # node to which the value(s) belongs - primary_value: ZwaveValue # the value object itself for primary value - platform: str # the home assistant platform for which an entity should be created - platform_hint: Optional[ - str - ] = "" # hint for the platform about this discovered entity + # node to which the value(s) belongs + node: ZwaveNode + # the value object itself for primary value + primary_value: ZwaveValue + # the home assistant platform for which an entity should be created + platform: str + # hint for the platform about this discovered entity + platform_hint: Optional[str] = "" @property def value_id(self) -> str: @@ -27,38 +29,140 @@ class ZwaveDiscoveryInfo: return f"{self.node.node_id}.{self.primary_value.value_id}" +@dataclass +class ZWaveValueDiscoverySchema: + """Z-Wave Value discovery schema. + + The Z-Wave Value must match these conditions. + Use the Z-Wave specifications to find out the values for these parameters: + https://github.com/zwave-js/node-zwave-js/tree/master/specs + """ + + # [optional] the value's command class must match ANY of these values + command_class: Optional[Set[int]] = None + # [optional] the value's endpoint must match ANY of these values + endpoint: Optional[Set[int]] = None + # [optional] the value's property must match ANY of these values + property: Optional[Set[Union[str, int]]] = None + # [optional] the value's metadata_type must match ANY of these values + type: Optional[Set[str]] = None + + @dataclass class ZWaveDiscoverySchema: """Z-Wave discovery schema. - The (primary) value for an entity must match these conditions. + The Z-Wave node and it's (primary) value for an entity must match these conditions. Use the Z-Wave specifications to find out the values for these parameters: https://github.com/zwave-js/node-zwave-js/tree/master/specs """ # specify the hass platform for which this scheme applies (e.g. light, sensor) platform: str + # primary value belonging to this discovery scheme + primary_value: ZWaveValueDiscoverySchema # [optional] hint for platform hint: Optional[str] = None + # [optional] the node's manufacturer_id must match ANY of these values + manufacturer_id: Optional[Set[int]] = None + # [optional] the node's product_id must match ANY of these values + product_id: Optional[Set[int]] = None + # [optional] the node's product_type must match ANY of these values + product_type: Optional[Set[int]] = None + # [optional] the node's firmware_version must match ANY of these values + firmware_version: Optional[Set[str]] = None # [optional] the node's basic device class must match ANY of these values device_class_basic: Optional[Set[str]] = None # [optional] the node's generic device class must match ANY of these values device_class_generic: Optional[Set[str]] = None # [optional] the node's specific device class must match ANY of these values device_class_specific: Optional[Set[str]] = None - # [optional] the value's command class must match ANY of these values - command_class: Optional[Set[int]] = None - # [optional] the value's endpoint must match ANY of these values - endpoint: Optional[Set[int]] = None - # [optional] the value's property must match ANY of these values - property: Optional[Set[Union[str, int]]] = None - # [optional] the value's metadata_type must match ANY of these values - type: Optional[Set[str]] = None + # [optional] additional values that ALL need to be present on the node for this scheme to pass + required_values: Optional[Set[ZWaveValueDiscoverySchema]] = None + # [optional] bool to specify if this primary value may be discovered by multiple platforms + allow_multi: bool = False # For device class mapping see: # https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json DISCOVERY_SCHEMAS = [ + # ====== START OF DEVICE SPECIFIC MAPPING SCHEMAS ======= + # Honeywell 39358 In-Wall Fan Control using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x0039}, + product_id={0x3131}, + product_type={0x4944}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # GE/Jasco fan controllers using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x0063}, + product_id={0x3034, 0x3131, 0x3138}, + product_type={0x4944}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Leviton ZW4SF fan controllers using switch multilevel CC + ZWaveDiscoverySchema( + platform="fan", + manufacturer_id={0x001D}, + product_id={0x0002}, + product_type={0x0038}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Fibaro Shutter Fibaro FGS222 + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x010F}, + product_id={0x1000}, + product_type={0x0302}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Qubino flush shutter + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x0159}, + product_id={0x0052}, + product_type={0x0003}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # Graber/Bali/Spring Fashion Covers + ZWaveDiscoverySchema( + platform="cover", + hint="fibaro_fgs222", + manufacturer_id={0x026E}, + product_id={0x5A31}, + product_type={0x4353}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + ), + # ====== START OF GENERIC MAPPING SCHEMAS ======= # locks ZWaveDiscoverySchema( platform="lock", @@ -69,12 +173,14 @@ DISCOVERY_SCHEMAS = [ "Secure Keypad Door Lock", "Secure Lockbox", }, - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={"currentMode", "locked"}, - type={"number", "boolean"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"currentMode", "locked"}, + type={"number", "boolean"}, + ), ), # door lock door status ZWaveDiscoverySchema( @@ -87,12 +193,14 @@ DISCOVERY_SCHEMAS = [ "Secure Keypad Door Lock", "Secure Lockbox", }, - command_class={ - CommandClass.LOCK, - CommandClass.DOOR_LOCK, - }, - property={"doorStatus"}, - type={"any"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"doorStatus"}, + type={"any"}, + ), ), # climate ZWaveDiscoverySchema( @@ -102,10 +210,14 @@ DISCOVERY_SCHEMAS = [ "Setback Thermostat", "Thermostat General", "Thermostat General V2", + "General Thermostat", + "General Thermostat V2", }, - command_class={CommandClass.THERMOSTAT_MODE}, - property={"mode"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), ), # climate # setpoint thermostats @@ -115,9 +227,11 @@ DISCOVERY_SCHEMAS = [ device_class_specific={ "Setpoint Thermostat", }, - command_class={CommandClass.THERMOSTAT_SETPOINT}, - property={"setpoint"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_SETPOINT}, + property={"setpoint"}, + type={"number"}, + ), ), # lights # primary value is the currentValue (brightness) @@ -132,85 +246,104 @@ DISCOVERY_SCHEMAS = [ "Multilevel Scene Switch", "Unused", }, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), # binary sensors ZWaveDiscoverySchema( platform="binary_sensor", hint="boolean", - command_class={ - CommandClass.SENSOR_BINARY, - CommandClass.BATTERY, - CommandClass.SENSOR_ALARM, - }, - type={"boolean"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_BINARY, + CommandClass.BATTERY, + CommandClass.SENSOR_ALARM, + }, + type={"boolean"}, + ), ), ZWaveDiscoverySchema( platform="binary_sensor", hint="notification", - command_class={ - CommandClass.NOTIFICATION, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + allow_multi=True, ), # generic text sensors ZWaveDiscoverySchema( platform="sensor", hint="string_sensor", - command_class={ - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - }, - type={"string"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + }, + type={"string"}, + ), ), # generic numeric sensors ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.SENSOR_MULTILEVEL, - CommandClass.SENSOR_ALARM, - CommandClass.INDICATOR, - CommandClass.BATTERY, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.SENSOR_MULTILEVEL, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + }, + type={"number"}, + ), ), # numeric sensors for Meter CC ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.METER, - }, - type={"number"}, - property={"value"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.METER, + }, + type={"number"}, + property={"value"}, + ), ), # special list sensors (Notification CC) ZWaveDiscoverySchema( platform="sensor", hint="list_sensor", - command_class={ - CommandClass.NOTIFICATION, - }, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + allow_multi=True, ), # sensor for basic CC ZWaveDiscoverySchema( platform="sensor", hint="numeric_sensor", - command_class={ - CommandClass.BASIC, - }, - type={"number"}, - property={"currentValue"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={ + CommandClass.BASIC, + }, + type={"number"}, + property={"currentValue"}, + ), ), # binary switches ZWaveDiscoverySchema( platform="switch", - command_class={CommandClass.SWITCH_BINARY}, - property={"currentValue"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_BINARY}, property={"currentValue"} + ), ), # cover ZWaveDiscoverySchema( @@ -223,9 +356,11 @@ DISCOVERY_SCHEMAS = [ "Motor Control Class C", "Multiposition Motor", }, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), # fan ZWaveDiscoverySchema( @@ -233,9 +368,11 @@ DISCOVERY_SCHEMAS = [ hint="fan", device_class_generic={"Multilevel Switch"}, device_class_specific={"Fan Switch"}, - command_class={CommandClass.SWITCH_MULTILEVEL}, - property={"currentValue"}, - type={"number"}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), ), ] @@ -243,8 +380,33 @@ DISCOVERY_SCHEMAS = [ @callback def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: """Run discovery on ZWave node and return matching (primary) values.""" + # pylint: disable=too-many-nested-blocks for value in node.values.values(): for schema in DISCOVERY_SCHEMAS: + # check manufacturer_id + if ( + schema.manufacturer_id is not None + and value.node.manufacturer_id not in schema.manufacturer_id + ): + continue + # check product_id + if ( + schema.product_id is not None + and value.node.product_id not in schema.product_id + ): + continue + # check product_type + if ( + schema.product_type is not None + and value.node.product_type not in schema.product_type + ): + continue + # check firmware_version + if ( + schema.firmware_version is not None + and value.node.firmware_version not in schema.firmware_version + ): + continue # check device_class_basic if ( schema.device_class_basic is not None @@ -263,21 +425,19 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None and value.node.device_class.specific not in schema.device_class_specific ): continue - # check command_class - if ( - schema.command_class is not None - and value.command_class not in schema.command_class - ): - continue - # check endpoint - if schema.endpoint is not None and value.endpoint not in schema.endpoint: - continue - # check property - if schema.property is not None and value.property_ not in schema.property: - continue - # check metadata_type - if schema.type is not None and value.metadata.type not in schema.type: + # check primary value + if not check_value(value, schema.primary_value): continue + # check additional required values + if schema.required_values is not None: + required_values_present = True + for val_scheme in schema.required_values: + for val in node.values.values(): + if not check_value(val, val_scheme): + required_values_present = False + break + if not required_values_present: + continue # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( node=value.node, @@ -285,3 +445,27 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None platform=schema.platform, platform_hint=schema.hint, ) + if not schema.allow_multi: + # break out of loop, this value may not be discovered by other schemas/platforms + break + + +@callback +def check_value(value: ZwaveValue, schema: ZWaveValueDiscoverySchema) -> bool: + """Check if value matches scheme.""" + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + return False + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + return False + # check property + if schema.property is not None and value.property_ not in schema.property: + return False + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + return False + return True