Move ISY994 data to dataclass and remove bad entities (#85744)
This commit is contained in:
parent
f941864308
commit
28bea53afe
20 changed files with 321 additions and 331 deletions
|
@ -610,6 +610,7 @@ omit =
|
||||||
homeassistant/components/isy994/helpers.py
|
homeassistant/components/isy994/helpers.py
|
||||||
homeassistant/components/isy994/light.py
|
homeassistant/components/isy994/light.py
|
||||||
homeassistant/components/isy994/lock.py
|
homeassistant/components/isy994/lock.py
|
||||||
|
homeassistant/components/isy994/models.py
|
||||||
homeassistant/components/isy994/number.py
|
homeassistant/components/isy994/number.py
|
||||||
homeassistant/components/isy994/sensor.py
|
homeassistant/components/isy994/sensor.py
|
||||||
homeassistant/components/isy994/services.py
|
homeassistant/components/isy994/services.py
|
||||||
|
|
|
@ -43,25 +43,15 @@ from .const import (
|
||||||
ISY_CONF_MODEL,
|
ISY_CONF_MODEL,
|
||||||
ISY_CONF_NAME,
|
ISY_CONF_NAME,
|
||||||
ISY_CONF_NETWORKING,
|
ISY_CONF_NETWORKING,
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NET_RES,
|
|
||||||
ISY_NODES,
|
|
||||||
ISY_PROGRAMS,
|
|
||||||
ISY_ROOT,
|
|
||||||
ISY_ROOT_NODES,
|
|
||||||
ISY_VARIABLES,
|
|
||||||
MANUFACTURER,
|
MANUFACTURER,
|
||||||
NODE_PLATFORMS,
|
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
PROGRAM_PLATFORMS,
|
|
||||||
ROOT_NODE_PLATFORMS,
|
|
||||||
SCHEME_HTTP,
|
SCHEME_HTTP,
|
||||||
SCHEME_HTTPS,
|
SCHEME_HTTPS,
|
||||||
SENSOR_AUX,
|
|
||||||
VARIABLE_PLATFORMS,
|
|
||||||
)
|
)
|
||||||
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
|
from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables
|
||||||
|
from .models import IsyData
|
||||||
from .services import async_setup_services, async_unload_services
|
from .services import async_setup_services, async_unload_services
|
||||||
|
from .util import _async_cleanup_registry_entries
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -135,15 +125,7 @@ async def async_setup_entry(
|
||||||
# they are missing from the options
|
# they are missing from the options
|
||||||
_async_import_options_from_data_if_missing(hass, entry)
|
_async_import_options_from_data_if_missing(hass, entry)
|
||||||
|
|
||||||
hass.data[DOMAIN][entry.entry_id] = {}
|
isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData()
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
|
|
||||||
hass_isy_data[ISY_NODES] = {p: [] for p in (NODE_PLATFORMS + [SENSOR_AUX])}
|
|
||||||
hass_isy_data[ISY_ROOT_NODES] = {p: [] for p in ROOT_NODE_PLATFORMS}
|
|
||||||
hass_isy_data[ISY_PROGRAMS] = {p: [] for p in PROGRAM_PLATFORMS}
|
|
||||||
hass_isy_data[ISY_VARIABLES] = {p: [] for p in VARIABLE_PLATFORMS}
|
|
||||||
hass_isy_data[ISY_NET_RES] = []
|
|
||||||
hass_isy_data[ISY_DEVICES] = {}
|
|
||||||
|
|
||||||
isy_config = entry.data
|
isy_config = entry.data
|
||||||
isy_options = entry.options
|
isy_options = entry.options
|
||||||
|
@ -212,34 +194,37 @@ async def async_setup_entry(
|
||||||
f"Invalid response ISY, device is likely still starting: {err}"
|
f"Invalid response ISY, device is likely still starting: {err}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
_categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier)
|
_categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier)
|
||||||
_categorize_programs(hass_isy_data, isy.programs)
|
_categorize_programs(isy_data, isy.programs)
|
||||||
# Categorize variables call to be removed with variable sensors in 2023.5.0
|
# Categorize variables call to be removed with variable sensors in 2023.5.0
|
||||||
_categorize_variables(hass_isy_data, isy.variables, variable_identifier)
|
_categorize_variables(isy_data, isy.variables, variable_identifier)
|
||||||
# Gather ISY Variables to be added. Identifier used to enable by default.
|
# Gather ISY Variables to be added. Identifier used to enable by default.
|
||||||
if len(isy.variables.children) > 0:
|
if isy.variables.children:
|
||||||
hass_isy_data[ISY_DEVICES][CONF_VARIABLES] = _create_service_device_info(
|
isy_data.devices[CONF_VARIABLES] = _create_service_device_info(
|
||||||
isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES
|
isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES
|
||||||
)
|
)
|
||||||
numbers = hass_isy_data[ISY_VARIABLES][Platform.NUMBER]
|
numbers = isy_data.variables[Platform.NUMBER]
|
||||||
for vtype, vname, vid in isy.variables.children:
|
for vtype, _, vid in isy.variables.children:
|
||||||
numbers.append((isy.variables[vtype][vid], variable_identifier in vname))
|
numbers.append(isy.variables[vtype][vid])
|
||||||
if isy.conf[ISY_CONF_NETWORKING]:
|
if isy.conf[ISY_CONF_NETWORKING]:
|
||||||
hass_isy_data[ISY_DEVICES][CONF_NETWORK] = _create_service_device_info(
|
isy_data.devices[CONF_NETWORK] = _create_service_device_info(
|
||||||
isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK
|
isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK
|
||||||
)
|
)
|
||||||
for resource in isy.networking.nobjs:
|
for resource in isy.networking.nobjs:
|
||||||
hass_isy_data[ISY_NET_RES].append(resource)
|
isy_data.net_resources.append(resource)
|
||||||
|
|
||||||
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
|
# Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs
|
||||||
_LOGGER.info(repr(isy.clock))
|
_LOGGER.info(repr(isy.clock))
|
||||||
|
|
||||||
hass_isy_data[ISY_ROOT] = isy
|
isy_data.root = isy
|
||||||
_async_get_or_create_isy_device_in_registry(hass, entry, isy)
|
_async_get_or_create_isy_device_in_registry(hass, entry, isy)
|
||||||
|
|
||||||
# Load platforms for the devices in the ISY controller that we support.
|
# Load platforms for the devices in the ISY controller that we support.
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
# Clean-up any old entities that we no longer provide.
|
||||||
|
_async_cleanup_registry_entries(hass, entry.entry_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_stop_auto_update(event: Event) -> None:
|
def _async_stop_auto_update(event: Event) -> None:
|
||||||
"""Stop the isy auto update on Home Assistant Shutdown."""
|
"""Stop the isy auto update on Home Assistant Shutdown."""
|
||||||
|
@ -328,9 +313,9 @@ async def async_unload_entry(
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
isy: ISY = hass_isy_data[ISY_ROOT]
|
isy: ISY = isy_data.root
|
||||||
|
|
||||||
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
_LOGGER.debug("ISY Stopping Event Stream and automatic updates")
|
||||||
isy.websocket.stop()
|
isy.websocket.stop()
|
||||||
|
@ -349,7 +334,7 @@ async def async_remove_config_entry_device(
|
||||||
device_entry: dr.DeviceEntry,
|
device_entry: dr.DeviceEntry,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Remove ISY config entry from a device."""
|
"""Remove ISY config entry from a device."""
|
||||||
hass_isy_devices = hass.data[DOMAIN][config_entry.entry_id][ISY_DEVICES]
|
isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
return not device_entry.identifiers.intersection(
|
return not device_entry.identifiers.intersection(
|
||||||
(DOMAIN, unique_id) for unique_id in hass_isy_devices
|
(DOMAIN, unique_id) for unique_id in isy_data.devices
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,9 +32,6 @@ from .const import (
|
||||||
BINARY_SENSOR_DEVICE_TYPES_ISY,
|
BINARY_SENSOR_DEVICE_TYPES_ISY,
|
||||||
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
|
BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NODES,
|
|
||||||
ISY_PROGRAMS,
|
|
||||||
SUBNODE_CLIMATE_COOL,
|
SUBNODE_CLIMATE_COOL,
|
||||||
SUBNODE_CLIMATE_HEAT,
|
SUBNODE_CLIMATE_HEAT,
|
||||||
SUBNODE_DUSK_DAWN,
|
SUBNODE_DUSK_DAWN,
|
||||||
|
@ -77,9 +74,9 @@ async def async_setup_entry(
|
||||||
] = []
|
] = []
|
||||||
entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity
|
entity: ISYInsteonBinarySensorEntity | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity
|
||||||
|
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR]:
|
for node in isy_data.nodes[Platform.BINARY_SENSOR]:
|
||||||
assert isinstance(node, Node)
|
assert isinstance(node, Node)
|
||||||
device_info = devices.get(node.primary_node)
|
device_info = devices.get(node.primary_node)
|
||||||
device_class, device_type = _detect_device_type_and_class(node)
|
device_class, device_type = _detect_device_type_and_class(node)
|
||||||
|
@ -205,7 +202,7 @@ async def async_setup_entry(
|
||||||
)
|
)
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
for name, status, _ in hass_isy_data[ISY_PROGRAMS][Platform.BINARY_SENSOR]:
|
for name, status, _ in isy_data.programs[Platform.BINARY_SENSOR]:
|
||||||
entities.append(ISYBinarySensorProgramEntity(name, status))
|
entities.append(ISYBinarySensorProgramEntity(name, status))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -13,14 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import CONF_NETWORK, DOMAIN
|
||||||
CONF_NETWORK,
|
|
||||||
DOMAIN,
|
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NET_RES,
|
|
||||||
ISY_ROOT,
|
|
||||||
ISY_ROOT_NODES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@ -29,21 +22,21 @@ async def async_setup_entry(
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up ISY/IoX button from config entry."""
|
"""Set up ISY/IoX button from config entry."""
|
||||||
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
isy: ISY = hass_isy_data[ISY_ROOT]
|
isy: ISY = isy_data.root
|
||||||
device_info = hass_isy_data[ISY_DEVICES]
|
device_info = isy_data.devices
|
||||||
entities: list[
|
entities: list[
|
||||||
ISYNodeQueryButtonEntity
|
ISYNodeQueryButtonEntity
|
||||||
| ISYNodeBeepButtonEntity
|
| ISYNodeBeepButtonEntity
|
||||||
| ISYNetworkResourceButtonEntity
|
| ISYNetworkResourceButtonEntity
|
||||||
] = []
|
] = []
|
||||||
|
|
||||||
for node in hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON]:
|
for node in isy_data.root_nodes[Platform.BUTTON]:
|
||||||
entities.append(
|
entities.append(
|
||||||
ISYNodeQueryButtonEntity(
|
ISYNodeQueryButtonEntity(
|
||||||
node=node,
|
node=node,
|
||||||
name="Query",
|
name="Query",
|
||||||
unique_id=f"{isy.uuid}_{node.address}_query",
|
unique_id=f"{isy_data.uid_base(node)}_query",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_info=device_info[node.address],
|
device_info=device_info[node.address],
|
||||||
)
|
)
|
||||||
|
@ -53,18 +46,18 @@ async def async_setup_entry(
|
||||||
ISYNodeBeepButtonEntity(
|
ISYNodeBeepButtonEntity(
|
||||||
node=node,
|
node=node,
|
||||||
name="Beep",
|
name="Beep",
|
||||||
unique_id=f"{isy.uuid}_{node.address}_beep",
|
unique_id=f"{isy_data.uid_base(node)}_beep",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
device_info=device_info[node.address],
|
device_info=device_info[node.address],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for node in hass_isy_data[ISY_NET_RES]:
|
for node in isy_data.net_resources:
|
||||||
entities.append(
|
entities.append(
|
||||||
ISYNetworkResourceButtonEntity(
|
ISYNetworkResourceButtonEntity(
|
||||||
node=node,
|
node=node,
|
||||||
name=node.name,
|
name=node.name,
|
||||||
unique_id=f"{isy.uuid}_{CONF_NETWORK}_{node.address}",
|
unique_id=isy_data.uid_base(node),
|
||||||
device_info=device_info[CONF_NETWORK],
|
device_info=device_info[CONF_NETWORK],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -43,9 +43,7 @@ from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HA_FAN_TO_ISY,
|
HA_FAN_TO_ISY,
|
||||||
HA_HVAC_TO_ISY,
|
HA_HVAC_TO_ISY,
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_HVAC_MODES,
|
ISY_HVAC_MODES,
|
||||||
ISY_NODES,
|
|
||||||
UOM_FAN_MODES,
|
UOM_FAN_MODES,
|
||||||
UOM_HVAC_ACTIONS,
|
UOM_HVAC_ACTIONS,
|
||||||
UOM_HVAC_MODE_GENERIC,
|
UOM_HVAC_MODE_GENERIC,
|
||||||
|
@ -65,9 +63,9 @@ async def async_setup_entry(
|
||||||
"""Set up the ISY thermostat platform."""
|
"""Set up the ISY thermostat platform."""
|
||||||
entities = []
|
entities = []
|
||||||
|
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.CLIMATE]:
|
for node in isy_data.nodes[Platform.CLIMATE]:
|
||||||
entities.append(ISYThermostatEntity(node, devices.get(node.primary_node)))
|
entities.append(ISYThermostatEntity(node, devices.get(node.primary_node)))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""Constants for the ISY Platform."""
|
"""Constants for the ISY Platform."""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
|
@ -85,6 +87,7 @@ NODE_PLATFORMS = [
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR]
|
||||||
PROGRAM_PLATFORMS = [
|
PROGRAM_PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.COVER,
|
Platform.COVER,
|
||||||
|
@ -98,6 +101,7 @@ VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR]
|
||||||
# Set of all platforms used by integration
|
# Set of all platforms used by integration
|
||||||
PLATFORMS = {
|
PLATFORMS = {
|
||||||
*NODE_PLATFORMS,
|
*NODE_PLATFORMS,
|
||||||
|
*NODE_AUX_PROP_PLATFORMS,
|
||||||
*PROGRAM_PLATFORMS,
|
*PROGRAM_PLATFORMS,
|
||||||
*ROOT_NODE_PLATFORMS,
|
*ROOT_NODE_PLATFORMS,
|
||||||
*VARIABLE_PLATFORMS,
|
*VARIABLE_PLATFORMS,
|
||||||
|
@ -109,14 +113,6 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
|
||||||
# (they can turn off, and report their state)
|
# (they can turn off, and report their state)
|
||||||
ISY_GROUP_PLATFORM = Platform.SWITCH
|
ISY_GROUP_PLATFORM = Platform.SWITCH
|
||||||
|
|
||||||
ISY_ROOT = "isy"
|
|
||||||
ISY_ROOT_NODES = "isy_root_nodes"
|
|
||||||
ISY_NET_RES = "isy_net_res"
|
|
||||||
ISY_NODES = "isy_nodes"
|
|
||||||
ISY_PROGRAMS = "isy_programs"
|
|
||||||
ISY_VARIABLES = "isy_variables"
|
|
||||||
ISY_DEVICES = "isy_devices"
|
|
||||||
|
|
||||||
ISY_CONF_NETWORKING = "Networking Module"
|
ISY_CONF_NETWORKING = "Networking Module"
|
||||||
ISY_CONF_UUID = "uuid"
|
ISY_CONF_UUID = "uuid"
|
||||||
ISY_CONF_NAME = "name"
|
ISY_CONF_NAME = "name"
|
||||||
|
@ -186,8 +182,6 @@ UOM_INDEX = "25"
|
||||||
UOM_ON_OFF = "2"
|
UOM_ON_OFF = "2"
|
||||||
UOM_PERCENTAGE = "51"
|
UOM_PERCENTAGE = "51"
|
||||||
|
|
||||||
SENSOR_AUX = "sensor_aux"
|
|
||||||
|
|
||||||
# Do not use the Home Assistant consts for the states here - we're matching exact API
|
# Do not use the Home Assistant consts for the states here - we're matching exact API
|
||||||
# responses, not using them for Home Assistant states
|
# responses, not using them for Home Assistant states
|
||||||
# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml
|
# Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml
|
||||||
|
@ -313,6 +307,10 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
|
||||||
FILTER_ZWAVE_CAT: ["140"],
|
FILTER_ZWAVE_CAT: ["140"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
NODE_AUX_FILTERS: dict[str, Platform] = {
|
||||||
|
PROP_ON_LEVEL: Platform.SENSOR,
|
||||||
|
PROP_RAMP_RATE: Platform.SENSOR,
|
||||||
|
}
|
||||||
|
|
||||||
UOM_FRIENDLY_NAME = {
|
UOM_FRIENDLY_NAME = {
|
||||||
"1": UnitOfElectricCurrent.AMPERE,
|
"1": UnitOfElectricCurrent.AMPERE,
|
||||||
|
|
|
@ -16,15 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE, UOM_BARRIER
|
||||||
_LOGGER,
|
|
||||||
DOMAIN,
|
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NODES,
|
|
||||||
ISY_PROGRAMS,
|
|
||||||
UOM_8_BIT_RANGE,
|
|
||||||
UOM_BARRIER,
|
|
||||||
)
|
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,13 +24,13 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY cover platform."""
|
"""Set up the ISY cover platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
|
entities: list[ISYCoverEntity | ISYCoverProgramEntity] = []
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.COVER]:
|
for node in isy_data.nodes[Platform.COVER]:
|
||||||
entities.append(ISYCoverEntity(node, devices.get(node.primary_node)))
|
entities.append(ISYCoverEntity(node, devices.get(node.primary_node)))
|
||||||
|
|
||||||
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.COVER]:
|
for name, status, actions in isy_data.programs[Platform.COVER]:
|
||||||
entities.append(ISYCoverProgramEntity(name, status, actions))
|
entities.append(ISYCoverProgramEntity(name, status, actions))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from pyisy.constants import (
|
||||||
PROTO_ZWAVE,
|
PROTO_ZWAVE,
|
||||||
)
|
)
|
||||||
from pyisy.helpers import EventListener, NodeProperty
|
from pyisy.helpers import EventListener, NodeProperty
|
||||||
from pyisy.nodes import Node
|
from pyisy.nodes import Group, Node
|
||||||
from pyisy.programs import Program
|
from pyisy.programs import Program
|
||||||
from pyisy.variables import Variable
|
from pyisy.variables import Variable
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ class ISYEntity(Entity):
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
_node: Node | Program | Variable
|
_node: Node | Program | Variable
|
||||||
|
|
||||||
def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
node: Node | Group | Variable | Program,
|
||||||
|
device_info: DeviceInfo | None = None,
|
||||||
|
) -> None:
|
||||||
"""Initialize the insteon device."""
|
"""Initialize the insteon device."""
|
||||||
self._node = node
|
self._node = node
|
||||||
self._attr_name = node.name
|
self._attr_name = node.name
|
||||||
|
@ -89,10 +93,7 @@ class ISYNodeEntity(ISYEntity):
|
||||||
attr = {}
|
attr = {}
|
||||||
node = self._node
|
node = self._node
|
||||||
# Insteon aux_properties are now their own sensors
|
# Insteon aux_properties are now their own sensors
|
||||||
if (
|
if hasattr(self._node, "aux_properties") and node.protocol != PROTO_INSTEON:
|
||||||
hasattr(self._node, "aux_properties")
|
|
||||||
and getattr(node, "protocol", None) != PROTO_INSTEON
|
|
||||||
):
|
|
||||||
for name, value in self._node.aux_properties.items():
|
for name, value in self._node.aux_properties.items():
|
||||||
attr_name = COMMAND_FRIENDLY_NAME.get(name, name)
|
attr_name = COMMAND_FRIENDLY_NAME.get(name, name)
|
||||||
attr[attr_name] = str(value.formatted).lower()
|
attr[attr_name] = str(value.formatted).lower()
|
||||||
|
@ -128,7 +129,7 @@ class ISYNodeEntity(ISYEntity):
|
||||||
|
|
||||||
async def async_get_zwave_parameter(self, parameter: Any) -> None:
|
async def async_get_zwave_parameter(self, parameter: Any) -> None:
|
||||||
"""Respond to an entity service command to request a Z-Wave device parameter from the ISY."""
|
"""Respond to an entity service command to request a Z-Wave device parameter from the ISY."""
|
||||||
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
|
if self._node.protocol != PROTO_ZWAVE:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave"
|
"Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave"
|
||||||
f" device {self.entity_id}"
|
f" device {self.entity_id}"
|
||||||
|
@ -139,7 +140,7 @@ class ISYNodeEntity(ISYEntity):
|
||||||
self, parameter: Any, value: Any | None, size: int | None
|
self, parameter: Any, value: Any | None, size: int | None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Respond to an entity service command to set a Z-Wave device parameter via the ISY."""
|
"""Respond to an entity service command to set a Z-Wave device parameter via the ISY."""
|
||||||
if not hasattr(self._node, "protocol") or self._node.protocol != PROTO_ZWAVE:
|
if self._node.protocol != PROTO_ZWAVE:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave"
|
"Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave"
|
||||||
f" device {self.entity_id}"
|
f" device {self.entity_id}"
|
||||||
|
@ -155,7 +156,10 @@ class ISYNodeEntity(ISYEntity):
|
||||||
class ISYProgramEntity(ISYEntity):
|
class ISYProgramEntity(ISYEntity):
|
||||||
"""Representation of an ISY program base."""
|
"""Representation of an ISY program base."""
|
||||||
|
|
||||||
def __init__(self, name: str, status: Any | None, actions: Program = None) -> None:
|
_actions: Program
|
||||||
|
_status: Program
|
||||||
|
|
||||||
|
def __init__(self, name: str, status: Program, actions: Program = None) -> None:
|
||||||
"""Initialize the ISY program-based entity."""
|
"""Initialize the ISY program-based entity."""
|
||||||
super().__init__(status)
|
super().__init__(status)
|
||||||
self._attr_name = name
|
self._attr_name = name
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.util.percentage import (
|
||||||
ranged_value_to_percentage,
|
ranged_value_to_percentage,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
|
from .const import _LOGGER, DOMAIN
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
|
||||||
SPEED_RANGE = (1, 255) # off is not included
|
SPEED_RANGE = (1, 255) # off is not included
|
||||||
|
@ -28,14 +28,14 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY fan platform."""
|
"""Set up the ISY fan platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
entities: list[ISYFanEntity | ISYFanProgramEntity] = []
|
entities: list[ISYFanEntity | ISYFanProgramEntity] = []
|
||||||
|
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.FAN]:
|
for node in isy_data.nodes[Platform.FAN]:
|
||||||
entities.append(ISYFanEntity(node, devices.get(node.primary_node)))
|
entities.append(ISYFanEntity(node, devices.get(node.primary_node)))
|
||||||
|
|
||||||
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.FAN]:
|
for name, status, actions in isy_data.programs[Platform.FAN]:
|
||||||
entities.append(ISYFanProgramEntity(name, status, actions))
|
entities.append(ISYFanProgramEntity(name, status, actions))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -5,6 +5,11 @@ from typing import cast
|
||||||
|
|
||||||
from pyisy.constants import (
|
from pyisy.constants import (
|
||||||
ISY_VALUE_UNKNOWN,
|
ISY_VALUE_UNKNOWN,
|
||||||
|
PROP_BUSY,
|
||||||
|
PROP_COMMS_ERROR,
|
||||||
|
PROP_ON_LEVEL,
|
||||||
|
PROP_RAMP_RATE,
|
||||||
|
PROP_STATUS,
|
||||||
PROTO_GROUP,
|
PROTO_GROUP,
|
||||||
PROTO_INSTEON,
|
PROTO_INSTEON,
|
||||||
PROTO_PROGRAM,
|
PROTO_PROGRAM,
|
||||||
|
@ -27,18 +32,12 @@ from .const import (
|
||||||
FILTER_STATES,
|
FILTER_STATES,
|
||||||
FILTER_UOM,
|
FILTER_UOM,
|
||||||
FILTER_ZWAVE_CAT,
|
FILTER_ZWAVE_CAT,
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_GROUP_PLATFORM,
|
ISY_GROUP_PLATFORM,
|
||||||
ISY_NODES,
|
|
||||||
ISY_PROGRAMS,
|
|
||||||
ISY_ROOT_NODES,
|
|
||||||
ISY_VARIABLES,
|
|
||||||
KEY_ACTIONS,
|
KEY_ACTIONS,
|
||||||
KEY_STATUS,
|
KEY_STATUS,
|
||||||
NODE_FILTERS,
|
NODE_FILTERS,
|
||||||
NODE_PLATFORMS,
|
NODE_PLATFORMS,
|
||||||
PROGRAM_PLATFORMS,
|
PROGRAM_PLATFORMS,
|
||||||
SENSOR_AUX,
|
|
||||||
SUBNODE_CLIMATE_COOL,
|
SUBNODE_CLIMATE_COOL,
|
||||||
SUBNODE_CLIMATE_HEAT,
|
SUBNODE_CLIMATE_HEAT,
|
||||||
SUBNODE_EZIO2X4_SENSORS,
|
SUBNODE_EZIO2X4_SENSORS,
|
||||||
|
@ -49,13 +48,19 @@ from .const import (
|
||||||
UOM_DOUBLE_TEMP,
|
UOM_DOUBLE_TEMP,
|
||||||
UOM_ISYV4_DEGREES,
|
UOM_ISYV4_DEGREES,
|
||||||
)
|
)
|
||||||
|
from .models import IsyData
|
||||||
|
|
||||||
BINARY_SENSOR_UOMS = ["2", "78"]
|
BINARY_SENSOR_UOMS = ["2", "78"]
|
||||||
BINARY_SENSOR_ISY_STATES = ["on", "off"]
|
BINARY_SENSOR_ISY_STATES = ["on", "off"]
|
||||||
|
ROOT_AUX_CONTROLS = {
|
||||||
|
PROP_ON_LEVEL,
|
||||||
|
PROP_RAMP_RATE,
|
||||||
|
}
|
||||||
|
SKIP_AUX_PROPS = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS, *ROOT_AUX_CONTROLS}
|
||||||
|
|
||||||
|
|
||||||
def _check_for_node_def(
|
def _check_for_node_def(
|
||||||
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None
|
isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the node_def_id for any platforms.
|
"""Check if the node matches the node_def_id for any platforms.
|
||||||
|
|
||||||
|
@ -71,14 +76,14 @@ def _check_for_node_def(
|
||||||
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
|
if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
|
||||||
hass_isy_data[ISY_NODES][platform].append(node)
|
isy_data.nodes[platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_insteon_type(
|
def _check_for_insteon_type(
|
||||||
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None
|
isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the Insteon type for any platforms.
|
"""Check if the node matches the Insteon type for any platforms.
|
||||||
|
|
||||||
|
@ -107,7 +112,7 @@ def _check_for_insteon_type(
|
||||||
|
|
||||||
# FanLinc, which has a light module as one of its nodes.
|
# FanLinc, which has a light module as one of its nodes.
|
||||||
if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
|
if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT:
|
||||||
hass_isy_data[ISY_NODES][Platform.LIGHT].append(node)
|
isy_data.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
|
||||||
|
@ -115,7 +120,7 @@ def _check_for_insteon_type(
|
||||||
SUBNODE_CLIMATE_COOL,
|
SUBNODE_CLIMATE_COOL,
|
||||||
SUBNODE_CLIMATE_HEAT,
|
SUBNODE_CLIMATE_HEAT,
|
||||||
):
|
):
|
||||||
hass_isy_data[ISY_NODES][Platform.BINARY_SENSOR].append(node)
|
isy_data.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
|
||||||
|
@ -124,7 +129,7 @@ def _check_for_insteon_type(
|
||||||
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[ISY_NODES][Platform.SWITCH].append(node)
|
isy_data.nodes[Platform.SWITCH].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Smartenit EZIO2X4
|
# Smartenit EZIO2X4
|
||||||
|
@ -133,17 +138,17 @@ def _check_for_insteon_type(
|
||||||
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[ISY_NODES][Platform.BINARY_SENSOR].append(node)
|
isy_data.nodes[Platform.BINARY_SENSOR].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
hass_isy_data[ISY_NODES][platform].append(node)
|
isy_data.nodes[platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_zwave_cat(
|
def _check_for_zwave_cat(
|
||||||
hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None
|
isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if the node matches the ISY Z-Wave Category for any platforms.
|
"""Check if the node matches the ISY Z-Wave Category for any platforms.
|
||||||
|
|
||||||
|
@ -164,14 +169,14 @@ def _check_for_zwave_cat(
|
||||||
device_type.startswith(t)
|
device_type.startswith(t)
|
||||||
for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
|
for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
|
||||||
):
|
):
|
||||||
hass_isy_data[ISY_NODES][platform].append(node)
|
isy_data.nodes[platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_uom_id(
|
def _check_for_uom_id(
|
||||||
hass_isy_data: dict,
|
isy_data: IsyData,
|
||||||
node: Group | Node,
|
node: Group | Node,
|
||||||
single_platform: Platform | None = None,
|
single_platform: Platform | None = None,
|
||||||
uom_list: list[str] | None = None,
|
uom_list: list[str] | None = None,
|
||||||
|
@ -190,23 +195,23 @@ def _check_for_uom_id(
|
||||||
if isinstance(node.uom, list):
|
if isinstance(node.uom, list):
|
||||||
node_uom = node.uom[0]
|
node_uom = node.uom[0]
|
||||||
|
|
||||||
if uom_list:
|
if uom_list and single_platform:
|
||||||
if node_uom in uom_list:
|
if node_uom in uom_list:
|
||||||
hass_isy_data[ISY_NODES][single_platform].append(node)
|
isy_data.nodes[single_platform].append(node)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
|
if node_uom in NODE_FILTERS[platform][FILTER_UOM]:
|
||||||
hass_isy_data[ISY_NODES][platform].append(node)
|
isy_data.nodes[platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _check_for_states_in_uom(
|
def _check_for_states_in_uom(
|
||||||
hass_isy_data: dict,
|
isy_data: IsyData,
|
||||||
node: Group | Node,
|
node: Group | Node,
|
||||||
single_platform: Platform | None = None,
|
single_platform: Platform | None = None,
|
||||||
states_list: list[str] | None = None,
|
states_list: list[str] | None = None,
|
||||||
|
@ -227,28 +232,26 @@ def _check_for_states_in_uom(
|
||||||
|
|
||||||
node_uom = set(map(str.lower, node.uom))
|
node_uom = set(map(str.lower, node.uom))
|
||||||
|
|
||||||
if states_list:
|
if states_list and single_platform:
|
||||||
if node_uom == set(states_list):
|
if node_uom == set(states_list):
|
||||||
hass_isy_data[ISY_NODES][single_platform].append(node)
|
isy_data.nodes[single_platform].append(node)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
platforms = NODE_PLATFORMS if not single_platform else [single_platform]
|
||||||
for platform in platforms:
|
for platform in platforms:
|
||||||
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
|
if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
|
||||||
hass_isy_data[ISY_NODES][platform].append(node)
|
isy_data.nodes[platform].append(node)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool:
|
def _is_sensor_a_binary_sensor(isy_data: IsyData, node: Group | Node) -> bool:
|
||||||
"""Determine if the given sensor node should be a binary_sensor."""
|
"""Determine if the given sensor node should be a binary_sensor."""
|
||||||
if _check_for_node_def(hass_isy_data, node, single_platform=Platform.BINARY_SENSOR):
|
if _check_for_node_def(isy_data, node, single_platform=Platform.BINARY_SENSOR):
|
||||||
return True
|
return True
|
||||||
if _check_for_insteon_type(
|
if _check_for_insteon_type(isy_data, node, single_platform=Platform.BINARY_SENSOR):
|
||||||
hass_isy_data, node, single_platform=Platform.BINARY_SENSOR
|
|
||||||
):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# For the next two checks, we're providing our own set of uoms that
|
# For the next two checks, we're providing our own set of uoms that
|
||||||
|
@ -256,14 +259,14 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool:
|
||||||
# checks in the context of already knowing that this is definitely a
|
# checks in the context of already knowing that this is definitely a
|
||||||
# sensor device.
|
# sensor device.
|
||||||
if _check_for_uom_id(
|
if _check_for_uom_id(
|
||||||
hass_isy_data,
|
isy_data,
|
||||||
node,
|
node,
|
||||||
single_platform=Platform.BINARY_SENSOR,
|
single_platform=Platform.BINARY_SENSOR,
|
||||||
uom_list=BINARY_SENSOR_UOMS,
|
uom_list=BINARY_SENSOR_UOMS,
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
if _check_for_states_in_uom(
|
if _check_for_states_in_uom(
|
||||||
hass_isy_data,
|
isy_data,
|
||||||
node,
|
node,
|
||||||
single_platform=Platform.BINARY_SENSOR,
|
single_platform=Platform.BINARY_SENSOR,
|
||||||
states_list=BINARY_SENSOR_ISY_STATES,
|
states_list=BINARY_SENSOR_ISY_STATES,
|
||||||
|
@ -309,7 +312,7 @@ def _generate_device_info(node: Node) -> DeviceInfo:
|
||||||
|
|
||||||
|
|
||||||
def _categorize_nodes(
|
def _categorize_nodes(
|
||||||
hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
|
isy_data: IsyData, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Sort the nodes to their proper platforms."""
|
"""Sort the nodes to their proper platforms."""
|
||||||
for path, node in nodes:
|
for path, node in nodes:
|
||||||
|
@ -320,44 +323,53 @@ def _categorize_nodes(
|
||||||
|
|
||||||
if hasattr(node, "parent_node") and node.parent_node is None:
|
if hasattr(node, "parent_node") and node.parent_node is None:
|
||||||
# This is a physical device / parent node
|
# This is a physical device / parent node
|
||||||
hass_isy_data[ISY_DEVICES][node.address] = _generate_device_info(node)
|
isy_data.devices[node.address] = _generate_device_info(node)
|
||||||
hass_isy_data[ISY_ROOT_NODES][Platform.BUTTON].append(node)
|
isy_data.root_nodes[Platform.BUTTON].append(node)
|
||||||
|
# Any parent node can have communication errors:
|
||||||
|
isy_data.aux_properties[Platform.SENSOR].append((node, PROP_COMMS_ERROR))
|
||||||
|
# Add Ramp Rate and On Levels for Dimmable Load devices
|
||||||
|
if getattr(node, "is_dimmable", False):
|
||||||
|
aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
|
||||||
|
for control in aux_controls:
|
||||||
|
isy_data.aux_properties[Platform.SENSOR].append((node, control))
|
||||||
|
|
||||||
if node.protocol == PROTO_GROUP:
|
if node.protocol == PROTO_GROUP:
|
||||||
hass_isy_data[ISY_NODES][ISY_GROUP_PLATFORM].append(node)
|
isy_data.nodes[ISY_GROUP_PLATFORM].append(node)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.protocol == PROTO_INSTEON:
|
if node.protocol == PROTO_INSTEON:
|
||||||
for control in node.aux_properties:
|
for control in node.aux_properties:
|
||||||
hass_isy_data[ISY_NODES][SENSOR_AUX].append((node, control))
|
if control in SKIP_AUX_PROPS:
|
||||||
|
continue
|
||||||
|
isy_data.aux_properties[Platform.SENSOR].append((node, control))
|
||||||
|
|
||||||
if sensor_identifier in path or sensor_identifier in node.name:
|
if sensor_identifier in path or sensor_identifier in node.name:
|
||||||
# User has specified to treat this as a sensor. First we need to
|
# User has specified to treat this as a sensor. First we need to
|
||||||
# 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(isy_data, node):
|
||||||
continue
|
continue
|
||||||
hass_isy_data[ISY_NODES][Platform.SENSOR].append(node)
|
isy_data.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,
|
||||||
# each of which works with different ISY firmware versions or device
|
# each of which works with different ISY firmware versions or device
|
||||||
# family. The order here is important, from most reliable to least.
|
# family. The order here is important, from most reliable to least.
|
||||||
if _check_for_node_def(hass_isy_data, node):
|
if _check_for_node_def(isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_insteon_type(hass_isy_data, node):
|
if _check_for_insteon_type(isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_zwave_cat(hass_isy_data, node):
|
if _check_for_zwave_cat(isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_uom_id(hass_isy_data, node):
|
if _check_for_uom_id(isy_data, node):
|
||||||
continue
|
continue
|
||||||
if _check_for_states_in_uom(hass_isy_data, node):
|
if _check_for_states_in_uom(isy_data, node):
|
||||||
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[ISY_NODES][Platform.SENSOR].append(node)
|
isy_data.nodes[Platform.SENSOR].append(node)
|
||||||
|
|
||||||
|
|
||||||
def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
|
def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
|
||||||
"""Categorize the ISY programs."""
|
"""Categorize the ISY programs."""
|
||||||
for platform in PROGRAM_PLATFORMS:
|
for platform in PROGRAM_PLATFORMS:
|
||||||
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
|
folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}")
|
||||||
|
@ -393,25 +405,21 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entity = (entity_folder.name, status, actions)
|
entity = (entity_folder.name, status, actions)
|
||||||
hass_isy_data[ISY_PROGRAMS][platform].append(entity)
|
isy_data.programs[platform].append(entity)
|
||||||
|
|
||||||
|
|
||||||
def _categorize_variables(
|
def _categorize_variables(
|
||||||
hass_isy_data: dict, variables: Variables, identifier: str
|
isy_data: IsyData, variables: Variables, identifier: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Gather the ISY Variables to be added as sensors."""
|
"""Gather the ISY Variables to be added as sensors."""
|
||||||
try:
|
try:
|
||||||
var_to_add = [
|
isy_data.variables[Platform.SENSOR] = [
|
||||||
(vtype, vname, vid)
|
variables[vtype][vid]
|
||||||
for (vtype, vname, vid) in variables.children
|
for (vtype, vname, vid) in variables.children
|
||||||
if identifier in vname
|
if identifier in vname
|
||||||
]
|
]
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
_LOGGER.error("Error adding ISY Variables: %s", err)
|
_LOGGER.error("Error adding ISY Variables: %s", err)
|
||||||
return
|
|
||||||
variable_entities = hass_isy_data[ISY_VARIABLES]
|
|
||||||
for vtype, vname, vid in var_to_add:
|
|
||||||
variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid]))
|
|
||||||
|
|
||||||
|
|
||||||
def convert_isy_value_to_hass(
|
def convert_isy_value_to_hass(
|
||||||
|
|
|
@ -15,14 +15,7 @@ from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
from .const import (
|
from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
|
||||||
_LOGGER,
|
|
||||||
CONF_RESTORE_LIGHT_STATE,
|
|
||||||
DOMAIN,
|
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NODES,
|
|
||||||
UOM_PERCENTAGE,
|
|
||||||
)
|
|
||||||
from .entity import ISYNodeEntity
|
from .entity import ISYNodeEntity
|
||||||
from .services import async_setup_light_services
|
from .services import async_setup_light_services
|
||||||
|
|
||||||
|
@ -33,13 +26,13 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY light platform."""
|
"""Set up the ISY light platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
isy_options = entry.options
|
isy_options = entry.options
|
||||||
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
|
restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False)
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.LIGHT]:
|
for node in isy_data.nodes[Platform.LIGHT]:
|
||||||
entities.append(
|
entities.append(
|
||||||
ISYLightEntity(node, restore_light_state, devices.get(node.primary_node))
|
ISYLightEntity(node, restore_light_state, devices.get(node.primary_node))
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
|
from .const import _LOGGER, DOMAIN
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
|
||||||
VALUE_TO_STATE = {0: False, 100: True}
|
VALUE_TO_STATE = {0: False, 100: True}
|
||||||
|
@ -22,13 +22,13 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY lock platform."""
|
"""Set up the ISY lock platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
entities: list[ISYLockEntity | ISYLockProgramEntity] = []
|
entities: list[ISYLockEntity | ISYLockProgramEntity] = []
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.LOCK]:
|
for node in isy_data.nodes[Platform.LOCK]:
|
||||||
entities.append(ISYLockEntity(node, devices.get(node.primary_node)))
|
entities.append(ISYLockEntity(node, devices.get(node.primary_node)))
|
||||||
|
|
||||||
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.LOCK]:
|
for name, status, actions in isy_data.programs[Platform.LOCK]:
|
||||||
entities.append(ISYLockProgramEntity(name, status, actions))
|
entities.append(ISYLockProgramEntity(name, status, actions))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
96
homeassistant/components/isy994/models.py
Normal file
96
homeassistant/components/isy994/models.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
"""The ISY/IoX integration data models."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from pyisy import ISY
|
||||||
|
from pyisy.constants import PROTO_INSTEON
|
||||||
|
from pyisy.networking import NetworkCommand
|
||||||
|
from pyisy.nodes import Group, Node
|
||||||
|
from pyisy.programs import Program
|
||||||
|
from pyisy.variables import Variable
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_NETWORK,
|
||||||
|
NODE_AUX_PROP_PLATFORMS,
|
||||||
|
NODE_PLATFORMS,
|
||||||
|
PROGRAM_PLATFORMS,
|
||||||
|
ROOT_NODE_PLATFORMS,
|
||||||
|
VARIABLE_PLATFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class IsyData:
|
||||||
|
"""Data for the ISY/IoX integration."""
|
||||||
|
|
||||||
|
root: ISY
|
||||||
|
nodes: dict[Platform, list[Node | Group]]
|
||||||
|
root_nodes: dict[Platform, list[Node]]
|
||||||
|
variables: dict[Platform, list[Variable]]
|
||||||
|
programs: dict[Platform, list[tuple[str, Program, Program]]]
|
||||||
|
net_resources: list[NetworkCommand]
|
||||||
|
devices: dict[str, DeviceInfo]
|
||||||
|
aux_properties: dict[Platform, list[tuple[Node, str]]]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize an empty ISY data class."""
|
||||||
|
self.nodes = {p: [] for p in NODE_PLATFORMS}
|
||||||
|
self.root_nodes = {p: [] for p in ROOT_NODE_PLATFORMS}
|
||||||
|
self.aux_properties = {p: [] for p in NODE_AUX_PROP_PLATFORMS}
|
||||||
|
self.programs = {p: [] for p in PROGRAM_PLATFORMS}
|
||||||
|
self.variables = {p: [] for p in VARIABLE_PLATFORMS}
|
||||||
|
self.net_resources = []
|
||||||
|
self.devices = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uuid(self) -> str:
|
||||||
|
"""Return the ISY UUID identification."""
|
||||||
|
return cast(str, self.root.uuid)
|
||||||
|
|
||||||
|
def uid_base(self, node: Node | Group | Variable | Program | NetworkCommand) -> str:
|
||||||
|
"""Return the unique id base string for a given node."""
|
||||||
|
if isinstance(node, NetworkCommand):
|
||||||
|
return f"{self.uuid}_{CONF_NETWORK}_{node.address}"
|
||||||
|
return f"{self.uuid}_{node.address}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_ids(self) -> set[tuple[Platform, str]]:
|
||||||
|
"""Return all the unique ids for a config entry id."""
|
||||||
|
current_unique_ids: set[tuple[Platform, str]] = {
|
||||||
|
(Platform.BUTTON, f"{self.uuid}_query")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Structure and prefixes here must match what's added in __init__ and helpers
|
||||||
|
for platform in NODE_PLATFORMS:
|
||||||
|
for node in self.nodes[platform]:
|
||||||
|
current_unique_ids.add((platform, self.uid_base(node)))
|
||||||
|
|
||||||
|
for platform in NODE_AUX_PROP_PLATFORMS:
|
||||||
|
for node, control in self.aux_properties[platform]:
|
||||||
|
current_unique_ids.add((platform, f"{self.uid_base(node)}_{control}"))
|
||||||
|
|
||||||
|
for platform in PROGRAM_PLATFORMS:
|
||||||
|
for _, node, _ in self.programs[platform]:
|
||||||
|
current_unique_ids.add((platform, self.uid_base(node)))
|
||||||
|
|
||||||
|
for platform in VARIABLE_PLATFORMS:
|
||||||
|
for node in self.variables[platform]:
|
||||||
|
current_unique_ids.add((platform, self.uid_base(node)))
|
||||||
|
if platform == Platform.NUMBER:
|
||||||
|
current_unique_ids.add((platform, f"{self.uid_base(node)}_init"))
|
||||||
|
|
||||||
|
for platform in ROOT_NODE_PLATFORMS:
|
||||||
|
for node in self.root_nodes[platform]:
|
||||||
|
current_unique_ids.add((platform, f"{self.uid_base(node)}_query"))
|
||||||
|
if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON:
|
||||||
|
current_unique_ids.add((platform, f"{self.uid_base(node)}_beep"))
|
||||||
|
|
||||||
|
for node in self.net_resources:
|
||||||
|
current_unique_ids.add((Platform.BUTTON, self.uid_base(node)))
|
||||||
|
|
||||||
|
return current_unique_ids
|
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyisy import ISY
|
|
||||||
from pyisy.helpers import EventListener, NodeProperty
|
from pyisy.helpers import EventListener, NodeProperty
|
||||||
from pyisy.variables import Variable
|
from pyisy.variables import Variable
|
||||||
|
|
||||||
|
@ -14,7 +13,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, ISY_DEVICES, ISY_ROOT, ISY_VARIABLES
|
from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN
|
||||||
from .helpers import convert_isy_value_to_hass
|
from .helpers import convert_isy_value_to_hass
|
||||||
|
|
||||||
ISY_MAX_SIZE = (2**32) / 2
|
ISY_MAX_SIZE = (2**32) / 2
|
||||||
|
@ -26,19 +25,19 @@ async def async_setup_entry(
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up ISY/IoX number entities from config entry."""
|
"""Set up ISY/IoX number entities from config entry."""
|
||||||
hass_isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
isy_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
isy: ISY = hass_isy_data[ISY_ROOT]
|
device_info = isy_data.devices
|
||||||
device_info = hass_isy_data[ISY_DEVICES]
|
|
||||||
entities: list[ISYVariableNumberEntity] = []
|
entities: list[ISYVariableNumberEntity] = []
|
||||||
|
var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
|
||||||
|
|
||||||
for node, enable_by_default in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]:
|
for node in isy_data.variables[Platform.NUMBER]:
|
||||||
step = 10 ** (-1 * node.prec)
|
step = 10 ** (-1 * node.prec)
|
||||||
min_max = ISY_MAX_SIZE / (10**node.prec)
|
min_max = ISY_MAX_SIZE / (10**node.prec)
|
||||||
description = NumberEntityDescription(
|
description = NumberEntityDescription(
|
||||||
key=node.address,
|
key=node.address,
|
||||||
name=node.name,
|
name=node.name,
|
||||||
icon="mdi:counter",
|
icon="mdi:counter",
|
||||||
entity_registry_enabled_default=enable_by_default,
|
entity_registry_enabled_default=var_id in node.name,
|
||||||
native_unit_of_measurement=None,
|
native_unit_of_measurement=None,
|
||||||
native_step=step,
|
native_step=step,
|
||||||
native_min_value=-min_max,
|
native_min_value=-min_max,
|
||||||
|
@ -59,7 +58,7 @@ async def async_setup_entry(
|
||||||
entities.append(
|
entities.append(
|
||||||
ISYVariableNumberEntity(
|
ISYVariableNumberEntity(
|
||||||
node,
|
node,
|
||||||
unique_id=f"{isy.uuid}_{node.address}",
|
unique_id=isy_data.uid_base(node),
|
||||||
description=description,
|
description=description,
|
||||||
device_info=device_info[CONF_VARIABLES],
|
device_info=device_info[CONF_VARIABLES],
|
||||||
)
|
)
|
||||||
|
@ -67,7 +66,7 @@ async def async_setup_entry(
|
||||||
entities.append(
|
entities.append(
|
||||||
ISYVariableNumberEntity(
|
ISYVariableNumberEntity(
|
||||||
node=node,
|
node=node,
|
||||||
unique_id=f"{isy.uuid}_{node.address}_init",
|
unique_id=f"{isy_data.uid_base(node)}_init",
|
||||||
description=description_init,
|
description=description_init,
|
||||||
device_info=device_info[CONF_VARIABLES],
|
device_info=device_info[CONF_VARIABLES],
|
||||||
init_entity=True,
|
init_entity=True,
|
||||||
|
|
|
@ -19,6 +19,7 @@ from pyisy.constants import (
|
||||||
)
|
)
|
||||||
from pyisy.helpers import NodeProperty
|
from pyisy.helpers import NodeProperty
|
||||||
from pyisy.nodes import Node
|
from pyisy.nodes import Node
|
||||||
|
from pyisy.variables import Variable
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -34,10 +35,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from .const import (
|
from .const import (
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ISY_DEVICES,
|
|
||||||
ISY_NODES,
|
|
||||||
ISY_VARIABLES,
|
|
||||||
SENSOR_AUX,
|
|
||||||
UOM_DOUBLE_TEMP,
|
UOM_DOUBLE_TEMP,
|
||||||
UOM_FRIENDLY_NAME,
|
UOM_FRIENDLY_NAME,
|
||||||
UOM_INDEX,
|
UOM_INDEX,
|
||||||
|
@ -110,20 +107,17 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY sensor platform."""
|
"""Set up the ISY sensor platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
|
entities: list[ISYSensorEntity | ISYSensorVariableEntity] = []
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
|
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.SENSOR]:
|
for node in isy_data.nodes[Platform.SENSOR]:
|
||||||
_LOGGER.debug("Loading %s", node.name)
|
_LOGGER.debug("Loading %s", node.name)
|
||||||
entities.append(ISYSensorEntity(node, devices.get(node.primary_node)))
|
entities.append(ISYSensorEntity(node, devices.get(node.primary_node)))
|
||||||
|
|
||||||
aux_nodes = set()
|
aux_sensors_list = isy_data.aux_properties[Platform.SENSOR]
|
||||||
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]:
|
for node, control in aux_sensors_list:
|
||||||
aux_nodes.add(node)
|
_LOGGER.debug("Loading %s %s", node.name, COMMAND_FRIENDLY_NAME.get(control))
|
||||||
if control in SKIP_AUX_PROPERTIES:
|
|
||||||
continue
|
|
||||||
_LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control])
|
|
||||||
enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any(
|
enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any(
|
||||||
control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH
|
control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH
|
||||||
)
|
)
|
||||||
|
@ -132,23 +126,13 @@ async def async_setup_entry(
|
||||||
node=node,
|
node=node,
|
||||||
control=control,
|
control=control,
|
||||||
enabled_default=enabled_default,
|
enabled_default=enabled_default,
|
||||||
|
unique_id=f"{isy_data.uid_base(node)}_{control}",
|
||||||
device_info=devices.get(node.primary_node),
|
device_info=devices.get(node.primary_node),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for node in aux_nodes:
|
for variable in isy_data.variables[Platform.SENSOR]:
|
||||||
# Any node in SENSOR_AUX can potentially have communication errors
|
entities.append(ISYSensorVariableEntity(variable))
|
||||||
entities.append(
|
|
||||||
ISYAuxSensorEntity(
|
|
||||||
node=node,
|
|
||||||
control=PROP_COMMS_ERROR,
|
|
||||||
enabled_default=False,
|
|
||||||
device_info=devices.get(node.primary_node),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for vname, vobj in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]:
|
|
||||||
entities.append(ISYSensorVariableEntity(vname, vobj))
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
@ -248,6 +232,7 @@ class ISYAuxSensorEntity(ISYSensorEntity):
|
||||||
node: Node,
|
node: Node,
|
||||||
control: str,
|
control: str,
|
||||||
enabled_default: bool,
|
enabled_default: bool,
|
||||||
|
unique_id: str,
|
||||||
device_info: DeviceInfo | None = None,
|
device_info: DeviceInfo | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the ISY aux sensor."""
|
"""Initialize the ISY aux sensor."""
|
||||||
|
@ -257,12 +242,11 @@ class ISYAuxSensorEntity(ISYSensorEntity):
|
||||||
self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control)
|
self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control)
|
||||||
self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control)
|
self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control)
|
||||||
self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control)
|
self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control)
|
||||||
|
self._attr_unique_id = unique_id
|
||||||
|
|
||||||
name = COMMAND_FRIENDLY_NAME.get(self._control, self._control)
|
name = COMMAND_FRIENDLY_NAME.get(self._control, self._control)
|
||||||
self._attr_name = f"{node.name} {name.replace('_', ' ').title()}"
|
self._attr_name = f"{node.name} {name.replace('_', ' ').title()}"
|
||||||
|
|
||||||
self._attr_unique_id = f"{node.isy.uuid}_{node.address}_{control}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target(self) -> Node | NodeProperty | None:
|
def target(self) -> Node | NodeProperty | None:
|
||||||
"""Return target for the sensor."""
|
"""Return target for the sensor."""
|
||||||
|
@ -283,10 +267,10 @@ class ISYSensorVariableEntity(ISYEntity, SensorEntity):
|
||||||
# Deprecated sensors, will be removed in 2023.5.0
|
# Deprecated sensors, will be removed in 2023.5.0
|
||||||
_attr_entity_registry_enabled_default = False
|
_attr_entity_registry_enabled_default = False
|
||||||
|
|
||||||
def __init__(self, vname: str, vobj: object) -> None:
|
def __init__(self, variable_node: Variable) -> None:
|
||||||
"""Initialize the ISY binary sensor program."""
|
"""Initialize the ISY binary sensor program."""
|
||||||
super().__init__(vobj)
|
super().__init__(variable_node)
|
||||||
self._name = vname
|
self._name = variable_node.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | int | None:
|
def native_value(self) -> float | int | None:
|
||||||
|
|
|
@ -23,15 +23,8 @@ import homeassistant.helpers.entity_registry as er
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
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 (
|
from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING
|
||||||
_LOGGER,
|
from .util import _async_cleanup_registry_entries
|
||||||
CONF_NETWORK,
|
|
||||||
DOMAIN,
|
|
||||||
ISY_CONF_NAME,
|
|
||||||
ISY_CONF_NETWORKING,
|
|
||||||
ISY_ROOT,
|
|
||||||
)
|
|
||||||
from .util import unique_ids_for_config_entry_id
|
|
||||||
|
|
||||||
# Common Services for All Platforms:
|
# Common Services for All Platforms:
|
||||||
SERVICE_SYSTEM_QUERY = "system_query"
|
SERVICE_SYSTEM_QUERY = "system_query"
|
||||||
|
@ -192,7 +185,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||||
isy_name = service.data.get(CONF_ISY)
|
isy_name = service.data.get(CONF_ISY)
|
||||||
entity_registry = er.async_get(hass)
|
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][ISY_ROOT]
|
isy_data = hass.data[DOMAIN][config_entry_id]
|
||||||
|
isy = isy_data.root
|
||||||
if isy_name and isy_name != isy.conf["name"]:
|
if isy_name and isy_name != isy.conf["name"]:
|
||||||
continue
|
continue
|
||||||
# If an address is provided, make sure we query the correct ISY.
|
# If an address is provided, make sure we query the correct ISY.
|
||||||
|
@ -235,7 +229,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||||
isy_name = service.data.get(CONF_ISY)
|
isy_name = service.data.get(CONF_ISY)
|
||||||
|
|
||||||
for config_entry_id in hass.data[DOMAIN]:
|
for config_entry_id in hass.data[DOMAIN]:
|
||||||
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
|
isy_data = hass.data[DOMAIN][config_entry_id]
|
||||||
|
isy = isy_data.root
|
||||||
if isy_name and isy_name != isy.conf[ISY_CONF_NAME]:
|
if isy_name and isy_name != isy.conf[ISY_CONF_NAME]:
|
||||||
continue
|
continue
|
||||||
if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]:
|
if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]:
|
||||||
|
@ -272,7 +267,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||||
isy_name = service.data.get(CONF_ISY)
|
isy_name = service.data.get(CONF_ISY)
|
||||||
|
|
||||||
for config_entry_id in hass.data[DOMAIN]:
|
for config_entry_id in hass.data[DOMAIN]:
|
||||||
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
|
isy_data = hass.data[DOMAIN][config_entry_id]
|
||||||
|
isy = isy_data.root
|
||||||
if isy_name and isy_name != isy.conf["name"]:
|
if isy_name and isy_name != isy.conf["name"]:
|
||||||
continue
|
continue
|
||||||
program = None
|
program = None
|
||||||
|
@ -295,7 +291,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||||
isy_name = service.data.get(CONF_ISY)
|
isy_name = service.data.get(CONF_ISY)
|
||||||
|
|
||||||
for config_entry_id in hass.data[DOMAIN]:
|
for config_entry_id in hass.data[DOMAIN]:
|
||||||
isy = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
|
isy_data = hass.data[DOMAIN][config_entry_id]
|
||||||
|
isy = isy_data.root
|
||||||
if isy_name and isy_name != isy.conf["name"]:
|
if isy_name and isy_name != isy.conf["name"]:
|
||||||
continue
|
continue
|
||||||
variable = None
|
variable = None
|
||||||
|
@ -323,32 +320,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||||
@callback
|
@callback
|
||||||
def async_cleanup_registry_entries(service: ServiceCall) -> None:
|
def async_cleanup_registry_entries(service: ServiceCall) -> None:
|
||||||
"""Remove extra entities that are no longer part of the integration."""
|
"""Remove extra entities that are no longer part of the integration."""
|
||||||
entity_registry = er.async_get(hass)
|
|
||||||
|
|
||||||
for config_entry_id in hass.data[DOMAIN]:
|
for config_entry_id in hass.data[DOMAIN]:
|
||||||
entries_for_this_config = er.async_entries_for_config_entry(
|
_async_cleanup_registry_entries(hass, config_entry_id)
|
||||||
entity_registry, config_entry_id
|
|
||||||
)
|
|
||||||
entities = {
|
|
||||||
(entity.domain, entity.unique_id): entity.entity_id
|
|
||||||
for entity in entries_for_this_config
|
|
||||||
}
|
|
||||||
|
|
||||||
extra_entities = set(entities.keys()).difference(
|
|
||||||
unique_ids_for_config_entry_id(hass, config_entry_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
for entity in extra_entities:
|
|
||||||
if entity_registry.async_is_registered(entities[entity]):
|
|
||||||
entity_registry.async_remove(entities[entity])
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
(
|
|
||||||
"Cleaning up ISY entities: removed %s extra entities for config entry: %s"
|
|
||||||
),
|
|
||||||
len(extra_entities),
|
|
||||||
len(config_entry_id),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_reload_config_entries(service: ServiceCall) -> None:
|
async def async_reload_config_entries(service: ServiceCall) -> None:
|
||||||
"""Trigger a reload of all ISY config entries."""
|
"""Trigger a reload of all ISY config entries."""
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN, ISY_DEVICES, ISY_NODES, ISY_PROGRAMS
|
from .const import _LOGGER, DOMAIN
|
||||||
from .entity import ISYNodeEntity, ISYProgramEntity
|
from .entity import ISYNodeEntity, ISYProgramEntity
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,10 +20,10 @@ async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the ISY switch platform."""
|
"""Set up the ISY switch platform."""
|
||||||
hass_isy_data = hass.data[DOMAIN][entry.entry_id]
|
isy_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
|
entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = []
|
||||||
devices: dict[str, DeviceInfo] = hass_isy_data[ISY_DEVICES]
|
devices: dict[str, DeviceInfo] = isy_data.devices
|
||||||
for node in hass_isy_data[ISY_NODES][Platform.SWITCH]:
|
for node in isy_data.nodes[Platform.SWITCH]:
|
||||||
primary = node.primary_node
|
primary = node.primary_node
|
||||||
if node.protocol == PROTO_GROUP and len(node.controllers) == 1:
|
if node.protocol == PROTO_GROUP and len(node.controllers) == 1:
|
||||||
# If Group has only 1 Controller, link to that device instead of the hub
|
# If Group has only 1 Controller, link to that device instead of the hub
|
||||||
|
@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||||
|
|
||||||
entities.append(ISYSwitchEntity(node, devices.get(primary)))
|
entities.append(ISYSwitchEntity(node, devices.get(primary)))
|
||||||
|
|
||||||
for name, status, actions in hass_isy_data[ISY_PROGRAMS][Platform.SWITCH]:
|
for name, status, actions in isy_data.programs[Platform.SWITCH]:
|
||||||
entities.append(ISYSwitchProgramEntity(name, status, actions))
|
entities.append(ISYSwitchProgramEntity(name, status, actions))
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
from .const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX
|
from .const import DOMAIN, ISY_URL_POSTFIX
|
||||||
|
from .models import IsyData
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -28,7 +29,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||||
config_entry_id = next(
|
config_entry_id = next(
|
||||||
iter(hass.data[DOMAIN])
|
iter(hass.data[DOMAIN])
|
||||||
) # Only first ISY is supported for now
|
) # Only first ISY is supported for now
|
||||||
isy: ISY = hass.data[DOMAIN][config_entry_id][ISY_ROOT]
|
isy_data: IsyData = hass.data[DOMAIN][config_entry_id]
|
||||||
|
isy: ISY = isy_data.root
|
||||||
|
|
||||||
entry = hass.config_entries.async_get_entry(config_entry_id)
|
entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||||
assert isinstance(entry, ConfigEntry)
|
assert isinstance(entry, ConfigEntry)
|
||||||
|
|
|
@ -1,69 +1,34 @@
|
||||||
"""ISY utils."""
|
"""ISY utils."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pyisy.constants import PROP_COMMS_ERROR, PROTO_INSTEON
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from .const import _LOGGER, DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import (
|
|
||||||
CONF_NETWORK,
|
|
||||||
DOMAIN,
|
|
||||||
ISY_NET_RES,
|
|
||||||
ISY_NODES,
|
|
||||||
ISY_PROGRAMS,
|
|
||||||
ISY_ROOT,
|
|
||||||
ISY_ROOT_NODES,
|
|
||||||
ISY_VARIABLES,
|
|
||||||
NODE_PLATFORMS,
|
|
||||||
PROGRAM_PLATFORMS,
|
|
||||||
ROOT_NODE_PLATFORMS,
|
|
||||||
SENSOR_AUX,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def unique_ids_for_config_entry_id(
|
@callback
|
||||||
hass: HomeAssistant, config_entry_id: str
|
def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None:
|
||||||
) -> set[tuple[Platform | str, str]]:
|
"""Remove extra entities that are no longer part of the integration."""
|
||||||
"""Find all the unique ids for a config entry id."""
|
entity_registry = er.async_get(hass)
|
||||||
hass_isy_data = hass.data[DOMAIN][config_entry_id]
|
isy_data = hass.data[DOMAIN][entry_id]
|
||||||
isy = hass_isy_data[ISY_ROOT]
|
|
||||||
current_unique_ids: set[tuple[Platform | str, str]] = {
|
existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id)
|
||||||
(Platform.BUTTON, f"{isy.uuid}_query")
|
entities = {
|
||||||
|
(entity.domain, entity.unique_id): entity.entity_id
|
||||||
|
for entity in existing_entries
|
||||||
}
|
}
|
||||||
|
|
||||||
# Structure and prefixes here must match what's added in __init__ and helpers
|
extra_entities = set(entities.keys()).difference(isy_data.unique_ids)
|
||||||
for platform in NODE_PLATFORMS:
|
if not extra_entities:
|
||||||
for node in hass_isy_data[ISY_NODES][platform]:
|
return
|
||||||
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}"))
|
|
||||||
|
|
||||||
for node, control in hass_isy_data[ISY_NODES][SENSOR_AUX]:
|
for entity in extra_entities:
|
||||||
current_unique_ids.add(
|
if entity_registry.async_is_registered(entities[entity]):
|
||||||
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{control}")
|
entity_registry.async_remove(entities[entity])
|
||||||
)
|
|
||||||
current_unique_ids.add(
|
|
||||||
(Platform.SENSOR, f"{isy.uuid}_{node.address}_{PROP_COMMS_ERROR}")
|
|
||||||
)
|
|
||||||
|
|
||||||
for platform in PROGRAM_PLATFORMS:
|
_LOGGER.debug(
|
||||||
for _, node, _ in hass_isy_data[ISY_PROGRAMS][platform]:
|
("Cleaning up ISY entities: removed %s extra entities for config entry %s"),
|
||||||
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}"))
|
len(extra_entities),
|
||||||
|
entry_id,
|
||||||
for node, _ in hass_isy_data[ISY_VARIABLES][Platform.NUMBER]:
|
)
|
||||||
current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}"))
|
|
||||||
current_unique_ids.add((Platform.NUMBER, f"{isy.uuid}_{node.address}_init"))
|
|
||||||
for _, node in hass_isy_data[ISY_VARIABLES][Platform.SENSOR]:
|
|
||||||
current_unique_ids.add((Platform.SENSOR, f"{isy.uuid}_{node.address}"))
|
|
||||||
|
|
||||||
for platform in ROOT_NODE_PLATFORMS:
|
|
||||||
for node in hass_isy_data[ISY_ROOT_NODES][platform]:
|
|
||||||
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_query"))
|
|
||||||
if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON:
|
|
||||||
current_unique_ids.add((platform, f"{isy.uuid}_{node.address}_beep"))
|
|
||||||
|
|
||||||
for node in hass_isy_data[ISY_NET_RES]:
|
|
||||||
current_unique_ids.add(
|
|
||||||
(Platform.BUTTON, f"{isy.uuid}_{CONF_NETWORK}_{node.address}")
|
|
||||||
)
|
|
||||||
|
|
||||||
return current_unique_ids
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from unittest.mock import Mock
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from homeassistant.components.isy994.const import DOMAIN, ISY_ROOT, ISY_URL_POSTFIX
|
from homeassistant.components.isy994.const import DOMAIN, ISY_URL_POSTFIX
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -31,15 +31,16 @@ async def test_system_health(hass, aioclient_mock):
|
||||||
unique_id=MOCK_UUID,
|
unique_id=MOCK_UUID,
|
||||||
).add_to_hass(hass)
|
).add_to_hass(hass)
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
isy_data = Mock(
|
||||||
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
|
root=Mock(
|
||||||
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
|
connected=True,
|
||||||
connected=True,
|
websocket=Mock(
|
||||||
websocket=Mock(
|
last_heartbeat=MOCK_HEARTBEAT,
|
||||||
last_heartbeat=MOCK_HEARTBEAT,
|
status=MOCK_CONNECTED,
|
||||||
status=MOCK_CONNECTED,
|
),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
|
||||||
|
|
||||||
info = await get_system_health_info(hass, DOMAIN)
|
info = await get_system_health_info(hass, DOMAIN)
|
||||||
|
|
||||||
|
@ -67,15 +68,16 @@ async def test_system_health_failed_connect(hass, aioclient_mock):
|
||||||
unique_id=MOCK_UUID,
|
unique_id=MOCK_UUID,
|
||||||
).add_to_hass(hass)
|
).add_to_hass(hass)
|
||||||
|
|
||||||
hass.data[DOMAIN] = {}
|
isy_data = Mock(
|
||||||
hass.data[DOMAIN][MOCK_ENTRY_ID] = {}
|
root=Mock(
|
||||||
hass.data[DOMAIN][MOCK_ENTRY_ID][ISY_ROOT] = Mock(
|
connected=True,
|
||||||
connected=True,
|
websocket=Mock(
|
||||||
websocket=Mock(
|
last_heartbeat=MOCK_HEARTBEAT,
|
||||||
last_heartbeat=MOCK_HEARTBEAT,
|
status=MOCK_CONNECTED,
|
||||||
status=MOCK_CONNECTED,
|
),
|
||||||
),
|
)
|
||||||
)
|
)
|
||||||
|
hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data}
|
||||||
|
|
||||||
info = await get_system_health_info(hass, DOMAIN)
|
info = await get_system_health_info(hass, DOMAIN)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue