Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Homee switch platform #137457

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
2 changes: 1 addition & 1 deletion homeassistant/components/homee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.COVER, Platform.SENSOR]
PLATFORMS = [Platform.COVER, Platform.SENSOR, Platform.SWITCH]

type HomeeConfigEntry = ConfigEntry[Homee]

Expand Down
32 changes: 32 additions & 0 deletions homeassistant/components/homee/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Constants for the homee integration."""

from pyHomee.const import NodeProfile

from homeassistant.const import (
DEGREE,
LIGHT_LUX,
Expand Down Expand Up @@ -62,3 +64,33 @@
2.0: "tilted",
}
WINDOW_MAP_REVERSED = {0.0: "open", 1.0: "closed", 2.0: "tilted"}

# Profile Groups
CLIMATE_PROFILES = [
NodeProfile.COSI_THERM_CHANNEL,
NodeProfile.HEATING_SYSTEM,
NodeProfile.RADIATOR_THERMOSTAT,
NodeProfile.ROOM_THERMOSTAT,
NodeProfile.ROOM_THERMOSTAT_WITH_HUMIDITY_SENSOR,
NodeProfile.THERMOSTAT_WITH_HEATING_AND_COOLING,
NodeProfile.WIFI_RADIATOR_THERMOSTAT,
NodeProfile.WIFI_ROOM_THERMOSTAT,
]
LIGHT_PROFILES = [
NodeProfile.DIMMABLE_COLOR_LIGHT,
NodeProfile.DIMMABLE_COLOR_METERING_PLUG,
NodeProfile.DIMMABLE_COLOR_TEMPERATURE_LIGHT,
NodeProfile.DIMMABLE_EXTENDED_COLOR_LIGHT,
NodeProfile.DIMMABLE_LIGHT,
NodeProfile.DIMMABLE_LIGHT_WITH_BRIGHTNESS_SENSOR,
NodeProfile.DIMMABLE_LIGHT_WITH_BRIGHTNESS_AND_PRESENCE_SENSOR,
NodeProfile.DIMMABLE_LIGHT_WITH_PRESENCE_SENSOR,
NodeProfile.DIMMABLE_METERING_SWITCH,
NodeProfile.DIMMABLE_METERING_PLUG,
NodeProfile.DIMMABLE_PLUG,
NodeProfile.DIMMABLE_RGBWLIGHT,
NodeProfile.DIMMABLE_SWITCH,
NodeProfile.WIFI_DIMMABLE_RGBWLIGHT,
NodeProfile.WIFI_DIMMABLE_LIGHT,
NodeProfile.WIFI_ON_OFF_DIMMABLE_METERING_SWITCH,
]
24 changes: 16 additions & 8 deletions homeassistant/components/homee/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ def __init__(self, attribute: HomeeAttribute, entry: HomeeConfigEntry) -> None:
f"{entry.runtime_data.settings.uid}-{attribute.node_id}-{attribute.id}"
)
self._entry = entry
node = entry.runtime_data.get_node_by_id(attribute.node_id)
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
}
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)

self._host_connected = entry.runtime_data.connected
Expand All @@ -50,6 +54,17 @@ def available(self) -> bool:
"""Return the availability of the underlying node."""
return (self._attribute.state == AttributeState.NORMAL) and self._host_connected

async def async_set_value(self, value: float) -> None:
"""Set an attribute value on the homee node."""
homee = self._entry.runtime_data
try:
await homee.set_value(self._attribute.node_id, self._attribute.id, value)
except ConnectionClosed as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_closed",
) from exception

async def async_update(self) -> None:
"""Update entity from homee."""
homee = self._entry.runtime_data
Expand Down Expand Up @@ -129,13 +144,6 @@ def _get_software_version(self) -> str | None:

return None

def has_attribute(self, attribute_type: AttributeType) -> bool:
"""Check if an attribute of the given type exists."""
if self._node.attribute_map is None:
return False

return attribute_type in self._node.attribute_map

async def async_set_value(self, attribute: HomeeAttribute, value: float) -> None:
"""Set an attribute value on the homee node."""
homee = self._entry.runtime_data
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/homee/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
"window_position": {
"default": "mdi:window-closed"
}
},
"switch": {
"watchdog_on_off": {
"default": "mdi:dog"
},
"manual_operation": {
"default": "mdi:hand-back-left"
}
}
}
}
16 changes: 15 additions & 1 deletion homeassistant/components/homee/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,25 @@
"tilted": "Tilted"
}
}
},
"switch": {
"external_binary_input": {
"name": "Child lock"
},
"manual_operation": {
"name": "Manual operation"
},
"on_off_instance": {
"name": "Switch {instance}"
},
"watchdog": {
"name": "Watchdog"
}
}
},
"exceptions": {
"connection_closed": {
"message": "Could not connect to Homee while setting attribute"
"message": "Could not connect to Homee while setting attribute."
}
}
}
124 changes: 124 additions & 0 deletions homeassistant/components/homee/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""The homee switch platform."""

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeAttribute

from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import HomeeConfigEntry
from .const import CLIMATE_PROFILES, LIGHT_PROFILES
from .entity import HomeeEntity


