Add network resource button entities to ISY994 and bump PyISY to 3.0.12 (#85429)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
shbatm 2023-01-08 20:45:54 -06:00 committed by GitHub
parent cdafd94550
commit a8cdb86b23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 146 additions and 36 deletions

View file

@ -7,6 +7,7 @@ from urllib.parse import urlparse
from aiohttp import CookieJar
import async_timeout
from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError
from pyisy.constants import PROTO_NETWORK_RESOURCE
import voluptuous as vol
from homeassistant import config_entries
@ -38,9 +39,19 @@ from .const import (
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
ISY_CONF_FIRMWARE,
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
ISY_CONN_ADDRESS,
ISY_CONN_PORT,
ISY_CONN_TLS,
MANUFACTURER,
PLATFORMS,
PROGRAM_PLATFORMS,
SCHEME_HTTP,
SCHEME_HTTPS,
SENSOR_AUX,
)
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
@ -122,7 +133,7 @@ async def async_setup_entry(
hass.data[DOMAIN][entry.entry_id] = {}
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
hass_isy_data[ISY994_NODES] = {SENSOR_AUX: []}
hass_isy_data[ISY994_NODES] = {SENSOR_AUX: [], PROTO_NETWORK_RESOURCE: []}
for platform in PLATFORMS:
hass_isy_data[ISY994_NODES][platform] = []
@ -148,13 +159,13 @@ async def async_setup_entry(
CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING
)
if host.scheme == "http":
if host.scheme == SCHEME_HTTP:
https = False
port = host.port or 80
session = aiohttp_client.async_create_clientsession(
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
)
elif host.scheme == "https":
elif host.scheme == SCHEME_HTTPS:
https = True
port = host.port or 443
session = aiohttp_client.async_get_clientsession(hass)
@ -202,6 +213,9 @@ async def async_setup_entry(
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
_categorize_programs(hass_isy_data, isy.programs)
_categorize_variables(hass_isy_data, isy.variables, variable_identifier)
if isy.configuration[ISY_CONF_NETWORKING]:
for resource in isy.networking.nobjs:
hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource)
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
_LOGGER.info(repr(isy.clock))
@ -262,8 +276,8 @@ def _async_import_options_from_data_if_missing(
def _async_isy_to_configuration_url(isy: ISY) -> str:
"""Extract the configuration url from the isy."""
connection_info = isy.conn.connection_info
proto = "https" if "tls" in connection_info else "http"
return f"{proto}://{connection_info['addr']}:{connection_info['port']}"
proto = SCHEME_HTTPS if ISY_CONN_TLS in connection_info else SCHEME_HTTP
return f"{proto}://{connection_info[ISY_CONN_ADDRESS]}:{connection_info[ISY_CONN_PORT]}"
@callback
@ -274,12 +288,12 @@ def _async_get_or_create_isy_device_in_registry(
url = _async_isy_to_configuration_url(isy)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])},
identifiers={(DOMAIN, isy.configuration["uuid"])},
connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration[ISY_CONF_UUID])},
identifiers={(DOMAIN, isy.configuration[ISY_CONF_UUID])},
manufacturer=MANUFACTURER,
name=isy.configuration["name"],
model=isy.configuration["model"],
sw_version=isy.configuration["firmware"],
name=isy.configuration[ISY_CONF_NAME],
model=isy.configuration[ISY_CONF_MODEL],
sw_version=isy.configuration[ISY_CONF_FIRMWARE],
configuration_url=url,
)

View file

