Add query button entities to ISY994 devices and hub (#85337)

This commit is contained in:
shbatm 2023-01-06 19:15:02 -06:00 committed by GitHub
parent 2976f843b5
commit cf3ca816a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 176 additions and 25 deletions

View File

@ -0,0 +1,56 @@
"""Representation of ISY/IoX buttons."""
from __future__ import annotations
from pyisy import ISY
from pyisy.nodes import Node
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN as ISY994_DOMAIN, ISY994_ISY, ISY994_NODES
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX button from config entry."""
hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id]
isy: ISY = hass_isy_data[ISY994_ISY]
uuid = isy.configuration["uuid"]
entities: list[ISYNodeQueryButtonEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]:
entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}"))
# Add entity to query full system
entities.append(ISYNodeQueryButtonEntity(isy, uuid))
async_add_entities(entities)
class ISYNodeQueryButtonEntity(ButtonEntity):
"""Representation of a device query button entity."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG
_attr_has_entity_name = True
def __init__(self, node: Node | ISY, base_unique_id: str) -> None:
"""Initialize a query ISY device button entity."""
self._node = node
# Entity class attributes
self._attr_name = "Query"
self._attr_unique_id = f"{base_unique_id}_query"
self._attr_device_info = DeviceInfo(
identifiers={(ISY994_DOMAIN, base_unique_id)}
)
async def async_press(self) -> None:
"""Press the button."""
self.hass.async_create_task(self._node.query())

View File

@ -76,6 +76,7 @@ KEY_STATUS = "status"
PLATFORMS = [ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE, Platform.CLIMATE,
Platform.COVER, Platform.COVER,
Platform.FAN, Platform.FAN,
@ -189,6 +190,14 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
], # Does a startswith() match; include the dot ], # Does a startswith() match; include the dot
FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))), FILTER_ZWAVE_CAT: (["104", "112", "138"] + list(map(str, range(148, 180)))),
}, },
Platform.BUTTON: {
# No devices automatically sorted as buttons at this time. Query buttons added elsewhere.
FILTER_UOM: [],
FILTER_STATES: [],
FILTER_NODE_DEF_ID: [],
FILTER_INSTEON_TYPE: [],
FILTER_ZWAVE_CAT: [],
},
Platform.SENSOR: { Platform.SENSOR: {
# This is just a more-readable way of including MOST uoms between 1-100 # This is just a more-readable way of including MOST uoms between 1-100
# (Remember that range() is non-inclusive of the stop value) # (Remember that range() is non-inclusive of the stop value)

View File

@ -16,12 +16,6 @@ from pyisy.nodes import Group, Node, Nodes
from pyisy.programs import Programs from pyisy.programs import Programs
from pyisy.variables import Variables from pyisy.variables import Variables
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.climate import DOMAIN as CLIMATE
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -95,7 +89,7 @@ def _check_for_insteon_type(
works for Insteon device. "Node Server" (v5+) and Z-Wave and others will works for Insteon device. "Node Server" (v5+) and Z-Wave and others will
not have a type. not have a type.
""" """
if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON: if node.protocol != PROTO_INSTEON:
return False return False
if not hasattr(node, "type") or node.type is None: if not hasattr(node, "type") or node.type is None:
# Node doesn't have a type (non-Insteon device most likely) # Node doesn't have a type (non-Insteon device most likely)
@ -115,34 +109,34 @@ def _check_for_insteon_type(
subnode_id = int(node.address.split(" ")[-1], 16) subnode_id = int(node.address.split(" ")[-1], 16)
# FanLinc, which has a light module as one of its nodes. # FanLinc, which has a light module as one of its nodes.
if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT: if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
hass_isy_data[ISY994_NODES][LIGHT].append(node) hass_isy_data[ISY994_NODES][Platform.LIGHT].append(node)
return True return True
# Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3
if platform == CLIMATE and subnode_id in ( if platform == Platform.CLIMATE and subnode_id in (
SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT, SUBNODE_CLIMATE_HEAT,
): ):
hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node)
return True return True
# IOLincs which have a sensor and relay on 2 different nodes # IOLincs which have a sensor and relay on 2 different nodes
if ( if (
platform == BINARY_SENSOR platform == Platform.BINARY_SENSOR
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
and subnode_id == SUBNODE_IOLINC_RELAY and subnode_id == SUBNODE_IOLINC_RELAY
): ):
hass_isy_data[ISY994_NODES][SWITCH].append(node) hass_isy_data[ISY994_NODES][Platform.SWITCH].append(node)
return True return True
# Smartenit EZIO2X4 # Smartenit EZIO2X4
if ( if (
platform == SWITCH platform == Platform.SWITCH
and device_type.startswith(TYPE_EZIO2X4) and device_type.startswith(TYPE_EZIO2X4)
and subnode_id in SUBNODE_EZIO2X4_SENSORS and subnode_id in SUBNODE_EZIO2X4_SENSORS
): ):
hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node)
return True return True
hass_isy_data[ISY994_NODES][platform].append(node) hass_isy_data[ISY994_NODES][platform].append(node)
@ -159,7 +153,7 @@ def _check_for_zwave_cat(
This is for (presumably) every version of the ISY firmware, but only This is for (presumably) every version of the ISY firmware, but only
works for Z-Wave Devices with the devtype.cat property. works for Z-Wave Devices with the devtype.cat property.
""" """
if not hasattr(node, "protocol") or node.protocol != PROTO_ZWAVE: if node.protocol != PROTO_ZWAVE:
return False return False
if not hasattr(node, "zwave_props") or node.zwave_props is None: if not hasattr(node, "zwave_props") or node.zwave_props is None:
@ -292,11 +286,15 @@ def _categorize_nodes(
# Don't import this node as a device at all # Don't import this node as a device at all
continue continue
if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: if hasattr(node, "parent_node") and node.parent_node is None:
# This is a physical device / parent node, add a query button
hass_isy_data[ISY994_NODES][Platform.BUTTON].append(node)
if node.protocol == PROTO_GROUP:
hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node)
continue continue
if getattr(node, "protocol", None) == PROTO_INSTEON: if node.protocol == PROTO_INSTEON:
for control in node.aux_properties: for control in node.aux_properties:
hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control))
@ -305,7 +303,7 @@ def _categorize_nodes(
# determine if it should be a binary_sensor. # determine if it should be a binary_sensor.
if _is_sensor_a_binary_sensor(hass_isy_data, node): if _is_sensor_a_binary_sensor(hass_isy_data, node):
continue continue
hass_isy_data[ISY994_NODES][SENSOR].append(node) hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node)
continue continue
# We have a bunch of different methods for determining the device type, # We have a bunch of different methods for determining the device type,
@ -323,7 +321,7 @@ def _categorize_nodes(
continue continue
# Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes.
hass_isy_data[ISY994_NODES][SENSOR].append(node) hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node)
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
@ -348,7 +346,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
) )
continue continue
if platform != BINARY_SENSOR: if platform != Platform.BINARY_SENSOR:
actions = entity_folder.get_by_name(KEY_ACTIONS) actions = entity_folder.get_by_name(KEY_ACTIONS)
if not actions or actions.protocol != PROTO_PROGRAM: if not actions or actions.protocol != PROTO_PROGRAM:
_LOGGER.warning( _LOGGER.warning(

View File

@ -13,12 +13,14 @@ from homeassistant.const import (
CONF_TYPE, CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD, SERVICE_RELOAD,
Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import entity_platform from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.entity_platform import async_get_platforms
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.service import entity_service_call from homeassistant.helpers.service import entity_service_call
from .const import _LOGGER, DOMAIN, ISY994_ISY from .const import _LOGGER, DOMAIN, ISY994_ISY
@ -181,7 +183,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
"""Handle a system query service call.""" """Handle a system query service call."""
address = service.data.get(CONF_ADDRESS) address = service.data.get(CONF_ADDRESS)
isy_name = service.data.get(CONF_ISY) isy_name = service.data.get(CONF_ISY)
entity_registry = er.async_get(hass)
for config_entry_id in hass.data[DOMAIN]: for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
if isy_name and isy_name != isy.configuration["name"]: if isy_name and isy_name != isy.configuration["name"]:
@ -195,11 +197,31 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
isy.configuration["uuid"], isy.configuration["uuid"],
) )
await isy.query(address) await isy.query(address)
async_log_deprecated_service_call(
hass,
call=service,
alternate_service="button.press",
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{isy.configuration['uuid']}_{address}_query",
),
breaks_in_ha_version="2023.5.0",
)
return return
_LOGGER.debug( _LOGGER.debug(
"Requesting system query of ISY %s", isy.configuration["uuid"] "Requesting system query of ISY %s", isy.configuration["uuid"]
) )
await isy.query() await isy.query()
async_log_deprecated_service_call(
hass,
call=service,
alternate_service="button.press",
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON, DOMAIN, f"{isy.configuration['uuid']}_query"
),
breaks_in_ha_version="2023.5.0",
)
async def async_run_network_resource_service_handler(service: ServiceCall) -> None: async def async_run_network_resource_service_handler(service: ServiceCall) -> None:
"""Handle a network resource service call.""" """Handle a network resource service call."""
@ -447,3 +469,43 @@ def async_setup_light_services(hass: HomeAssistant) -> None:
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate" SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate"
) )
@callback
def async_log_deprecated_service_call(
hass: HomeAssistant,
call: ServiceCall,
alternate_service: str,
alternate_target: str | None,
breaks_in_ha_version: str,
) -> None:
"""Log a warning about a deprecated service call."""
deprecated_service = f"{call.domain}.{call.service}"
alternate_target = alternate_target or "this device"
async_create_issue(
hass,
DOMAIN,
f"deprecated_service_{deprecated_service}",
breaks_in_ha_version=breaks_in_ha_version,
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_service",
translation_placeholders={
"alternate_service": alternate_service,
"alternate_target": alternate_target,
"deprecated_service": deprecated_service,
},
)
_LOGGER.warning(
(
'The "%s" service is deprecated and will be removed in %s; use the "%s" '
'service and pass it a target entity ID of "%s"'
),
deprecated_service,
breaks_in_ha_version,
alternate_service,
alternate_target,
)

