Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions homeassistant/components/isy994/button.py
Original file line number Diff line number Diff line change
@@ -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())
9 changes: 9 additions & 0 deletions homeassistant/components/isy994/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@

PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.COVER,
Platform.FAN,
Expand Down Expand Up @@ -189,6 +190,14 @@
], # 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)
Expand Down
40 changes: 19 additions & 21 deletions homeassistant/components/isy994/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@
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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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))

Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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(
Expand Down
64 changes: 63 additions & 1 deletion homeassistant/components/isy994/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
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
Expand Down Expand Up @@ -181,7 +183,7 @@ async def async_system_query_service_handler(service: ServiceCall) -> None:
"""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"]:
Expand All @@ -195,11 +197,31 @@ async def async_system_query_service_handler(service: ServiceCall) -> None:
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."""
Expand Down Expand Up @@ -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"
Comment thread
bdraco marked this conversation as resolved.

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,
)
4 changes: 2 additions & 2 deletions homeassistant/components/isy994/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions homeassistant/components/isy994/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}`."
}
}
}
}
}
}
15 changes: 14 additions & 1 deletion homeassistant/components/isy994/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}