Skip to content

Commit

Permalink
Add diagnostics data to SHC (cpu, ram, disk) #62
Browse files Browse the repository at this point in the history
  • Loading branch information
planbnet committed Feb 15, 2024
1 parent 488516a commit df94315
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 43 deletions.
3 changes: 2 additions & 1 deletion custom_components/livisi/entity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Code to handle a Livisi switches."""
"""Code to handle a Livisi entity."""

from __future__ import annotations


Expand Down
77 changes: 47 additions & 30 deletions custom_components/livisi/livisi_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
V2_NAME,
LOGGER,
REQUEST_TIMEOUT,
SHC_ID,
WEBSERVICE_PORT,
)

Expand Down Expand Up @@ -206,10 +207,18 @@ async def async_get_devices(
# needed so subsequent parallel requests don't fail
messages = await self.async_send_authorized_request("get", path="message")

devices, capabilities, rooms = await asyncio.gather(
(
low_battery_devices,
update_available_devices,
unreachable_devices,
updated_devices,
) = self.parse_messages(messages)

devices, capabilities, rooms, shc_state = await asyncio.gather(
self.async_send_authorized_request("get", path="device"),
self.async_send_authorized_request("get", path="capability"),
self.async_send_authorized_request("get", path="location"),
self.async_send_authorized_request("get", path=f"device/{SHC_ID}/state"),
return_exceptions=True,
)

Expand All @@ -230,9 +239,6 @@ async def async_get_devices(
if "id" in room:
roomid = room["id"]
room_map[roomid] = room.get("config", {}).get("name")
else:
LOGGER.warning("Invalid room: %s", room)
LOGGER.warning(rooms)

for capability in capabilities:
if "device" in capability:
Expand All @@ -247,10 +253,36 @@ async def async_get_devices(
capability_map[device_id][cap_type] = capability["id"]
if "config" in capability:
capability_config[device_id][cap_type] = capability["config"]
else:
LOGGER.warning("Invalid capability: %s", capability)
LOGGER.warning(capabilities)

devicelist = []

for device in devices:
device_id = device.get("id")
device["capabilities"] = capability_map.get(device_id, {})
device["capability_config"] = capability_config.get(device_id, {})
device["cls"] = device.get("class")
device["battery_low"] = device_id in low_battery_devices
device["update_available"] = device_id in update_available_devices
device["updated"] = device_id in updated_devices
device["unreachable"] = device_id in unreachable_devices
if device.get("location") is not None:
roomid = device["location"].removeprefix("/location/")
device["room"] = room_map.get(roomid)

if device_id == SHC_ID:
if isinstance(shc_state, Exception):
device["state"] = {}
else:
device["state"] = shc_state

devicelist.append(parse_dataclass(device, LivisiDevice))

LOGGER.debug("Loaded %d devices", len(devices))

return devicelist

def parse_messages(self, messages):
"""Parse message data from shc."""
low_battery_devices = set()
update_available_devices = set()
unreachable_devices = set()
Expand All @@ -270,8 +302,8 @@ async def async_get_devices(
device_ids = [
d.removeprefix("/device/") for d in message.get("devices", [])
]
if len(device_id) == 0:
source = message.get("source", "00000000000000000000000000000000")
if len(device_ids) == 0:
source = message.get("source", SHC_ID)
device_ids.add(source.replace("/device/", ""))
if msgtype == "DeviceLowBattery":
for device_id in device_ids:
Expand All @@ -285,27 +317,12 @@ async def async_get_devices(
elif msgtype == "DeviceUnreachable":
for device_id in device_ids:
unreachable_devices.add(device_id)

devicelist = []

for device in devices:
device_id = device.get("id")
device["capabilities"] = capability_map.get(device_id, {})
device["capability_config"] = capability_config.get(device_id, {})
device["cls"] = device.get("class")
device["battery_low"] = device_id in low_battery_devices
device["update_available"] = device_id in update_available_devices
device["updated"] = device_id in updated_devices
device["unreachable"] = device_id in unreachable_devices
if device.get("location") is not None:
roomid = device["location"].removeprefix("/location/")
device["room"] = room_map.get(roomid)

devicelist.append(parse_dataclass(device, LivisiDevice))

LOGGER.debug("Loaded %d devices", len(devices))

return devicelist
return (
low_battery_devices,
update_available_devices,
unreachable_devices,
updated_devices,
)

async def async_get_device_state(self, capability: str, key: str) -> Any | None:
"""Get state of the device."""
Expand Down
3 changes: 3 additions & 0 deletions custom_components/livisi/livisi_const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for the Livisi Smart Home integration."""

