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

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 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
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,
]
18 changes: 10 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,11 @@ 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
await homee.set_value(self._attribute.node_id, self._attribute.id, value)

async def async_update(self) -> None:
"""Update entity from homee."""
homee = self._entry.runtime_data
Expand Down Expand Up @@ -129,13 +138,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"
Taraman17 marked this conversation as resolved.
Show resolved Hide resolved
},
"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."
}
}
}
128 changes: 128 additions & 0 deletions homeassistant/components/homee/switch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""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,
Taraman17 marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""Add the Homee platform for the switch component."""
Taraman17 marked this conversation as resolved.
Show resolved Hide resolved

devices: list[HomeeSwitch] = []
for node in config_entry.runtime_data.nodes:
devices.extend(
Taraman17 marked this conversation as resolved.
Show resolved Hide resolved
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
)
)
if devices:
async_add_devices(devices)


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
self._attr_is_on = bool(attribute.current_value)
Taraman17 marked this conversation as resolved.
Show resolved Hide resolved
if not ((attribute.type == AttributeType.ON_OFF) and (attribute.instance == 0)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we missing _attr_translation_key for when (attribute.type == AttributeType.ON_OFF) and (attribute.instance == 0) is true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but on purpose.
If this is the case, it is the only on/off switch of the device and thus should inherit the name of the device.

Taraman17 marked this conversation as resolved.
Show resolved Hide resolved
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