From 1524400c1bd72192148082af39a9dc8a74f5a2fc Mon Sep 17 00:00:00 2001 From: UpstreamData Date: Fri, 5 Jan 2024 09:12:17 -0700 Subject: [PATCH] Add fan speed data. --- custom_components/miner/const.py | 2 +- custom_components/miner/coordinator.py | 24 +++--- custom_components/miner/number.py | 2 +- custom_components/miner/sensor.py | 109 +++++++++++++++++++++++-- custom_components/miner/switch.py | 2 +- requirements.txt | 2 +- 6 files changed, 118 insertions(+), 23 deletions(-) diff --git a/custom_components/miner/const.py b/custom_components/miner/const.py index 59eefa5..bcf5e9f 100644 --- a/custom_components/miner/const.py +++ b/custom_components/miner/const.py @@ -9,4 +9,4 @@ DEVICE_CLASS_HASHRATE = "hashrate" DEVICE_CLASS_EFFICIENCY = "efficiency" TERA_HASH_PER_SECOND = "TH/s" -JOULES_PER_TERAHASH = "J/TH" +JOULES_PER_TERA_HASH = "J/TH" diff --git a/custom_components/miner/coordinator.py b/custom_components/miner/coordinator.py index a85584b..ed5d7d0 100644 --- a/custom_components/miner/coordinator.py +++ b/custom_components/miner/coordinator.py @@ -1,4 +1,4 @@ -"""IoTaWatt DataUpdateCoordinator.""" +"""Miner DataUpdateCoordinator.""" import logging from datetime import timedelta @@ -52,15 +52,16 @@ async def _async_update_data(self): miner_data = await self.miner.get_data( include=[ - "hostname", - "mac", - "is_mining", - "fw_ver", - "hashrate", - "expected_hashrate", - "hashboards", - "wattage", - "wattage_limit", + pyasic.DataOptions.HOSTNAME, + pyasic.DataOptions.MAC, + pyasic.DataOptions.IS_MINING, + pyasic.DataOptions.FW_VERSION, + pyasic.DataOptions.HASHRATE, + pyasic.DataOptions.EXPECTED_HASHRATE, + pyasic.DataOptions.HASHBOARDS, + pyasic.DataOptions.WATTAGE, + pyasic.DataOptions.WATTAGE_LIMIT, + pyasic.DataOptions.FANS, ] ) @@ -96,5 +97,8 @@ async def _async_update_data(self): } for board in miner_data.hashboards }, + "fan_sensors": { + idx: {"fan_speed": fan.speed} for idx, fan in enumerate(miner_data.fans) + }, } return data diff --git a/custom_components/miner/number.py b/custom_components/miner/number.py index dee85b3..211c490 100644 --- a/custom_components/miner/number.py +++ b/custom_components/miner/number.py @@ -1,4 +1,4 @@ -"""Support for IoTaWatt Energy monitor.""" +"""Support for Bitcoin ASIC miners.""" from __future__ import annotations import logging diff --git a/custom_components/miner/sensor.py b/custom_components/miner/sensor.py index a3cad35..8bfecfc 100644 --- a/custom_components/miner/sensor.py +++ b/custom_components/miner/sensor.py @@ -12,7 +12,7 @@ SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import POWER_WATT, TEMP_CELSIUS +from homeassistant.const import UnitOfPower, UnitOfTemperature, REVOLUTIONS_PER_MINUTE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,7 +23,7 @@ DEVICE_CLASS_EFFICIENCY, DEVICE_CLASS_HASHRATE, DOMAIN, - JOULES_PER_TERAHASH, + JOULES_PER_TERA_HASH, TERA_HASH_PER_SECOND, ) from .coordinator import MinerCoordinator @@ -49,19 +49,19 @@ class MinerNumberEntityDescription(SensorEntityDescription): ] = { "temperature": MinerSensorEntityDescription( "Temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, ), "board_temperature": MinerSensorEntityDescription( "Board Temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, ), "chip_temperature": MinerSensorEntityDescription( "Chip Temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, ), @@ -86,21 +86,27 @@ class MinerNumberEntityDescription(SensorEntityDescription): "power_limit": MinerSensorEntityDescription( "Power Limit", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), "miner_consumption": MinerSensorEntityDescription( "Miner Consumption", state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, ), "efficiency": MinerSensorEntityDescription( "Efficiency", - native_unit_of_measurement=JOULES_PER_TERAHASH, + native_unit_of_measurement=JOULES_PER_TERA_HASH, state_class=SensorStateClass.MEASUREMENT, device_class=DEVICE_CLASS_EFFICIENCY, ), + "fan_speed": MinerSensorEntityDescription( + "Fan Speed", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.SPEED + ) } @@ -138,6 +144,20 @@ def _create_board_entity(board_num: int, sensor: str) -> MinerBoardSensor: entity_description=description, ) + @callback + def _create_fan_entity(fan_num: int, sensor: str) -> MinerFanSensor: + """Create a fan sensor entity.""" + sensor_created.add(f"fan_{fan_num}-{sensor}") + description = ENTITY_DESCRIPTION_KEY_MAP.get( + sensor, MinerSensorEntityDescription("base_sensor") + ) + return MinerFanSensor( + coordinator=coordinator, + fan_num=fan_num, + sensor=sensor, + entity_description=description, + ) + await coordinator.async_config_entry_first_refresh() sensors = [] @@ -149,6 +169,11 @@ def _create_board_entity(board_num: int, sensor: str) -> MinerBoardSensor: _create_board_entity(board, sensor) for sensor in ["board_temperature", "chip_temperature", "board_hashrate"] ) + for fan in range(coordinator.miner.fan_count): + sensors.extend( + _create_fan_entity(fan, sensor) + for sensor in ["fan_speed"] + ) if sensors: async_add_entities(sensors) @@ -169,6 +194,13 @@ def new_data_received(): for sensor in coordinator.data["board_sensors"][new_board] if f"{new_board}-{sensor}" not in sensor_created ) + if coordinator.data["fan_sensors"]: + for new_fan in coordinator.data["fan_sensors"]: + new_sensors.extend( + _create_fan_entity(new_fan, sensor) + for sensor in coordinator.data["fan_sensors"][new_fan] + if f"{new_fan}-{sensor}" not in sensor_created + ) if new_sensors: async_add_entities(new_sensors) @@ -228,7 +260,7 @@ def native_value(self) -> StateType: class MinerBoardSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): - """Defines a Miner Sensor.""" + """Defines a Miner Board Sensor.""" entity_description: MinerSensorEntityDescription @@ -284,3 +316,62 @@ def _handle_coordinator_update(self) -> None: def native_value(self) -> StateType: """Return the state of the sensor.""" return self._sensor_data + + +class MinerFanSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): + """Defines a Miner Fan Sensor.""" + + entity_description: MinerSensorEntityDescription + + def __init__( + self, + coordinator: MinerCoordinator, + fan_num: int, + sensor: str, + entity_description: MinerSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator=coordinator) + self._attr_unique_id = f"{self.coordinator.data['mac']}-{fan_num}-{sensor}" + self._fan_num = fan_num + self._sensor = sensor + self.entity_description = entity_description + self._attr_force_update = True + + @property + def _sensor_data(self): + """Return sensor data.""" + if ( + self._fan_num in self.coordinator.data["fan_sensors"] + and self._sensor in self.coordinator.data["fan_sensors"][self._fan_num] + ): + return self.coordinator.data["fan_sensors"][self._fan_num][self._sensor] + else: + return None + + @property + def name(self) -> str | None: + """Return name of the entity.""" + return f"{self.coordinator.entry.title} Fan #{self._fan_num} {self.entity_description.key}" + + @property + def device_info(self) -> entity.DeviceInfo: + """Return device info.""" + return entity.DeviceInfo( + identifiers={(DOMAIN, self.coordinator.data["mac"])}, + manufacturer=self.coordinator.data["make"], + model=self.coordinator.data["model"], + sw_version=self.coordinator.data["fw_ver"], + name=f"{self.coordinator.entry.title}", + ) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + super()._handle_coordinator_update() + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._sensor_data diff --git a/custom_components/miner/switch.py b/custom_components/miner/switch.py index d7eab12..0324576 100644 --- a/custom_components/miner/switch.py +++ b/custom_components/miner/switch.py @@ -36,7 +36,7 @@ async def async_setup_entry( created = set() @callback - def _create_entity(key: str) -> SwitchEntity: + def _create_entity(key: str): """Create a sensor entity.""" created.add(key) diff --git a/requirements.txt b/requirements.txt index 6c69569..ca05151 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ colorlog==6.7.0 homeassistant==2024.1.0 pip>=21.0,<23.2 ruff==0.0.267 -pyasic==0.45.0 +pyasic==0.45.1 pre-commit