diff --git a/custom_components/miner/__init__.py b/custom_components/miner/__init__.py index be3384c..d4a0299 100644 --- a/custom_components/miner/__init__.py +++ b/custom_components/miner/__init__.py @@ -1,21 +1,27 @@ """The Miner integration.""" from __future__ import annotations +import pyasic from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import DOMAIN, CONF_IP from .coordinator import MinerCoordinator -# TODO List the platforms that you want to support. -# For your initial PR, limit it to 1 platform. PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Miner from a config entry.""" + miner_ip = entry.data[CONF_IP] + miner = await pyasic.get_miner(miner_ip) + + if miner is None: + raise ConfigEntryNotReady("Miner could not be found.") + m_coordinator = MinerCoordinator(hass, entry) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = m_coordinator diff --git a/custom_components/miner/config_flow.py b/custom_components/miner/config_flow.py index 0aba903..91af602 100644 --- a/custom_components/miner/config_flow.py +++ b/custom_components/miner/config_flow.py @@ -3,14 +3,19 @@ import pyasic import voluptuous as vol -from homeassistant import config_entries, exceptions -from homeassistant.helpers.selector import ( - TextSelector, - TextSelectorConfig, - TextSelectorType, -) - -from .const import CONF_IP, CONF_PASSWORD, CONF_TITLE, CONF_USERNAME, DOMAIN +from homeassistant import config_entries +from homeassistant.helpers.selector import TextSelector +from homeassistant.helpers.selector import TextSelectorConfig +from homeassistant.helpers.selector import TextSelectorType + +from .const import CONF_IP +from .const import CONF_RPC_PASSWORD +from .const import CONF_SSH_PASSWORD +from .const import CONF_SSH_USERNAME +from .const import CONF_TITLE +from .const import CONF_WEB_PASSWORD +from .const import CONF_WEB_USERNAME +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,24 +26,20 @@ # return len(devices) > 0 -# config_entry_flow.register_discovery_flow(DOMAIN, "Miner", _async_has_devices) +# config_entry_flow.register_discovery_flow(DOMAIN, "miner", _async_has_devices) -async def validate_input(data: dict[str, str]) -> dict[str, str]: +async def validate_ip_input( + data: dict[str, str] +) -> tuple[dict[str, str], pyasic.AnyMiner | None]: """Validate the user input allows us to connect.""" miner_ip = data.get(CONF_IP) - miner_username = data.get(CONF_USERNAME) - miner_password = data.get(CONF_PASSWORD) miner = await pyasic.get_miner(miner_ip) if miner is None: - return {"base": "Unable to connect to Miner, is IP correct?"} - - miner.username = miner_username - miner.pwd = miner_password - await miner.get_data(include=["mac"]) + return {"base": "Unable to connect to Miner, is IP correct?"}, None - return {} + return {}, miner class MinerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -49,44 +50,108 @@ class MinerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize.""" self._data = {} + self._miner = None async def async_step_user(self, user_input=None): - """Handle the initial step.""" + """Get miner IP and check if it is available.""" if user_input is None: user_input = {} schema = vol.Schema( - { - vol.Required(CONF_IP, default=user_input.get(CONF_IP, "")): str, - vol.Required( - CONF_USERNAME, default=user_input.get(CONF_USERNAME, "root") - ): str, - vol.Optional( - CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") - ): TextSelector( - TextSelectorConfig( - type=TextSelectorType.PASSWORD, autocomplete="current-password" - ) - ), - } + {vol.Required(CONF_IP, default=user_input.get(CONF_IP, "")): str} ) + if not user_input: return self.async_show_form(step_id="user", data_schema=schema) - errors = await validate_input(user_input) + errors, miner = await validate_ip_input(user_input) - if not errors: - self._data.update(user_input) - return await self.async_step_title() + if errors: + return self.async_show_form( + step_id="user", data_schema=schema, errors=errors + ) - return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + self._miner = miner + self._data.update(user_input) + return await self.async_step_login() - async def async_step_title(self, user_input=None): - """Ask for Entity Title.""" + async def async_step_login(self, user_input=None): + """Get miner login credentials.""" + if user_input is None: + user_input = {} - miner_ip = self._data.get(CONF_IP) - miner = await pyasic.get_miner(miner_ip) # should be fast, cached - title = await miner.get_hostname() + schema_data = {} + + if self._miner.api is not None: + if self._miner.api.pwd is not None: + schema_data[ + vol.Optional( + CONF_RPC_PASSWORD, + default=user_input.get( + CONF_RPC_PASSWORD, + self._miner.web.pwd + if self._miner.api.pwd is not None + else "", + ), + ) + ] = TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, autocomplete="current-password" + ) + ) + + if self._miner.web is not None: + schema_data[ + vol.Required( + CONF_WEB_USERNAME, + default=user_input.get(CONF_WEB_USERNAME, self._miner.web.username), + ) + ] = str + schema_data[ + vol.Optional( + CONF_WEB_PASSWORD, + default=user_input.get( + CONF_WEB_PASSWORD, + self._miner.web.pwd if self._miner.web.pwd is not None else "", + ), + ) + ] = TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, autocomplete="current-password" + ) + ) + + if self._miner.ssh is not None: + schema_data[ + vol.Required( + CONF_SSH_USERNAME, + default=user_input.get(CONF_SSH_USERNAME, self._miner.ssh.username), + ) + ] = str + schema_data[ + vol.Optional( + CONF_SSH_PASSWORD, + default=user_input.get( + CONF_SSH_PASSWORD, + self._miner.ssh.pwd if self._miner.ssh.pwd is not None else "", + ), + ) + ] = TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, autocomplete="current-password" + ) + ) + + schema = vol.Schema(schema_data) + if not user_input: + return self.async_show_form(step_id="login", data_schema=schema) + + self._data.update(user_input) + return await self.async_step_title() + + async def async_step_title(self, user_input=None): + """Get entity title.""" + title = await self._miner.get_hostname() if user_input is None: user_input = {} @@ -102,14 +167,6 @@ async def async_step_title(self, user_input=None): if not user_input: return self.async_show_form(step_id="title", data_schema=data_schema) - data = {**self._data, **user_input} - - return self.async_create_entry(title=data[CONF_TITLE], data=data) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - + self._data.update(user_input) -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" + return self.async_create_entry(title=self._data[CONF_TITLE], data=self._data) diff --git a/custom_components/miner/const.py b/custom_components/miner/const.py index bcf5e9f..113db10 100644 --- a/custom_components/miner/const.py +++ b/custom_components/miner/const.py @@ -1,12 +1,14 @@ """Constants for the Miner integration.""" DOMAIN = "miner" + CONF_IP = "ip" CONF_TITLE = "title" -CONF_PASSWORD = "password" -CONF_USERNAME = "username" +CONF_SSH_PASSWORD = "ssh_password" +CONF_SSH_USERNAME = "ssh_username" +CONF_RPC_PASSWORD = "rpc_password" +CONF_WEB_PASSWORD = "web_password" +CONF_WEB_USERNAME = "web_username" -DEVICE_CLASS_HASHRATE = "hashrate" -DEVICE_CLASS_EFFICIENCY = "efficiency" TERA_HASH_PER_SECOND = "TH/s" JOULES_PER_TERA_HASH = "J/TH" diff --git a/custom_components/miner/coordinator.py b/custom_components/miner/coordinator.py index ed5d7d0..03d5502 100644 --- a/custom_components/miner/coordinator.py +++ b/custom_components/miner/coordinator.py @@ -8,7 +8,14 @@ from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_IP, CONF_PASSWORD, CONF_USERNAME +from .const import ( + CONF_IP, + CONF_RPC_PASSWORD, + CONF_SSH_PASSWORD, + CONF_SSH_USERNAME, + CONF_WEB_PASSWORD, + CONF_WEB_USERNAME, +) _LOGGER = logging.getLogger(__name__) @@ -37,18 +44,35 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: ), ) + @property + def available(self): + """Return if device is available or not.""" + return self.miner is not None + async def _async_update_data(self): """Fetch sensors from miners.""" miner_ip = self.entry.data[CONF_IP] - miner_username = self.entry.data[CONF_USERNAME] - miner_password = self.entry.data[CONF_PASSWORD] + if self.miner is None: + self.miner = await pyasic.get_miner(miner_ip) + + _LOGGER.debug(f"Found miner :{self.miner}") + + if self.miner is None: + raise UpdateFailed("Miner Offline") try: - if self.miner is None: - self.miner = await pyasic.get_miner(miner_ip) - self.miner.username = miner_username - self.miner.pwd = miner_password + if self.miner.api is not None: + if self.miner.api.pwd is not None: + self.miner.api.pwd = self.entry.data.get(CONF_RPC_PASSWORD, "") + + if self.miner.web is not None: + self.miner.web.username = self.entry.data.get(CONF_WEB_USERNAME, "") + self.miner.web.pwd = self.entry.data.get(CONF_WEB_PASSWORD, "") + + if self.miner.ssh is not None: + self.miner.ssh.username = self.entry.data.get(CONF_SSH_USERNAME, "") + self.miner.ssh.pwd = self.entry.data.get(CONF_SSH_PASSWORD, "") miner_data = await self.miner.get_data( include=[ @@ -69,9 +93,9 @@ async def _async_update_data(self): raise UpdateFailed("API Error") from err except Exception as err: - raise UpdateFailed("API Error") from err + raise UpdateFailed("Unknown Error") from err - _LOGGER.debug(miner_data) + _LOGGER.debug(f"Got data: {miner_data}") data = { "hostname": miner_data.hostname, diff --git a/custom_components/miner/manifest.json b/custom_components/miner/manifest.json index 5315636..5a6dd82 100644 --- a/custom_components/miner/manifest.json +++ b/custom_components/miner/manifest.json @@ -7,8 +7,8 @@ "homekit": {}, "iot_class": "local_polling", "issue_tracker": "https://github.com/Schnitzel/hass-miner/issues", - "requirements": ["pyasic==0.46.0"], + "requirements": ["pyasic==0.48.5"], "ssdp": [], - "version": "1.1.0", + "version": "1.1.1b4", "zeroconf": [] } diff --git a/custom_components/miner/number.py b/custom_components/miner/number.py index 211c490..ffac0f4 100644 --- a/custom_components/miner/number.py +++ b/custom_components/miner/number.py @@ -6,11 +6,11 @@ import pyasic from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from pyasic.miners.backends import BOSMiner from .const import DOMAIN from .coordinator import MinerCoordinator @@ -25,12 +25,6 @@ async def async_setup_entry( ) -> None: """Add sensors for passed config_entry in HA.""" coordinator: MinerCoordinator = hass.data[DOMAIN][config_entry.entry_id] - created = set() - - @callback - def _create_entity(key: str): - """Create a sensor entity.""" - created.add(key) await coordinator.async_config_entry_first_refresh() if coordinator.miner.supports_autotuning: @@ -109,42 +103,15 @@ async def async_set_native_value(self, value): miner = self.coordinator.miner _LOGGER.debug( - "%s: setting power limit to %s.", self.coordinator.entry.title, value + f"{self.coordinator.entry.title}: setting power limit to {value}." ) if not miner.supports_autotuning: raise TypeError( - f"{self.coordinator.entry.title} does not support setting power limit." + f"{self.coordinator.entry.title}: Tuning not supported." ) - if isinstance(miner, BOSMiner): - max_diff = 500 - try: - try: - current_value = self._attr_native_value - diff = int(value) - int(current_value) - smooth_tune = -max_diff < diff < max_diff - - if smooth_tune: - if diff < 0: - result = await miner.web.grpc.decrement_power_target( - abs(diff) - ) - else: - result = await miner.web.grpc.increment_power_target( - abs(diff) - ) - else: - result = await miner.web.grpc.set_power_target(int(value)) - except TypeError: - result = await miner.web.grpc.set_power_target(int(value)) - except pyasic.APIError: - result = await miner.set_power_limit(int(value)) - - else: - result = await miner.set_power_limit( - int(value) - ) # noqa: ignore miner being assumed to be None + result = await miner.set_power_limit(int(value)) if not result: raise pyasic.APIError("Failed to set wattage.") @@ -160,3 +127,8 @@ def _handle_coordinator_update(self) -> None: ] super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Return if entity is available or not.""" + return self.coordinator.available diff --git a/custom_components/miner/sensor.py b/custom_components/miner/sensor.py index 8bfecfc..42650f3 100644 --- a/custom_components/miner/sensor.py +++ b/custom_components/miner/sensor.py @@ -1,31 +1,26 @@ -"""Support for IoTaWatt Energy monitor.""" +"""Support for Miner sensors.""" from __future__ import annotations import logging from collections.abc import Callable from dataclasses import dataclass -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) +from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntityDescription +from homeassistant.components.sensor import SensorStateClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfPower, UnitOfTemperature, REVOLUTIONS_PER_MINUTE -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import REVOLUTIONS_PER_MINUTE +from homeassistant.const import UnitOfPower +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DEVICE_CLASS_EFFICIENCY, - DEVICE_CLASS_HASHRATE, - DOMAIN, - JOULES_PER_TERA_HASH, - TERA_HASH_PER_SECOND, -) +from .const import DOMAIN +from .const import JOULES_PER_TERA_HASH +from .const import TERA_HASH_PER_SECOND from .coordinator import MinerCoordinator _LOGGER = logging.getLogger(__name__) @@ -38,75 +33,57 @@ class MinerSensorEntityDescription(SensorEntityDescription): value: Callable = None -class MinerNumberEntityDescription(SensorEntityDescription): - """Class describing ASIC Miner number entities.""" - - value: Callable = None - - -ENTITY_DESCRIPTION_KEY_MAP: dict[ - str, MinerSensorEntityDescription or MinerNumberEntityDescription -] = { +ENTITY_DESCRIPTION_KEY_MAP: dict[str, MinerSensorEntityDescription] = { "temperature": MinerSensorEntityDescription( - "Temperature", + key="Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.TEMPERATURE, ), "board_temperature": MinerSensorEntityDescription( - "Board Temperature", + key="Board Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.TEMPERATURE, ), "chip_temperature": MinerSensorEntityDescription( - "Chip Temperature", + key="Chip Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.TEMPERATURE, ), "hashrate": MinerSensorEntityDescription( - "Hashrate", + key="Hashrate", native_unit_of_measurement=TERA_HASH_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - device_class=DEVICE_CLASS_HASHRATE, ), "ideal_hashrate": MinerSensorEntityDescription( - "Ideal Hashrate", + key="Ideal Hashrate", native_unit_of_measurement=TERA_HASH_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - device_class=DEVICE_CLASS_HASHRATE, ), "board_hashrate": MinerSensorEntityDescription( - "Board Hashrate", + key="Board Hashrate", native_unit_of_measurement=TERA_HASH_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, - device_class=DEVICE_CLASS_HASHRATE, ), "power_limit": MinerSensorEntityDescription( - "Power Limit", + key="Power Limit", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, ), "miner_consumption": MinerSensorEntityDescription( - "Miner Consumption", + key="Miner Consumption", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, ), "efficiency": MinerSensorEntityDescription( - "Efficiency", + key="Efficiency", native_unit_of_measurement=JOULES_PER_TERA_HASH, state_class=SensorStateClass.MEASUREMENT, - device_class=DEVICE_CLASS_EFFICIENCY, ), "fan_speed": MinerSensorEntityDescription( - "Fan Speed", + key="Fan Speed", native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.SPEED - ) + ), } @@ -117,23 +94,20 @@ async def async_setup_entry( ) -> None: """Add sensors for passed config_entry in HA.""" coordinator: MinerCoordinator = hass.data[DOMAIN][config_entry.entry_id] - sensor_created = set() - @callback - def _create_miner_entity(key: str) -> MinerSensor: + def _create_miner_entity(sensor: str) -> MinerSensor: """Create a miner sensor entity.""" - sensor_created.add(key) description = ENTITY_DESCRIPTION_KEY_MAP.get( - key, MinerSensorEntityDescription("base_sensor") + sensor, MinerSensorEntityDescription("base_sensor") ) return MinerSensor( - coordinator=coordinator, key=key, entity_description=description + coordinator=coordinator, + sensor=sensor, + entity_description=description, ) - @callback def _create_board_entity(board_num: int, sensor: str) -> MinerBoardSensor: """Create a board sensor entity.""" - sensor_created.add(f"board_{board_num}-{sensor}") description = ENTITY_DESCRIPTION_KEY_MAP.get( sensor, MinerSensorEntityDescription("base_sensor") ) @@ -144,10 +118,8 @@ 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") ) @@ -161,51 +133,15 @@ def _create_fan_entity(fan_num: int, sensor: str) -> MinerFanSensor: await coordinator.async_config_entry_first_refresh() sensors = [] - sensors.extend( - _create_miner_entity(key) for key in coordinator.data["miner_sensors"] - ) + for s in coordinator.data["miner_sensors"]: + sensors.append(_create_miner_entity(s)) for board in range(coordinator.miner.expected_hashboards): - sensors.extend( - _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) - - @callback - def new_data_received(): - """Check for new sensors.""" - new_sensors = [] - new_sensors.extend( - _create_miner_entity(key) - for key in coordinator.data["miner_sensors"] - if key not in sensor_created - ) - - if coordinator.data["board_sensors"]: - for new_board in coordinator.data["board_sensors"]: - new_sensors.extend( - _create_board_entity(new_board, sensor) - 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) - - coordinator.async_add_listener(new_data_received) + for s in ["board_temperature", "chip_temperature", "board_hashrate"]: + sensors.append(_create_board_entity(board, s)) + for fan in range(coordinator.miner.expected_fans): + for s in ["fan_speed"]: + sensors.append(_create_fan_entity(fan, s)) + async_add_entities(sensors) class MinerSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): @@ -216,20 +152,22 @@ class MinerSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): def __init__( self, coordinator: MinerCoordinator, - key: str, + sensor: str, entity_description: MinerSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator=coordinator) - self._attr_unique_id = f"{self.coordinator.data['mac']}-{key}" - self._key = key + self._attr_unique_id = f"{self.coordinator.data['mac']}-{sensor}" + self._sensor = sensor self.entity_description = entity_description - self._attr_force_update = True @property def _sensor_data(self): """Return sensor data.""" - return self.coordinator.data["miner_sensors"][self._key] + try: + return self.coordinator.data["miner_sensors"][self._sensor] + except LookupError: + return None @property def name(self) -> str | None: @@ -247,17 +185,16 @@ def device_info(self) -> entity.DeviceInfo: 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 + @property + def available(self) -> bool: + """Return if entity is available or not.""" + return self.coordinator.available + class MinerBoardSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): """Defines a Miner Board Sensor.""" @@ -277,17 +214,13 @@ def __init__( self._board_num = board_num self._sensor = sensor self.entity_description = entity_description - self._attr_force_update = True @property def _sensor_data(self): """Return sensor data.""" - if ( - self._board_num in self.coordinator.data["board_sensors"] - and self._sensor in self.coordinator.data["board_sensors"][self._board_num] - ): + try: return self.coordinator.data["board_sensors"][self._board_num][self._sensor] - else: + except LookupError: return None @property @@ -306,17 +239,16 @@ def device_info(self) -> entity.DeviceInfo: 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 + @property + def available(self) -> bool: + """Return if entity is available or not.""" + return self.coordinator.available + class MinerFanSensor(CoordinatorEntity[MinerCoordinator], SensorEntity): """Defines a Miner Fan Sensor.""" @@ -341,12 +273,9 @@ def __init__( @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] - ): + try: return self.coordinator.data["fan_sensors"][self._fan_num][self._sensor] - else: + except LookupError: return None @property @@ -365,13 +294,12 @@ def device_info(self) -> entity.DeviceInfo: 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 + + @property + def available(self) -> bool: + """Return if entity is available or not.""" + return self.coordinator.available diff --git a/custom_components/miner/strings.json b/custom_components/miner/strings.json index 67fb78b..a8ffc70 100644 --- a/custom_components/miner/strings.json +++ b/custom_components/miner/strings.json @@ -3,14 +3,21 @@ "step": { "user": { "data": { - "ip": "[%key:common::config_flow::data::ip%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "ip": "[%key:common::config_flow::data::ip%]" } }, - "hostname": { + "login": { "data": { - "hostname": "[%key:common::config_flow::data::hostname%]" + "ssh_username": "[%key:common::config_flow::data::ssh_username%]", + "ssh_password": "[%key:common::config_flow::data::ssh_password%]", + "rpc_password": "[%key:common::config_flow::data::rpc_password%]", + "web_username": "[%key:common::config_flow::data::web_username%]", + "web_password": "[%key:common::config_flow::data::web_password%]" + } + }, + "title": { + "data": { + "title": "[%key:common::config_flow::data::title%]" } } }, diff --git a/custom_components/miner/switch.py b/custom_components/miner/switch.py index 0324576..6c11c74 100644 --- a/custom_components/miner/switch.py +++ b/custom_components/miner/switch.py @@ -1,4 +1,4 @@ -"""Support for IoTaWatt Energy monitor.""" +"""Support for Miner shutdown.""" from __future__ import annotations import logging @@ -8,7 +8,8 @@ from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -21,7 +22,7 @@ @dataclass class MinerSensorEntityDescription(SensorEntityDescription): - """Class describing IotaWatt sensor entities.""" + """Class describing Miner sensor entities.""" value: Callable = None @@ -50,17 +51,6 @@ def _create_entity(key: str): ] ) - # @callback - # def new_data_received(): - # """Check for new sensors.""" - # entities = [ - # _create_entity(key) for key in coordinator.data if key not in created - # ] - # if entities: - # async_add_entities(entities) - - # coordinator.async_add_listener(new_data_received) - class MinerActiveSwitch(CoordinatorEntity[MinerCoordinator], SwitchEntity): """Defines a Miner Switch to pause and unpause the miner.""" @@ -93,9 +83,9 @@ def device_info(self) -> entity.DeviceInfo: async def async_turn_on(self) -> None: """Turn on miner.""" miner = self.coordinator.miner - _LOGGER.debug("%s: Resume mining.", self.coordinator.entry.title) + _LOGGER.debug(f"{self.coordinator.entry.title}: Resume mining.") if not miner.supports_shutdown: - raise TypeError(f"{miner} does not support shutdown mode.") + raise TypeError(f"{miner}: Shutdown not supported.") self._attr_is_on = True await miner.resume_mining() self.async_write_ha_state() @@ -103,9 +93,9 @@ async def async_turn_on(self) -> None: async def async_turn_off(self) -> None: """Turn off miner.""" miner = self.coordinator.miner - _LOGGER.debug("%s: Stop mining.", self.coordinator.entry.title) + _LOGGER.debug(f"{self.coordinator.entry.title}: Stop mining.") if not miner.supports_shutdown: - raise TypeError(f"{miner} does not support shutdown mode.") + raise TypeError(f"{miner}: Shutdown not supported.") self._attr_is_on = False await miner.stop_mining() self.async_write_ha_state() @@ -117,3 +107,8 @@ def _handle_coordinator_update(self) -> None: self._attr_is_on = self.coordinator.data["is_mining"] super()._handle_coordinator_update() + + @property + def available(self) -> bool: + """Return if entity is available or not.""" + return self.coordinator.available diff --git a/custom_components/miner/translations/en.json b/custom_components/miner/translations/en.json index 3818b97..e0b038f 100644 --- a/custom_components/miner/translations/en.json +++ b/custom_components/miner/translations/en.json @@ -5,19 +5,23 @@ "single_instance_allowed": "Already configured. Only a single configuration possible." }, "step": { - "confirm": { - "description": "Do you want to start set up?" - }, "user": { "data": { - "ip": "IP Address", - "username": "Username", - "password": "Password" + "ip": "IP Address" + } + }, + "login": { + "data": { + "ssh_username": "SSH Username", + "ssh_password": "SSH Password", + "rpc_password": "RPC API Password", + "web_username": "Web Username", + "web_password": "Web Password" } }, - "hostname": { + "title": { "data": { - "hostname": "Hostname of Miner to be used in HomeAssistant" + "title": "Title" } } } diff --git a/requirements.txt b/requirements.txt index aecdd72..17997f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ colorlog==6.7.0 -homeassistant==2024.1.0 +homeassistant>=2024.1.0 pip>=21.0,<23.2 ruff==0.0.267 -pyasic==0.46.0 +pyasic==0.48.5 +setuptools==69.0.3 pre-commit