def get_device_class(
attribute: HomeeAttribute, config_entry: HomeeConfigEntry
) -> SwitchDeviceClass:
"""Check device class of Switch according to node profile."""
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
if node.profile in [
NodeProfile.ON_OFF_PLUG,
NodeProfile.METERING_PLUG,
NodeProfile.DOUBLE_ON_OFF_PLUG,
NodeProfile.IMPULSE_PLUG,
]:
return SwitchDeviceClass.OUTLET

return SwitchDeviceClass.SWITCH


@dataclass(frozen=True, kw_only=True)
class HomeeSwitchEntityDescription(SwitchEntityDescription):
"""A class that describes Homee switch entity."""

device_class_fn: Callable[[HomeeAttribute, HomeeConfigEntry], SwitchDeviceClass] = (
lambda attribute, entry: SwitchDeviceClass.SWITCH
)


SWITCH_DESCRIPTIONS: dict[AttributeType, HomeeSwitchEntityDescription] = {
AttributeType.EXTERNAL_BINARY_INPUT: HomeeSwitchEntityDescription(
key="external_binary_input", entity_category=EntityCategory.CONFIG
),
AttributeType.MANUAL_OPERATION: HomeeSwitchEntityDescription(
key="manual_operation"
),
AttributeType.ON_OFF: HomeeSwitchEntityDescription(
key="on_off", device_class_fn=get_device_class, name=None
),
AttributeType.WATCHDOG_ON_OFF: HomeeSwitchEntityDescription(
key="watchdog", entity_category=EntityCategory.CONFIG
),
}


async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_devices: AddEntitiesCallback,
) -> None:
"""Add the Homee platform for the switch component."""

for node in config_entry.runtime_data.nodes:
async_add_devices(
HomeeSwitch(attribute, config_entry, SWITCH_DESCRIPTIONS[attribute.type])
for attribute in node.attributes
if (attribute.type in SWITCH_DESCRIPTIONS and attribute.editable)
and not (
attribute.type == AttributeType.ON_OFF
and node.profile in LIGHT_PROFILES
)
and not (
attribute.type == AttributeType.MANUAL_OPERATION
and node.profile in CLIMATE_PROFILES
)
)


class HomeeSwitch(HomeeEntity, SwitchEntity):
"""Representation of a Homee switch."""

entity_description: HomeeSwitchEntityDescription

def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeSwitchEntityDescription,
) -> None:
"""Initialize a Homee switch entity."""
super().__init__(attribute, entry)
self.entity_description = description
if not ((attribute.type == AttributeType.ON_OFF) and (attribute.instance == 0)):
self._attr_translation_key = description.key
if attribute.instance > 0:
self._attr_translation_key = f"{description.key}_instance"
self._attr_translation_placeholders = {"instance": str(attribute.instance)}

@property
def is_on(self) -> bool:
"""Return True if entity is on."""
return bool(self._attribute.current_value)

@property
def device_class(self) -> SwitchDeviceClass:
"""Return the device class of the switch."""
return self.entity_description.device_class_fn(self._attribute, self._entry)

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.async_set_value(1)

async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.async_set_value(0)
127 changes: 127 additions & 0 deletions tests/components/homee/fixtures/switches.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"id": 1,
"name": "Test Switch",
"profile": 10,
"image": "nodeicon_dimmablebulb",
"favorite": 0,
"order": 27,
"protocol": 3,
"routing": 0,
"state": 1,
"state_changed": 1736188706,
"added": 1610308228,
"history": 1,
"cube_type": 3,
"note": "All known switches",
"services": 7,
"phonetic_name": "",
"owner": 2,
"security": 0,
"attributes": [
{
"id": 1,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 1,
"current_value": 0.0,
"target_value": 0.0,
"last_value": 0.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 309,
"state": 1,
"last_changed": 1677692134,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
},
{
"id": 2,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 1,
"current_value": 0.0,
"target_value": 0.0,
"last_value": 0.0,
"unit": "",
"step_value": 1.0,
"editable": 1,
"type": 91,
"state": 1,
"last_changed": 1711796633,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
},
{
"id": 3,
"node_id": 1,
"instance": 1,
"minimum": 0,
"maximum": 1,
"current_value": 0.0,
"target_value": 0.0,
"last_value": 0.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 1,
"state": 1,
"last_changed": 1736743294,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
},
{
"id": 4,
"node_id": 1,
"instance": 2,
"minimum": 0,
"maximum": 1,
"current_value": 1.0,
"target_value": 1.0,
"last_value": 1.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 1,
"state": 1,
"last_changed": 1736743294,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 1,
"data": "",
"name": ""
},
{
"id": 5,
"node_id": 1,
"instance": 0,
"minimum": 0,
"maximum": 1,
"current_value": 1.0,
"target_value": 1.0,
"last_value": 0.0,
"unit": "n/a",
"step_value": 1.0,
"editable": 1,
"type": 385,
"state": 1,
"last_changed": 1735663169,
"changed_by": 1,
"changed_by_id": 0,
"based_on": 0,
"data": "",
"name": ""
}
]
}
Loading