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
14 changes: 11 additions & 3 deletions homeassistant/components/isy994/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
HVACMode,
)
from homeassistant.components.lock import LockState
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
Expand Down Expand Up @@ -431,7 +432,7 @@
"127": UnitOfPressure.MMHG,
"128": "J",
"129": "BMI", # Body Mass Index
"130": f"{UnitOfVolume.LITERS}/{UnitOfTime.HOURS}",
"130": UnitOfVolumeFlowRate.LITERS_PER_HOUR,
"131": SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
"132": "bpm", # Breaths per minute
"133": UnitOfFrequency.KILOHERTZ,
Expand All @@ -444,8 +445,8 @@
"140": f"{UnitOfMass.MILLIGRAMS}/{UnitOfVolume.LITERS}",
"141": "N", # Netwon
"142": f"{UnitOfVolume.GALLONS}/{UnitOfTime.SECONDS}",
"143": "gpm", # Gallon per Minute
"144": "gph", # Gallon per Hour
"143": UnitOfVolumeFlowRate.GALLONS_PER_MINUTE,
"144": UnitOfVolumeFlowRate.GALLONS_PER_HOUR,
}

UOM_TO_STATES = {
Expand Down Expand Up @@ -653,6 +654,13 @@

HA_FAN_TO_ISY = {FAN_ON: "on", FAN_AUTO: "auto"}

TOTAL_INCREASING_DEVICE_CLASSES = {
SensorDeviceClass.ENERGY,
SensorDeviceClass.WATER,
SensorDeviceClass.GAS,
SensorDeviceClass.PRECIPITATION,
}

BINARY_SENSOR_DEVICE_TYPES_ISY = {
BinarySensorDeviceClass.MOISTURE: ["16.8.", "16.13.", "16.14."],
BinarySensorDeviceClass.OPENING: [
Expand Down
117 changes: 111 additions & 6 deletions homeassistant/components/isy994/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@
SensorEntity,
SensorStateClass,
)
from homeassistant.const import EntityCategory, Platform, UnitOfTemperature
from homeassistant.const import (
EntityCategory,
Platform,
UnitOfTemperature,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import (
_LOGGER,
TOTAL_INCREASING_DEVICE_CLASSES,
UOM_DOUBLE_TEMP,
UOM_FRIENDLY_NAME,
UOM_INDEX,
Expand Down Expand Up @@ -73,6 +79,7 @@
"DISTANC": SensorDeviceClass.DISTANCE,
"ETO": SensorDeviceClass.PRECIPITATION_INTENSITY, # codespell:ignore eto
"FATM": SensorDeviceClass.WEIGHT,
"FLOW": SensorDeviceClass.VOLUME_FLOW_RATE,
"FREQ": SensorDeviceClass.FREQUENCY,
"MUSCLEM": SensorDeviceClass.WEIGHT,
"PF": SensorDeviceClass.POWER_FACTOR,
Expand All @@ -95,16 +102,78 @@
"WEIGHT": SensorDeviceClass.WEIGHT,
"WINDCH": SensorDeviceClass.TEMPERATURE,
}
ISY_CONTROL_TO_STATE_CLASS = dict.fromkeys(
ISY_CONTROL_TO_DEVICE_CLASS, SensorStateClass.MEASUREMENT
)
UOM_TO_DEVICE_CLASS = {
"1": SensorDeviceClass.CURRENT,
"3": SensorDeviceClass.POWER,
"4": SensorDeviceClass.TEMPERATURE,
"7": SensorDeviceClass.VOLUME_FLOW_RATE,
"12": SensorDeviceClass.SOUND_PRESSURE,
"13": SensorDeviceClass.SOUND_PRESSURE,
"17": SensorDeviceClass.TEMPERATURE,
"23": SensorDeviceClass.ATMOSPHERIC_PRESSURE,
"24": SensorDeviceClass.PRECIPITATION_INTENSITY,
"26": SensorDeviceClass.TEMPERATURE,
"28": SensorDeviceClass.WEIGHT,
"29": SensorDeviceClass.VOLTAGE,
"30": SensorDeviceClass.POWER,
"31": SensorDeviceClass.PRESSURE,
"32": SensorDeviceClass.SPEED,
"33": SensorDeviceClass.ENERGY,
"35": SensorDeviceClass.WATER,
"39": SensorDeviceClass.VOLUME_FLOW_RATE,
"40": SensorDeviceClass.SPEED,
"41": SensorDeviceClass.CURRENT,
"43": SensorDeviceClass.VOLTAGE,
"46": SensorDeviceClass.PRECIPITATION_INTENSITY,
"48": SensorDeviceClass.SPEED,
"49": SensorDeviceClass.SPEED,
"52": SensorDeviceClass.WEIGHT,
"54": SensorDeviceClass.CO2,
"69": SensorDeviceClass.WATER,
"72": SensorDeviceClass.VOLTAGE,
"73": SensorDeviceClass.POWER,
"74": SensorDeviceClass.IRRADIANCE,
"82": SensorDeviceClass.DISTANCE,
"83": SensorDeviceClass.DISTANCE,
"90": SensorDeviceClass.FREQUENCY,
"105": SensorDeviceClass.DISTANCE,
"106": SensorDeviceClass.PRECIPITATION_INTENSITY,
"116": SensorDeviceClass.DISTANCE,
"117": SensorDeviceClass.PRESSURE,
"118": SensorDeviceClass.ATMOSPHERIC_PRESSURE,
"119": SensorDeviceClass.ENERGY,
"120": SensorDeviceClass.PRECIPITATION_INTENSITY,
"127": SensorDeviceClass.PRESSURE,
"130": SensorDeviceClass.VOLUME_FLOW_RATE,
"131": SensorDeviceClass.SIGNAL_STRENGTH,
"133": SensorDeviceClass.FREQUENCY,
"138": SensorDeviceClass.PRESSURE,
"142": SensorDeviceClass.VOLUME_FLOW_RATE,
"143": SensorDeviceClass.VOLUME_FLOW_RATE,
"144": SensorDeviceClass.VOLUME_FLOW_RATE,
}
ISY_CONTROL_TO_ENTITY_CATEGORY = {
PROP_RAMP_RATE: EntityCategory.DIAGNOSTIC,
PROP_ON_LEVEL: EntityCategory.DIAGNOSTIC,
PROP_COMMS_ERROR: EntityCategory.DIAGNOSTIC,
}


def _check_volume_flow_rate_uom(
device_class: SensorDeviceClass | None,
uom: str | list[str] | None,
) -> SensorDeviceClass | None:
"""Check if the volume flow rate unit is supported."""
if device_class != SensorDeviceClass.VOLUME_FLOW_RATE:
return device_class
# Backwards compatibility for ISYv4 firmware which may return a list.
if isinstance(uom, list):
uom = uom[0] if uom else None
if uom is not None and UOM_FRIENDLY_NAME.get(uom) in UnitOfVolumeFlowRate:
return device_class
return None


async def async_setup_entry(
hass: HomeAssistant,
entry: IsyConfigEntry,
Expand Down Expand Up @@ -141,6 +210,26 @@ async def async_setup_entry(
class ISYSensorEntity(ISYNodeEntity, SensorEntity):
"""Representation of an ISY sensor device."""

def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None:
"""Initialize the ISY sensor."""
super().__init__(node, device_info=device_info)
uom = self._node.uom
if isinstance(uom, list):
uom = uom[0]

# Determine device class
self._attr_device_class = _check_volume_flow_rate_uom(
UOM_TO_DEVICE_CLASS.get(uom), uom
)

# Determine state class
if self._attr_device_class in TOTAL_INCREASING_DEVICE_CLASSES:
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
elif self._attr_device_class is not None:
self._attr_state_class = SensorStateClass.MEASUREMENT
else:
self._attr_state_class = None

@property
def target(self) -> Node | NodeProperty | None:
"""Return target for the sensor."""
Expand Down Expand Up @@ -240,8 +329,24 @@ def __init__(
self._control = control
self._attr_entity_registry_enabled_default = enabled_default
self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control)
self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control)
self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control)

uom = None
if control in self._node.aux_properties:
uom = self._node.aux_properties[control].uom

# Determine device class
self._attr_device_class = _check_volume_flow_rate_uom(
ISY_CONTROL_TO_DEVICE_CLASS.get(control), uom
)

# Determine state class
if self._attr_device_class in TOTAL_INCREASING_DEVICE_CLASSES:
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
elif self._attr_device_class is not None:
self._attr_state_class = SensorStateClass.MEASUREMENT
else:
self._attr_state_class = None

self._attr_unique_id = unique_id
self._change_handler: EventListener = None
self._availability_handler: EventListener = None
Expand Down
97 changes: 97 additions & 0 deletions tests/components/isy994/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""Fixtures for the ISY994 tests."""

from unittest.mock import AsyncMock, MagicMock, patch

from pyisy.nodes import Node
import pytest

from homeassistant.components.isy994.const import DOMAIN
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME

from tests.common import MockConfigEntry

MOCK_UUID = "00:00:00:00:00:00"


@pytest.fixture
def mock_config_entry():
"""Return a mock config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: "http://1.1.1.1",
CONF_USERNAME: "test-username",
CONF_PASSWORD: "test-password",
},
unique_id=MOCK_UUID,
)


@pytest.fixture
def mock_isy():
"""Return a mock ISY object."""
mock = MagicMock()
mock.nodes = MagicMock()
mock.nodes.__iter__.return_value = []
mock.nodes.status_events = MagicMock()
mock.programs = MagicMock()
mock.programs.get_by_name.return_value = None
mock.variables = MagicMock()
mock.variables.children = []
mock.networking = MagicMock()
mock.networking.nobjs = []
mock.clock = MagicMock()
mock.websocket = MagicMock()
mock.conf = {
"name": "Skynet ISY",
"model": "IoX",
"firmware": "6.0.4",
"Networking Module": True,
"Portal": True,
}
mock.uuid = MOCK_UUID
mock.conn.url = "http://1.1.1.1:80"
mock.initialize = AsyncMock()
return mock


@pytest.fixture
def mock_node():
"""Return a mock ISY node."""

def _mock_node(isy, address, name, node_def_id, node_type=None):
node = MagicMock(spec=Node)
node.isy = isy
node.address = address
node.name = name
node.node_def_id = node_def_id
node.type = node_type
node.status = 0
node.uom = None
node.prec = 0
node.protocol = "insteon"
node.folder = None
node.parent_node = None
node.primary_node = address
node.aux_properties = {}
node.status_events = MagicMock()
node.status_events.subscribe.return_value = MagicMock()
node.control_events = MagicMock()
node.control_events.subscribe.return_value = MagicMock()
node.is_backlight_supported = False
return node

return _mock_node


@pytest.fixture(autouse=True)
def mock_isy_init(mock_isy):
"""Mock pyisy.ISY initialization."""
with (
patch("homeassistant.components.isy994.ISY", return_value=mock_isy),
patch(
"homeassistant.components.isy994.config_flow.Connection.test_connection",
return_value="<configuration></configuration>",
),
):
yield mock_isy
Loading
Loading