-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add Shelly support for REST sensors #40429
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
Changes from all commits
d15d223
413f4f3
5bfb099
c7cf0a0
6a17bc1
d3247cb
9095ca9
a8db66e
2f6f81f
07f0a37
2a041e1
d648cc0
b1e5358
7effad4
ee7000c
6a6e98a
bf6c44f
a691a0d
e3ad1ca
583dec5
3d4ab3e
01e1096
ed5e54e
e7fbe85
d3552da
564739c
f61273c
42d1e90
5882358
9b40132
f0d2669
6d20de6
d0b0c2c
4ae6423
2e9dd77
77f4325
5a42ba9
81f74d1
0a76498
d76d2d1
bcf87eb
73e2548
20489dc
42f822a
15a73af
4c32f86
28f3f86
bc024f4
ee19df9
e998dd5
17b9516
4624514
5c3dc69
e80aafd
1427dbf
73bd655
f6ef3d1
692bf0f
bf32003
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,60 @@ | |
|
|
||
| import aioshelly | ||
|
|
||
| from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT | ||
| from homeassistant.core import callback | ||
| from homeassistant.helpers import device_registry, entity | ||
|
|
||
| from . import ShellyDeviceWrapper | ||
| from .const import DATA_CONFIG_ENTRY, DOMAIN | ||
| from .utils import get_entity_name | ||
| from homeassistant.helpers import device_registry, entity, update_coordinator | ||
|
|
||
| from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper | ||
| from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST | ||
| from .utils import get_entity_name, get_rest_value_from_path | ||
|
|
||
|
|
||
| def temperature_unit(block_info: dict) -> str: | ||
| """Detect temperature unit.""" | ||
| if block_info[aioshelly.BLOCK_VALUE_UNIT] == "F": | ||
| return TEMP_FAHRENHEIT | ||
| return TEMP_CELSIUS | ||
|
|
||
|
|
||
| def shelly_naming(self, block, entity_type: str): | ||
| """Naming for switch and sensors.""" | ||
|
|
||
| entity_name = self.wrapper.name | ||
| if not block: | ||
| return f"{entity_name} {self.description.name}" | ||
|
|
||
| channels = 0 | ||
| mode = block.type + "s" | ||
| if "num_outputs" in self.wrapper.device.shelly: | ||
| channels = self.wrapper.device.shelly["num_outputs"] | ||
| if ( | ||
| self.wrapper.model in ["SHSW-21", "SHSW-25"] | ||
| and self.wrapper.device.settings["mode"] == "roller" | ||
| ): | ||
| channels = 1 | ||
| if block.type == "emeter" and "num_emeters" in self.wrapper.device.shelly: | ||
| channels = self.wrapper.device.shelly["num_emeters"] | ||
| if channels > 1 and block.type != "device": | ||
| # Shelly EM (SHEM) with firmware v1.8.1 doesn't have "name" key; will be fixed in next firmware release | ||
| if "name" in self.wrapper.device.settings[mode][int(block.channel)]: | ||
| entity_name = self.wrapper.device.settings[mode][int(block.channel)]["name"] | ||
| else: | ||
| entity_name = None | ||
| if not entity_name: | ||
| if self.wrapper.model == "SHEM-3": | ||
| base = ord("A") | ||
| else: | ||
| base = ord("1") | ||
| entity_name = f"{self.wrapper.name} channel {chr(int(block.channel)+base)}" | ||
|
|
||
| if entity_type == "switch": | ||
| return entity_name | ||
|
|
||
| if entity_type == "sensor": | ||
| return f"{entity_name} {self.description.name}" | ||
|
|
||
| raise ValueError | ||
|
|
||
|
|
||
| async def async_setup_entry_attribute_entities( | ||
|
|
@@ -18,7 +66,7 @@ async def async_setup_entry_attribute_entities( | |
| """Set up entities for block attributes.""" | ||
| wrapper: ShellyDeviceWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ | ||
| config_entry.entry_id | ||
| ] | ||
| ][COAP] | ||
| blocks = [] | ||
|
|
||
| for block in wrapper.device.blocks: | ||
|
|
@@ -44,6 +92,27 @@ async def async_setup_entry_attribute_entities( | |
| ) | ||
|
|
||
|
|
||
| async def async_setup_entry_rest( | ||
| hass, config_entry, async_add_entities, sensors, sensor_class | ||
| ): | ||
| """Set up entities for REST sensors.""" | ||
| wrapper: ShellyDeviceRestWrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ | ||
| config_entry.entry_id | ||
| ][REST] | ||
|
|
||
| entities = [] | ||
| for sensor_id in sensors: | ||
| _desc = sensors.get(sensor_id) | ||
|
|
||
| if not wrapper.device.settings.get("sleep_mode"): | ||
| entities.append(_desc) | ||
|
|
||
| if not entities: | ||
| return | ||
|
|
||
| async_add_entities([sensor_class(wrapper, description) for description in entities]) | ||
|
|
||
|
|
||
| @dataclass | ||
| class BlockAttributeDescription: | ||
| """Class to describe a sensor.""" | ||
|
|
@@ -60,6 +129,21 @@ class BlockAttributeDescription: | |
| ] = None | ||
|
|
||
|
|
||
| @dataclass | ||
| class RestAttributeDescription: | ||
| """Class to describe a REST sensor.""" | ||
|
|
||
| path: str | ||
| name: str | ||
| # Callable = lambda attr_info: unit | ||
| icon: Optional[str] = None | ||
| unit: Union[None, str, Callable[[dict], str]] = None | ||
| value: Callable[[Any], Any] = lambda val: val | ||
| device_class: Optional[str] = None | ||
| default_enabled: bool = True | ||
| attributes: Optional[dict] = None | ||
|
|
||
|
|
||
| class ShellyBlockEntity(entity.Entity): | ||
| """Helper class to represent a block.""" | ||
|
|
||
|
|
@@ -133,7 +217,7 @@ def __init__( | |
|
|
||
| self._unit = unit | ||
| self._unique_id = f"{super().unique_id}-{self.attribute}" | ||
| self._name = get_entity_name(wrapper, block, self.description.name) | ||
| self._name = shelly_naming(self, block, "sensor") | ||
|
|
||
| @property | ||
| def unique_id(self): | ||
|
|
@@ -187,3 +271,85 @@ def device_state_attributes(self): | |
| return None | ||
|
|
||
| return self.description.device_state_attributes(self.block) | ||
|
|
||
|
|
||
| class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity): | ||
| """Class to load info from REST.""" | ||
|
|
||
| def __init__( | ||
| self, wrapper: ShellyDeviceWrapper, description: RestAttributeDescription | ||
| ) -> None: | ||
| """Initialize sensor.""" | ||
| super().__init__(wrapper) | ||
| self.wrapper = wrapper | ||
| self.description = description | ||
|
|
||
| self._unit = self.description.unit | ||
| self._name = shelly_naming(self, None, "sensor") | ||
| self.path = self.description.path | ||
| self._attributes = self.description.attributes | ||
|
|
||
| @property | ||
| def name(self): | ||
| """Name of sensor.""" | ||
| return self._name | ||
|
|
||
| @property | ||
| def device_info(self): | ||
| """Device info.""" | ||
| return { | ||
| "connections": {(device_registry.CONNECTION_NETWORK_MAC, self.wrapper.mac)} | ||
| } | ||
|
|
||
| @property | ||
| def entity_registry_enabled_default(self) -> bool: | ||
| """Return if it should be enabled by default.""" | ||
| return self.description.default_enabled | ||
|
|
||
| @property | ||
| def available(self): | ||
| """Available.""" | ||
| return self.wrapper.last_update_success | ||
|
|
||
| @property | ||
| def attribute_value(self): | ||
| """Attribute.""" | ||
| return get_rest_value_from_path( | ||
| self.wrapper.device.status, self.description.device_class, self.path | ||
| ) | ||
|
|
||
| @property | ||
| def unit_of_measurement(self): | ||
| """Return unit of sensor.""" | ||
| return self.description.unit | ||
|
|
||
| @property | ||
| def device_class(self): | ||
| """Device class of sensor.""" | ||
| return self.description.device_class | ||
|
|
||
| @property | ||
| def icon(self): | ||
| """Icon of sensor.""" | ||
| return self.description.icon | ||
|
|
||
| @property | ||
| def unique_id(self): | ||
| """Return unique ID of entity.""" | ||
| return f"{self.wrapper.mac}-{self.description.path}" | ||
|
|
||
| @property | ||
| def device_state_attributes(self): | ||
| """Return the state attributes.""" | ||
|
|
||
| if self._attributes is None: | ||
| return None | ||
|
|
||
| _description = self._attributes.get("description") | ||
| _attribute_value = get_rest_value_from_path( | ||
|
Comment on lines
+348
to
+349
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why prefix with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I though "_" was a code convention for non public func/values. Am I wrong ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As class methods, yes. When used for variables it means it's not going to be used. |
||
| self.wrapper.device.status, | ||
| self.description.device_class, | ||
| self._attributes.get("path"), | ||
| ) | ||
|
|
||
| return {_description: _attribute_value} | ||
|
chemelli74 marked this conversation as resolved.
|
||
Uh oh!
There was an error while loading. Please reload this page.