mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Fix Shelly uptime sensor (#43651)
Fix sensor to include time zone Report new value only if delta > 5 seconds Modify REST sensors class to use callable attributes
This commit is contained in:
parent
5e3f4954f7
commit
2498340e1f
@ -75,19 +75,19 @@ SENSORS = {
|
|||||||
REST_SENSORS = {
|
REST_SENSORS = {
|
||||||
"cloud": RestAttributeDescription(
|
"cloud": RestAttributeDescription(
|
||||||
name="Cloud",
|
name="Cloud",
|
||||||
|
value=lambda status, _: status["cloud"]["connected"],
|
||||||
device_class=DEVICE_CLASS_CONNECTIVITY,
|
device_class=DEVICE_CLASS_CONNECTIVITY,
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
path="cloud/connected",
|
|
||||||
),
|
),
|
||||||
"fwupdate": RestAttributeDescription(
|
"fwupdate": RestAttributeDescription(
|
||||||
name="Firmware update",
|
name="Firmware update",
|
||||||
icon="mdi:update",
|
icon="mdi:update",
|
||||||
|
value=lambda status, _: status["update"]["has_update"],
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
path="update/has_update",
|
device_state_attributes=lambda status: {
|
||||||
attributes=[
|
"latest_stable_version": status["update"]["new_version"],
|
||||||
{"description": "latest_stable_version", "path": "update/new_version"},
|
"installed_version": status["update"]["old_version"],
|
||||||
{"description": "installed_version", "path": "update/old_version"},
|
},
|
||||||
],
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry, entity, update_coordinator
|
|||||||
|
|
||||||
from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
|
from . import ShellyDeviceRestWrapper, ShellyDeviceWrapper
|
||||||
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
|
from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN, REST
|
||||||
from .utils import async_remove_shelly_entity, get_entity_name, get_rest_value_from_path
|
from .utils import async_remove_shelly_entity, get_entity_name
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry_attribute_entities(
|
async def async_setup_entry_attribute_entities(
|
||||||
@ -64,15 +64,20 @@ async def async_setup_entry_rest(
|
|||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for sensor_id in sensors:
|
for sensor_id in sensors:
|
||||||
_desc = sensors.get(sensor_id)
|
description = sensors.get(sensor_id)
|
||||||
|
|
||||||
if not wrapper.device.settings.get("sleep_mode"):
|
if not wrapper.device.settings.get("sleep_mode"):
|
||||||
entities.append(_desc)
|
entities.append((sensor_id, description))
|
||||||
|
|
||||||
if not entities:
|
if not entities:
|
||||||
return
|
return
|
||||||
|
|
||||||
async_add_entities([sensor_class(wrapper, description) for description in entities])
|
async_add_entities(
|
||||||
|
[
|
||||||
|
sensor_class(wrapper, sensor_id, description)
|
||||||
|
for sensor_id, description in entities
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -98,15 +103,13 @@ class BlockAttributeDescription:
|
|||||||
class RestAttributeDescription:
|
class RestAttributeDescription:
|
||||||
"""Class to describe a REST sensor."""
|
"""Class to describe a REST sensor."""
|
||||||
|
|
||||||
path: str
|
|
||||||
name: str
|
name: str
|
||||||
# Callable = lambda attr_info: unit
|
|
||||||
icon: Optional[str] = None
|
icon: Optional[str] = None
|
||||||
unit: Union[None, str, Callable[[dict], str]] = None
|
unit: Optional[str] = None
|
||||||
value: Callable[[Any], Any] = lambda val: val
|
value: Callable[[dict, Any], Any] = None
|
||||||
device_class: Optional[str] = None
|
device_class: Optional[str] = None
|
||||||
default_enabled: bool = True
|
default_enabled: bool = True
|
||||||
attributes: Optional[dict] = None
|
device_state_attributes: Optional[Callable[[dict], Optional[dict]]] = None
|
||||||
|
|
||||||
|
|
||||||
class ShellyBlockEntity(entity.Entity):
|
class ShellyBlockEntity(entity.Entity):
|
||||||
@ -247,17 +250,18 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
|
|||||||
"""Class to load info from REST."""
|
"""Class to load info from REST."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, wrapper: ShellyDeviceWrapper, description: RestAttributeDescription
|
self,
|
||||||
|
wrapper: ShellyDeviceWrapper,
|
||||||
|
attribute: str,
|
||||||
|
description: RestAttributeDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize sensor."""
|
"""Initialize sensor."""
|
||||||
super().__init__(wrapper)
|
super().__init__(wrapper)
|
||||||
self.wrapper = wrapper
|
self.wrapper = wrapper
|
||||||
|
self.attribute = attribute
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
self._unit = self.description.unit
|
|
||||||
self._name = get_entity_name(wrapper.device, None, self.description.name)
|
self._name = get_entity_name(wrapper.device, None, self.description.name)
|
||||||
self.path = self.description.path
|
self._last_value = None
|
||||||
self._attributes = self.description.attributes
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -283,10 +287,11 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def attribute_value(self):
|
def attribute_value(self):
|
||||||
"""Attribute."""
|
"""Value of sensor."""
|
||||||
return get_rest_value_from_path(
|
self._last_value = self.description.value(
|
||||||
self.wrapper.device.status, self.description.device_class, self.path
|
self.wrapper.device.status, self._last_value
|
||||||
)
|
)
|
||||||
|
return self._last_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def unit_of_measurement(self):
|
||||||
@ -306,23 +311,12 @@ class ShellyRestAttributeEntity(update_coordinator.CoordinatorEntity):
|
|||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return unique ID of entity."""
|
"""Return unique ID of entity."""
|
||||||
return f"{self.wrapper.mac}-{self.description.path}"
|
return f"{self.wrapper.mac}-{self.attribute}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self) -> dict:
|
def device_state_attributes(self) -> dict:
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
|
if self.description.device_state_attributes is None:
|
||||||
if self._attributes is None:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
attributes = dict()
|
return self.description.device_state_attributes(self.wrapper.device.status)
|
||||||
for attrib in self._attributes:
|
|
||||||
description = attrib.get("description")
|
|
||||||
attribute_value = get_rest_value_from_path(
|
|
||||||
self.wrapper.device.status,
|
|
||||||
self.description.device_class,
|
|
||||||
attrib.get("path"),
|
|
||||||
)
|
|
||||||
attributes[description] = attribute_value
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
@ -21,7 +21,7 @@ from .entity import (
|
|||||||
async_setup_entry_attribute_entities,
|
async_setup_entry_attribute_entities,
|
||||||
async_setup_entry_rest,
|
async_setup_entry_rest,
|
||||||
)
|
)
|
||||||
from .utils import temperature_unit
|
from .utils import get_device_uptime, temperature_unit
|
||||||
|
|
||||||
SENSORS = {
|
SENSORS = {
|
||||||
("device", "battery"): BlockAttributeDescription(
|
("device", "battery"): BlockAttributeDescription(
|
||||||
@ -170,15 +170,15 @@ REST_SENSORS = {
|
|||||||
"rssi": RestAttributeDescription(
|
"rssi": RestAttributeDescription(
|
||||||
name="RSSI",
|
name="RSSI",
|
||||||
unit=SIGNAL_STRENGTH_DECIBELS,
|
unit=SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
value=lambda status, _: status["wifi_sta"]["rssi"],
|
||||||
device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH,
|
device_class=sensor.DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
path="wifi_sta/rssi",
|
|
||||||
),
|
),
|
||||||
"uptime": RestAttributeDescription(
|
"uptime": RestAttributeDescription(
|
||||||
name="Uptime",
|
name="Uptime",
|
||||||
|
value=get_device_uptime,
|
||||||
device_class=sensor.DEVICE_CLASS_TIMESTAMP,
|
device_class=sensor.DEVICE_CLASS_TIMESTAMP,
|
||||||
default_enabled=False,
|
default_enabled=False,
|
||||||
path="uptime",
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
"""Shelly helpers functions."""
|
"""Shelly helpers functions."""
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import aioshelly
|
import aioshelly
|
||||||
|
|
||||||
from homeassistant.components.sensor import DEVICE_CLASS_TIMESTAMP
|
|
||||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||||
|
from homeassistant.util.dt import parse_datetime, utcnow
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
@ -81,23 +81,6 @@ def get_entity_name(
|
|||||||
return entity_name
|
return entity_name
|
||||||
|
|
||||||
|
|
||||||
def get_rest_value_from_path(status, device_class, path: str):
|
|
||||||
"""Parser for REST path from device status."""
|
|
||||||
|
|
||||||
if "/" not in path:
|
|
||||||
attribute_value = status[path]
|
|
||||||
else:
|
|
||||||
attribute_value = status[path.split("/")[0]][path.split("/")[1]]
|
|
||||||
if device_class == DEVICE_CLASS_TIMESTAMP:
|
|
||||||
last_boot = datetime.utcnow() - timedelta(seconds=attribute_value)
|
|
||||||
attribute_value = last_boot.replace(microsecond=0).isoformat()
|
|
||||||
|
|
||||||
if "new_version" in path:
|
|
||||||
attribute_value = attribute_value.split("/")[1].split("@")[0]
|
|
||||||
|
|
||||||
return attribute_value
|
|
||||||
|
|
||||||
|
|
||||||
def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
|
def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
|
||||||
"""Return true if input button settings is set to a momentary type."""
|
"""Return true if input button settings is set to a momentary type."""
|
||||||
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
|
button = settings.get("relays") or settings.get("lights") or settings.get("inputs")
|
||||||
@ -112,3 +95,16 @@ def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool:
|
|||||||
button_type = button[channel].get("btn_type")
|
button_type = button[channel].get("btn_type")
|
||||||
|
|
||||||
return button_type in ["momentary", "momentary_on_release"]
|
return button_type in ["momentary", "momentary_on_release"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_uptime(status: dict, last_uptime: str) -> str:
|
||||||
|
"""Return device uptime string, tolerate up to 5 seconds deviation."""
|
||||||
|
uptime = utcnow() - timedelta(seconds=status["uptime"])
|
||||||
|
|
||||||
|
if not last_uptime:
|
||||||
|
return uptime.replace(microsecond=0).isoformat()
|
||||||
|
|
||||||
|
if abs((uptime - parse_datetime(last_uptime)).total_seconds()) > 5:
|
||||||
|
return uptime.replace(microsecond=0).isoformat()
|
||||||
|
|
||||||
|
return last_uptime
|
||||||
|
Loading…
x
Reference in New Issue
Block a user