@ -2,17 +2,29 @@
from __future__ import annotations
from pyisy import ISY
from pyisy.constants import PROTO_INSTEON
from pyisy.constants import PROTO_INSTEON, PROTO_NETWORK_RESOURCE
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.device_registry import DeviceEntryType
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
from . import _async_isy_to_configuration_url
from .const import (
DOMAIN as ISY994_DOMAIN,
ISY994_ISY,
ISY994_NODES,
ISY_CONF_FIRMWARE,
ISY_CONF_MODEL,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
MANUFACTURER,
)
async def async_setup_entry(
@ -23,13 +35,23 @@ async def async_setup_entry(
"""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 | ISYNodeBeepButtonEntity] = []
for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]:
uuid = isy.configuration[ISY_CONF_UUID]
entities: list[
ISYNodeQueryButtonEntity
| ISYNodeBeepButtonEntity
| ISYNetworkResourceButtonEntity
] = []
nodes: dict = hass_isy_data[ISY994_NODES]
for node in nodes[Platform.BUTTON]:
entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}"))
if node.protocol == PROTO_INSTEON:
entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}"))
for node in nodes[PROTO_NETWORK_RESOURCE]:
entities.append(
ISYNetworkResourceButtonEntity(node, f"{uuid}_{PROTO_NETWORK_RESOURCE}")
)
# Add entity to query full system
entities.append(ISYNodeQueryButtonEntity(isy, uuid))
@ -80,3 +102,39 @@ class ISYNodeBeepButtonEntity(ButtonEntity):
async def async_press(self) -> None:
"""Press the button."""
await self._node.beep()
class ISYNetworkResourceButtonEntity(ButtonEntity):
"""Representation of an ISY/IoX Network Resource button entity."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(self, node: Node, base_unique_id: str) -> None:
"""Initialize an ISY network resource button entity."""
self._node = node
# Entity class attributes
self._attr_name = node.name
self._attr_unique_id = f"{base_unique_id}_{node.address}"
url = _async_isy_to_configuration_url(node.isy)
config = node.isy.configuration
self._attr_device_info = DeviceInfo(
identifiers={
(
ISY994_DOMAIN,
f"{config[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}",
)
},
manufacturer=MANUFACTURER,
name=f"{config[ISY_CONF_NAME]} {ISY_CONF_NETWORKING}",
model=config[ISY_CONF_MODEL],
sw_version=config[ISY_CONF_FIRMWARE],
configuration_url=url,
via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]),
entry_type=DeviceEntryType.SERVICE,
)
async def async_press(self) -> None:
"""Press the button."""
await self._node.run()

View file

@ -34,6 +34,8 @@ from .const import (
DOMAIN,
HTTP_PORT,
HTTPS_PORT,
ISY_CONF_NAME,
ISY_CONF_UUID,
ISY_URL_POSTFIX,
SCHEME_HTTP,
SCHEME_HTTPS,
@ -106,11 +108,14 @@ async def validate_input(
isy_conf = Configuration(xml=isy_conf_xml)
except ISYResponseParseError as error:
raise CannotConnect from error
if not isy_conf or "name" not in isy_conf or not isy_conf["name"]:
if not isy_conf or ISY_CONF_NAME not in isy_conf or not isy_conf[ISY_CONF_NAME]:
raise CannotConnect
# Return info that you want to store in the config entry.
return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]}
return {
"title": f"{isy_conf[ISY_CONF_NAME]} ({host.hostname})",
ISY_CONF_UUID: isy_conf[ISY_CONF_UUID],
}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -151,7 +156,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
if not errors:
await self.async_set_unique_id(info["uuid"], raise_on_progress=False)
await self.async_set_unique_id(
info[ISY_CONF_UUID], raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)

View file

@ -104,6 +104,16 @@ ISY994_NODES = "isy994_nodes"
ISY994_PROGRAMS = "isy994_programs"
ISY994_VARIABLES = "isy994_variables"
ISY_CONF_NETWORKING = "Networking Module"
ISY_CONF_UUID = "uuid"
ISY_CONF_NAME = "name"
ISY_CONF_MODEL = "model"
ISY_CONF_FIRMWARE = "firmware"
ISY_CONN_PORT = "port"
ISY_CONN_ADDRESS = "addr"
ISY_CONN_TLS = "tls"
FILTER_UOM = "uom"
FILTER_STATES = "states"
FILTER_NODE_DEF_ID = "node_def_id"

View file

@ -21,7 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo, Entity
from . import _async_isy_to_configuration_url
from .const import DOMAIN
from .const import DOMAIN, ISY_CONF_UUID
class ISYEntity(Entity):
@ -73,7 +73,7 @@ class ISYEntity(Entity):
def device_info(self) -> DeviceInfo | None:
"""Return the device_info of the device."""
isy = self._node.isy
uuid = isy.configuration["uuid"]
uuid = isy.configuration[ISY_CONF_UUID]
node = self._node
url = _async_isy_to_configuration_url(isy)
@ -127,7 +127,7 @@ class ISYEntity(Entity):
def unique_id(self) -> str | None:
"""Get the unique identifier of the device."""
if hasattr(self._node, "address"):
return f"{self._node.isy.configuration['uuid']}_{self._node.address}"
return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}"
return None
@property

View file