import logging
from typing import Final

Expand All @@ -10,6 +11,8 @@
WEBSERVICE_PORT: Final = 8080
REQUEST_TIMEOUT: Final = 2000

SHC_ID: Final = "00000000000000000000000000000000"

BATTERY_LOW: Final = "batteryLow"
UPDATE_AVAILABLE: Final = "DeviceUpdateAvailable"

Expand Down
9 changes: 9 additions & 0 deletions custom_components/livisi/livisi_device.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Code to represent a livisi device."""

from __future__ import annotations
from typing import Any

from dataclasses import dataclass

from .livisi_const import SHC_ID


@dataclass
class LivisiDevice:
Expand All @@ -13,6 +16,7 @@ class LivisiDevice:
type: str
tags: dict[str, str]
config: dict[str, Any]
state: dict[str, Any]
manufacturer: str
version: str
cls: str
Expand Down Expand Up @@ -42,3 +46,8 @@ def tag_category(self) -> str:
def tag_type(self) -> str:
"""Get tag type from config."""
return self.tags.get("type")

@property
def is_shc(self) -> bool:
"""Indicate whether this device is the controller."""
return self.id == SHC_ID
106 changes: 94 additions & 12 deletions custom_components/livisi/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
)
from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfTemperature, UnitOfPower
from homeassistant.const import (
PERCENTAGE,
UnitOfTemperature,
UnitOfPower,
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .livisi_device import LivisiDevice
Expand All @@ -36,7 +42,35 @@
from .coordinator import LivisiDataUpdateCoordinator
from .entity import LivisiEntity

SENSOR_TYPES = {

# if not isinstance(shc_state, Exception):
# {"configVersion": {"value": 707,"lastChanged": "2024-02-13T11:21:45.171684Z"},"cpuUsage": {"value": 3.0,"lastChanged": "2024-02-15T08:02:35.827063Z"},"currentUtcOffset": {"value": 60.0,"lastChanged": "1970-01-01T00:00:00Z"},"deviceConfigurationState": {"value": "Complete","lastChanged": "1970-01-01T00:00:00Z"},"deviceInclusionState": {"value": "Included","lastChanged": "2023-03-04T20:14:38.123688Z"},"discoveryActive": {"value": false,"lastChanged": "1970-01-01T00:00:00Z"},"diskUsage": {"value": 65.0,"lastChanged": "2024-02-15T07:57:35.829675Z"},"ethIpAddress": {"value": "192.168.10.121","lastChanged": "1970-01-01T00:00:00Z"},"ethMacAddress": {"value": "94:c6:91:3e:01:32","lastChanged": "1970-01-01T00:00:00Z"},"firmwareVersion": {"value": null,"lastChanged": "1970-01-01T00:00:00Z"},"inUseAdapter": {"value": "eth","lastChanged": "1970-01-01T00:00:00Z"},"innogyLayerAttached": {"value": true,"lastChanged": "1970-01-01T00:00:00Z"},"isReachable": {"value": null,"lastChanged": "1970-01-01T00:00:00Z"},"lastReboot": {"value": "2024-02-15T07:52:35+00:00","lastChanged": "2024-02-15T07:52:35.439616Z"},"lbDongleAttached": {"value": false,"lastChanged": "1970-01-01T00:00:00Z"},"memoryUsage": {"value": 27.0,"lastChanged": "2024-02-15T07:57:35.829820Z"},"operationStatus": {"value": "active","lastChanged": "2024-02-15T07:53:32.753651Z"},"updateAvailable": {"value": "","lastChanged": "2024-01-16T00:02:52.673931Z"},"updateState": {"value": "UpToDate","lastChanged": "1970-01-01T00:00:00Z"},"wMBusDongleAttached": {"value": false,"lastChanged": "1970-01-01T00:00:00Z"},"wifiIpAddress": {"value": "","lastChanged": "1970-01-01T00:00:00Z"},"wifiMacAddress": {"value": "80:91:33:75:b4:5b","lastChanged": "1970-01-01T00:00:00Z"},"wifiSignalStrength": {"value": 0,"lastChanged": "1970-01-01T00:00:00Z"}}%

CONTROLLER_SENSORS = {
"cpuUsage": SensorEntityDescription(
key="cpuUsage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"diskUsage": SensorEntityDescription(
key="diskUsage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
"memoryUsage": SensorEntityDescription(
key="memoryUsage",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
),
}

CAPABILITY_SENSORS = {
CAPABILITY_LUMINANCE_SENSOR: SensorEntityDescription(
key=LUMINANCE,
device_class=SensorDeviceClass.ILLUMINANCE,
Expand Down Expand Up @@ -93,22 +127,34 @@ def handle_coordinator_update() -> None:
for device in shc_devices:
if device.id not in known_devices:
known_devices.add(device.id)
for capability_name in SENSOR_TYPES:
if capability_name in device.capabilities:
sensor: SensorEntity = LivisiSensor(
if device.is_shc:
for sensor_name in CONTROLLER_SENSORS:
sensor: SensorEntity = LivisiControllerSensor(
config_entry,
coordinator,
device,
SENSOR_TYPES.get(capability_name),
capability_name=capability_name,
)
LOGGER.debug(
"Include device type: %s as %s",
device.type,
capability_name,
CONTROLLER_SENSORS.get(sensor_name),
)
coordinator.devices.add(device.id)
LOGGER.debug("Include SHC sensor %s", sensor_name)
entities.append(sensor)
else:
for capability_name in CAPABILITY_SENSORS:
if capability_name in device.capabilities:
sensor: SensorEntity = LivisiSensor(
config_entry,
coordinator,
device,
CAPABILITY_SENSORS.get(capability_name),
capability_name=capability_name,
)
LOGGER.debug(
"Include device type: %s as %s",
device.type,
capability_name,
)
coordinator.devices.add(device.id)
entities.append(sensor)
async_add_entities(entities)

config_entry.async_on_unload(
Expand Down Expand Up @@ -166,3 +212,39 @@ def update_states(self, state: Decimal) -> None:
"""Update the state of the device."""
self._attr_native_value = state
self.async_write_ha_state()


class LivisiControllerSensor(LivisiEntity, SensorEntity):
"""Represents a Livisi SHC Sensor."""

def __init__(
self,
config_entry: ConfigEntry,
coordinator: LivisiDataUpdateCoordinator,
device: LivisiDevice,
entity_desc: SensorEntityDescription,
) -> None:
"""Initialize the Livisi sensor."""
super().__init__(
config_entry,
coordinator,
device,
)
self._attr_name = entity_desc.key
self._attr_unique_id = device.id + "_" + entity_desc.key
self.entity_description = entity_desc
self._attr_translation_key = entity_desc.key

self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.entry_id)}
)

@property
def native_value(self):
"""Return the state of the sensor."""
shc_devices: list[LivisiDevice] = self.coordinator.data
for device in shc_devices:
if device.is_shc:
return device.state.get(self.entity_description.key, {}).get(
"value", None
)

0 comments on commit df94315

Please sign in to comment.