View File

@ -168,8 +168,8 @@ set_ramp_rate:
min: 0 min: 0
max: 31 max: 31
system_query: system_query:
name: System query name: System query (Deprecated)
description: Request the ISY Query the connected devices. description: "Request the ISY Query the connected devices. Deprecated: Use device Query button entity."
fields: fields:
address: address:
name: Address name: Address

View File

@ -53,5 +53,18 @@
"last_heartbeat": "Last Heartbeat Time", "last_heartbeat": "Last Heartbeat Time",
"websocket_status": "Event Socket Status" "websocket_status": "Event Socket Status"
} }
},
"issues": {
"deprecated_service": {
"title": "The {deprecated_service} service will be removed",
"fix_flow": {
"step": {
"confirm": {
"title": "The {deprecated_service} service will be removed",
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`."
}
}
}
}
} }
} }

View File

@ -53,5 +53,18 @@
"last_heartbeat": "Last Heartbeat Time", "last_heartbeat": "Last Heartbeat Time",
"websocket_status": "Event Socket Status" "websocket_status": "Event Socket Status"
} }
},
"issues": {
"deprecated_service": {
"fix_flow": {
"step": {
"confirm": {
"description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.",
"title": "The {deprecated_service} service will be removed"
}
}
},
"title": "The {deprecated_service} service will be removed"
}
} }
} }