@ -3,7 +3,7 @@
"name": "Universal Devices ISY/IoX",
"integration_type": "hub",
"documentation": "https://www.home-assistant.io/integrations/isy994",
"requirements": ["pyisy==3.0.11"],
"requirements": ["pyisy==3.0.12"],
"codeowners": ["@bdraco", "@shbatm"],
"config_flow": true,
"ssdp": [

View file

@ -36,6 +36,7 @@ from .const import (
DOMAIN as ISY994_DOMAIN,
ISY994_NODES,
ISY994_VARIABLES,
ISY_CONF_UUID,
SENSOR_AUX,
UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME,
@ -255,7 +256,7 @@ class ISYAuxSensorEntity(ISYSensorEntity):
"""Get the unique identifier of the device and aux sensor."""
if not hasattr(self._node, "address"):
return None
return f"{self._node.isy.configuration['uuid']}_{self._node.address}_{self._control}"
return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}_{self._control}"
@property
def name(self) -> str:

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any
from pyisy.constants import COMMAND_FRIENDLY_NAME
from pyisy.constants import COMMAND_FRIENDLY_NAME, PROTO_NETWORK_RESOURCE
import voluptuous as vol
from homeassistant.const import (
@ -23,7 +23,14 @@ 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
from .const import (
_LOGGER,
DOMAIN,
ISY994_ISY,
ISY_CONF_NAME,
ISY_CONF_NETWORKING,
ISY_CONF_UUID,
)
from .util import unique_ids_for_config_entry_id
# Common Services for All Platforms:
@ -194,7 +201,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
_LOGGER.debug(
"Requesting query of device %s on ISY %s",
address,
isy.configuration["uuid"],
isy.configuration[ISY_CONF_UUID],
)
await isy.query(address)
async_log_deprecated_service_call(
@ -204,13 +211,13 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{isy.configuration['uuid']}_{address}_query",
f"{isy.configuration[ISY_CONF_UUID]}_{address}_query",
),
breaks_in_ha_version="2023.5.0",
)
return
_LOGGER.debug(
"Requesting system query of ISY %s", isy.configuration["uuid"]
"Requesting system query of ISY %s", isy.configuration[ISY_CONF_UUID]
)
await isy.query()
async_log_deprecated_service_call(
@ -218,7 +225,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
call=service,
alternate_service="button.press",
alternate_target=entity_registry.async_get_entity_id(
Platform.BUTTON, DOMAIN, f"{isy.configuration['uuid']}_query"
Platform.BUTTON, DOMAIN, f"{isy.configuration[ISY_CONF_UUID]}_query"
),
breaks_in_ha_version="2023.5.0",
)
@ -231,9 +238,9 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
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"]:
if isy_name and isy_name != isy.configuration[ISY_CONF_NAME]:
continue
if not hasattr(isy, "networking") or isy.networking is None:
if isy.networking is None or not isy.configuration[ISY_CONF_NETWORKING]:
continue
command = None
if address:
@ -242,6 +249,18 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
command = isy.networking.get_by_name(name)
if command is not None:
await command.run()
entity_registry = er.async_get(hass)
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[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}_{address}",
),
breaks_in_ha_version="2023.5.0",
)
return
_LOGGER.error(
"Could not run network resource command; not found or enabled on the ISY"

View file

@ -267,8 +267,8 @@ send_program_command:
selector:
text:
run_network_resource:
name: Run network resource
description: Run a network resource on the ISY.
name: Run network resource (Deprecated)
description: "Run a network resource on the ISY. Deprecated: Use Network Resource button entity."
fields:
address:
name: Address

View file

@ -9,6 +9,7 @@ from .const import (
ISY994_NODES,
ISY994_PROGRAMS,
ISY994_VARIABLES,
ISY_CONF_UUID,
PLATFORMS,
PROGRAM_PLATFORMS,
)
@ -19,7 +20,7 @@ def unique_ids_for_config_entry_id(
) -> set[str]:
"""Find all the unique ids for a config entry id."""
hass_isy_data = hass.data[DOMAIN][config_entry_id]
uuid = hass_isy_data[ISY994_ISY].configuration["uuid"]
uuid = hass_isy_data[ISY994_ISY].configuration[ISY_CONF_UUID]
current_unique_ids: set[str] = {uuid}
for platform in PLATFORMS:

View file

@ -1693,7 +1693,7 @@ pyirishrail==0.0.2
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.11
pyisy==3.0.12
# homeassistant.components.itach
pyitachip2ir==0.0.7

View file

@ -1206,7 +1206,7 @@ pyiqvia==2022.04.0
pyiss==1.0.1
# homeassistant.components.isy994
pyisy==3.0.11
pyisy==3.0.12
# homeassistant.components.kaleidescape
pykaleidescape==1.0.1