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 = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.FAN,
@ -189,6 +190,14 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
], # Does a startswith() match; include the dot
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: {
# This is just a more-readable way of including MOST uoms between 1-100
# (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.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.core import HomeAssistant
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
not have a type.
"""
if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON:
if node.protocol != PROTO_INSTEON:
return False
if not hasattr(node, "type") or node.type is None:
# 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)
# FanLinc, which has a light module as one of its nodes.
if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
hass_isy_data[ISY994_NODES][LIGHT].append(node)
if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
hass_isy_data[ISY994_NODES][Platform.LIGHT].append(node)
return True
# 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_HEAT,
):
hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node)
hass_isy_data[ISY994_NODES][Platform.BINARY_SENSOR].append(node)
return True
# IOLincs which have a sensor and relay on 2 different nodes
if (
platform == BINARY_SENSOR
platform == Platform.BINARY_SENSOR
and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
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
# Smartenit EZIO2X4
if (
platform == SWITCH
platform == Platform.SWITCH
and device_type.startswith(TYPE_EZIO2X4)
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
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
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
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
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)
continue
if getattr(node, "protocol", None) == PROTO_INSTEON:
if node.protocol == PROTO_INSTEON:
for control in node.aux_properties:
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.
if _is_sensor_a_binary_sensor(hass_isy_data, node):
continue
hass_isy_data[ISY994_NODES][SENSOR].append(node)
hass_isy_data[ISY994_NODES][Platform.SENSOR].append(node)
continue
# We have a bunch of different methods for determining the device type,
@ -323,7 +321,7 @@ def _categorize_nodes(
continue
# 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:
@ -348,7 +346,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
)
continue
if platform != BINARY_SENSOR:
if platform != Platform.BINARY_SENSOR:
actions = entity_folder.get_by_name(KEY_ACTIONS)
if not actions or actions.protocol != PROTO_PROGRAM:
_LOGGER.warning(

View File

@ -13,12 +13,14 @@ from homeassistant.const import (
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
SERVICE_RELOAD,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import entity_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import async_get_platforms
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 .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."""
address = service.data.get(CONF_ADDRESS)
isy_name = service.data.get(CONF_ISY)
entity_registry = er.async_get(hass)
for config_entry_id in hass.data[DOMAIN]:
isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY]
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"],
)
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
_LOGGER.debug(
"Requesting system query of ISY %s", isy.configuration["uuid"]
)
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:
"""Handle a network resource service call."""
@ -447,3 +469,43 @@ def async_setup_light_services(hass: HomeAssistant) -> None:
platform.async_register_entity_service(
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
max: 31
system_query:
name: System query
description: Request the ISY Query the connected devices.
name: System query (Deprecated)
description: "Request the ISY Query the connected devices. Deprecated: Use device Query button entity."
fields:
address:
name: Address

View File

@ -53,5 +53,18 @@
"last_heartbeat": "Last Heartbeat Time",
"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",
"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"
}
}
}