From 96498e580e322389e0c2be91dc7766dcd0bc099d Mon Sep 17 00:00:00 2001 From: Brian Berg Date: Wed, 17 Jun 2020 01:46:53 +0000 Subject: [PATCH] refactor: split out data coordinators and sensors - remove unused imports --- custom_components/nicehash/__init__.py | 73 +----- .../nicehash/data_coordinators.py | 82 +++++++ custom_components/nicehash/sensor.py | 208 +--------------- custom_components/nicehash/sensors.py | 226 ++++++++++++++++++ 4 files changed, 314 insertions(+), 275 deletions(-) create mode 100644 custom_components/nicehash/data_coordinators.py create mode 100644 custom_components/nicehash/sensors.py diff --git a/custom_components/nicehash/__init__.py b/custom_components/nicehash/__init__.py index 3dd1cb6..15dacba 100644 --- a/custom_components/nicehash/__init__.py +++ b/custom_components/nicehash/__init__.py @@ -4,16 +4,13 @@ Integrates NiceHash with Home Assistant For more details about this integration, please refer to https://github.com/brianberg/ha-nicehash """ -from datetime import timedelta import logging import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICES, CONF_TIMEOUT from homeassistant.core import Config, HomeAssistant from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.exceptions import PlatformNotReady from .const import ( @@ -21,15 +18,15 @@ from .const import ( CONF_API_SECRET, CONF_CURRENCY, CONF_ORGANIZATION_ID, - CURRENCY_BTC, CURRENCY_USD, DOMAIN, STARTUP_MESSAGE, ) -from .nicehash import NiceHashPrivateClient, NiceHashPublicClient - -SCAN_INTERVAL_RIGS = timedelta(minutes=1) -SCAN_INTERVAL_ACCOUNTS = timedelta(minutes=60) +from .nicehash import NiceHashPrivateClient +from .data_coordinators import ( + NiceHashAccountsDataUpdateCoordinator, + NiceHashMiningRigsDataUpdateCoordinator, +) _LOGGER = logging.getLogger(__name__) @@ -86,63 +83,3 @@ async def async_setup(hass: HomeAssistant, config: Config): await discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config) return True - - -class NiceHashAccountsDataUpdateCoordinator(DataUpdateCoordinator): - """Manages fetching accounts data from NiceHash API""" - - def __init__(self, hass: HomeAssistant, client: NiceHashPrivateClient): - """Initialize""" - self.name = f"{DOMAIN}_accounts_coordinator" - self._client = client - - super().__init__( - hass, _LOGGER, name=self.name, update_interval=SCAN_INTERVAL_ACCOUNTS - ) - - async def _async_update_data(self): - """Update accounts data and exchange rates""" - try: - accounts = await self._client.get_accounts() - exchange_rates = await NiceHashPublicClient().get_exchange_rates() - rates_dict = dict() - for rate in exchange_rates: - fromCurrency = rate.get("fromCurrency") - # Only care about the Bitcoin exchange rates - if fromCurrency == CURRENCY_BTC: - toCurrency = rate.get("toCurrency") - exchange_rate = float(rate.get("exchangeRate")) - rates_dict[f"{fromCurrency}-{toCurrency}"] = exchange_rate - return { - "accounts": accounts, - "exchange_rates": rates_dict, - } - except Exception as e: - raise UpdateFailed(e) - - -class NiceHashMiningRigsDataUpdateCoordinator(DataUpdateCoordinator): - """Manages fetching mining rigs data from NiceHash API""" - - def __init__(self, hass: HomeAssistant, client: NiceHashPrivateClient): - """Initialize""" - self.name = f"{DOMAIN}_mining_rigs_coordinator" - self._client = client - - super().__init__( - hass, _LOGGER, name=self.name, update_interval=SCAN_INTERVAL_RIGS - ) - - async def _async_update_data(self): - """Update mining rigs data""" - try: - data = await self._client.get_mining_rigs() - mining_rigs = data.get("miningRigs") - rigs_dict = dict() - for rig in mining_rigs: - rig_id = rig.get("rigId") - rigs_dict[f"{rig_id}"] = rig - data["miningRigs"] = rigs_dict - return data - except Exception as e: - raise UpdateFailed(e) diff --git a/custom_components/nicehash/data_coordinators.py b/custom_components/nicehash/data_coordinators.py new file mode 100644 index 0000000..bca1a30 --- /dev/null +++ b/custom_components/nicehash/data_coordinators.py @@ -0,0 +1,82 @@ +""" +NiceHash Data Update Coordinators +""" +from datetime import timedelta +import logging + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + CURRENCY_BTC, + DOMAIN, +) +from .nicehash import NiceHashPrivateClient, NiceHashPublicClient + +SCAN_INTERVAL_RIGS = timedelta(minutes=1) +SCAN_INTERVAL_ACCOUNTS = timedelta(minutes=60) + +_LOGGER = logging.getLogger(__name__) + + +class NiceHashAccountsDataUpdateCoordinator(DataUpdateCoordinator): + """Manages fetching accounts data from NiceHash API""" + + def __init__(self, hass: HomeAssistant, client: NiceHashPrivateClient): + """Initialize""" + self.name = f"{DOMAIN}_accounts_coordinator" + self._client = client + + super().__init__( + hass, _LOGGER, name=self.name, update_interval=SCAN_INTERVAL_ACCOUNTS + ) + + async def _async_update_data(self): + """Update accounts data and exchange rates""" + try: + accounts = await self._client.get_accounts() + exchange_rates = await NiceHashPublicClient().get_exchange_rates() + rates_dict = dict() + for rate in exchange_rates: + fromCurrency = rate.get("fromCurrency") + # Only care about the Bitcoin exchange rates + if fromCurrency == CURRENCY_BTC: + toCurrency = rate.get("toCurrency") + exchange_rate = float(rate.get("exchangeRate")) + rates_dict[f"{fromCurrency}-{toCurrency}"] = exchange_rate + return { + "accounts": accounts, + "exchange_rates": rates_dict, + } + except Exception as e: + raise UpdateFailed(e) + + +class NiceHashMiningRigsDataUpdateCoordinator(DataUpdateCoordinator): + """Manages fetching mining rigs data from NiceHash API""" + + def __init__(self, hass: HomeAssistant, client: NiceHashPrivateClient): + """Initialize""" + self.name = f"{DOMAIN}_mining_rigs_coordinator" + self._client = client + + super().__init__( + hass, _LOGGER, name=self.name, update_interval=SCAN_INTERVAL_RIGS + ) + + async def _async_update_data(self): + """Update mining rigs data""" + try: + data = await self._client.get_mining_rigs() + mining_rigs = data.get("miningRigs") + rigs_dict = dict() + for rig in mining_rigs: + rig_id = rig.get("rigId") + rigs_dict[f"{rig_id}"] = rig + data["miningRigs"] = rigs_dict + return data + except Exception as e: + raise UpdateFailed(e) diff --git a/custom_components/nicehash/sensor.py b/custom_components/nicehash/sensor.py index 3e02879..6d5efed 100644 --- a/custom_components/nicehash/sensor.py +++ b/custom_components/nicehash/sensor.py @@ -1,16 +1,13 @@ """ Sensor platform for NiceHash """ -from datetime import datetime, timedelta import logging -import os from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import Config, HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle from .const import ( BALANCE_TYPE_AVAILABLE, @@ -28,11 +25,7 @@ from .const import ( ICON_TEMPERATURE, ) from .nicehash import NiceHashPrivateClient, NiceHashPublicClient - -ATTRIBUTION = "Data provided by NiceHash" -FORMAT_DATETIME = "%d-%m-%Y %H:%M" -SCAN_INTERVAL_RIGS = timedelta(minutes=1) -SCAN_INTERVAL_ACCOUNTS = timedelta(minutes=60) +from .sensors import NiceHashBalanceSensor, NiceHashRigTemperatureSensor _LOGGER = logging.getLogger(__name__) @@ -109,202 +102,3 @@ async def async_setup_platform( [NiceHashRigTemperatureSensor(rigs_coordinator, rig) for rig in mining_rigs], True, ) - - -class NiceHashBalanceSensor(Entity): - """NiceHash Account Balance Sensor""" - - def __init__( - self, - coordinator, - organization_id, - currency, - balance_type=BALANCE_TYPE_AVAILABLE, - ): - """Initialize the sensor""" - _LOGGER.debug(f"Account Balance Sensor: {balance_type} {currency}") - self.coordinator = coordinator - self.currency = currency - self.organization_id = organization_id - self.balance_type = balance_type - self._available = 0.00 - self._pending = 0.00 - self._total_balance = 0.00 - self._exchange_rate = 0.00 - - @property - def name(self): - """Sensor name""" - balance_type = self.balance_type[0].upper() + self.balance_type[1:] - return f"{DEFAULT_NAME} {balance_type} Account Balance {self.currency}" - - @property - def unique_id(self): - """Unique entity id""" - return f"{self.organization_id}:{self.currency}:{self.balance_type}" - - @property - def should_poll(self): - """No need to pool, Coordinator notifies entity of updates""" - return False - - @property - def available(self): - """Whether sensor is available""" - return self.coordinator.last_update_success - - @property - def state(self): - """Sensor state""" - accounts = self.coordinator.data.get("accounts") - total = accounts.get("total") - self._pending = float(total.get("pending")) - self._available = float(total.get("available")) - self._total_balance = float(total.get("totalBalance")) - - if self.balance_type == BALANCE_TYPE_TOTAL: - balance = self._total_balance - elif self.balance_type == BALANCE_TYPE_PENDING: - balance = self._pending - else: - balance = self._available - - if self.currency == CURRENCY_BTC: - return balance - else: - exchange_rates = self.coordinator.data.get("exchange_rates") - self._exchange_rate = exchange_rates.get(f"{CURRENCY_BTC}-{self.currency}") - return round(balance * self._exchange_rate, 2) - - @property - def icon(self): - """Sensor icon""" - if self.currency == CURRENCY_EUR: - return ICON_CURRENCY_EUR - elif self.currency == CURRENCY_USD: - return ICON_CURRENCY_USD - return ICON_CURRENCY_BTC - - @property - def unit_of_measurement(self): - """Sensor unit of measurement""" - return self.currency - - @property - def device_state_attributes(self): - """Sensor device state attributes""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "total": self._total_balance, - "available": self._available, - "pending": self._pending, - "exchange_rate": self._exchange_rate, - } - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update entity""" - await self.coordinator.async_request_refresh() - - -class NiceHashRigTemperatureSensor(Entity): - """NichHash Mining Rig Temperature Sensor""" - - def __init__(self, coordinator, rig): - """Initialize the sensor""" - self.coordinator = coordinator - self._rig_id = rig["rigId"] - self._name = rig["name"] - self._temps = [] - self._num_devices = 0 - self._num_active_devices = 0 - _LOGGER.debug(f"Mining Rig Temperature Sensor: {self._name} ({self._rig_id})") - - @property - def name(self): - """Sensor name""" - return self._name - - @property - def unique_id(self): - """Unique entity id""" - return self._rig_id - - @property - def should_poll(self): - """No need to pool, Coordinator notifies entity of updates""" - return False - - @property - def available(self): - """Whether sensor is available""" - return self.coordinator.last_update_success - - @property - def state(self): - """Sensor state""" - mining_rigs = self.coordinator.data.get("miningRigs") - try: - rig_data = mining_rigs.get(self._rig_id) - devices = rig_data.get("devices") - highest_temp = 0 - self._temps = [] - self._num_devices = len(devices) - - if self._num_devices > 0: - _LOGGER.debug(f"{self._name}: Found {self._num_devices} devices") - for device in devices: - status = device.get("status").get("enumName") - # Ignore inactive devices - if status == DEVICE_STATUS_INACTIVE: - continue - temp = int(device.get("temperature")) - self._temps.append(temp) - if temp > highest_temp: - highest_temp = temp - - self._num_active_devices = len(self._temps) - return highest_temp - else: - _LOGGER.debug(f"{self._name}: No devices found") - self._num_devices = 0 - return 0 - except Exception as e: - _LOGGER.error(f"Unable to get mining rig {self._rig_id}\n{e}") - return 0 - - @property - def icon(self): - """Sensor icon""" - return ICON_TEMPERATURE - - @property - def unit_of_measurement(self): - """Sensor unit of measurement""" - # Not Celsius because then HA might convert to Fahrenheit - return "C" - - @property - def device_state_attributes(self): - """Sensor device state attributes""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "temperatures": self._temps, - "active_devices": self._num_active_devices, - "total_devices": self._num_devices, - } - - async def async_added_to_hass(self): - """Connect to dispatcher listening for entity data notifications""" - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state) - ) - - async def async_update(self): - """Update entity""" - await self.coordinator.async_request_refresh() diff --git a/custom_components/nicehash/sensors.py b/custom_components/nicehash/sensors.py new file mode 100644 index 0000000..bd3ac9a --- /dev/null +++ b/custom_components/nicehash/sensors.py @@ -0,0 +1,226 @@ +""" +NiceHash Mining Rig Temperature Sensor +""" +import logging + +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.entity import Entity + +from .const import ( + BALANCE_TYPE_AVAILABLE, + BALANCE_TYPE_PENDING, + BALANCE_TYPE_TOTAL, + CURRENCY_BTC, + CURRENCY_EUR, + CURRENCY_USD, + DEFAULT_NAME, + DEVICE_STATUS_INACTIVE, + ICON_CURRENCY_BTC, + ICON_CURRENCY_EUR, + ICON_CURRENCY_USD, + ICON_TEMPERATURE, +) + +ATTRIBUTION = "Data provided by NiceHash" +FORMAT_DATETIME = "%d-%m-%Y %H:%M" + +_LOGGER = logging.getLogger(__name__) + + +class NiceHashBalanceSensor(Entity): + """NiceHash Account Balance Sensor""" + + def __init__( + self, + coordinator, + organization_id, + currency, + balance_type=BALANCE_TYPE_AVAILABLE, + ): + """Initialize the sensor""" + _LOGGER.debug(f"Account Balance Sensor: {balance_type} {currency}") + self.coordinator = coordinator + self.currency = currency + self.organization_id = organization_id + self.balance_type = balance_type + self._available = 0.00 + self._pending = 0.00 + self._total_balance = 0.00 + self._exchange_rate = 0.00 + + @property + def name(self): + """Sensor name""" + balance_type = self.balance_type[0].upper() + self.balance_type[1:] + return f"{DEFAULT_NAME} {balance_type} Account Balance {self.currency}" + + @property + def unique_id(self): + """Unique entity id""" + return f"{self.organization_id}:{self.currency}:{self.balance_type}" + + @property + def should_poll(self): + """No need to pool, Coordinator notifies entity of updates""" + return False + + @property + def available(self): + """Whether sensor is available""" + return self.coordinator.last_update_success + + @property + def state(self): + """Sensor state""" + accounts = self.coordinator.data.get("accounts") + total = accounts.get("total") + self._pending = float(total.get("pending")) + self._available = float(total.get("available")) + self._total_balance = float(total.get("totalBalance")) + + if self.balance_type == BALANCE_TYPE_TOTAL: + balance = self._total_balance + elif self.balance_type == BALANCE_TYPE_PENDING: + balance = self._pending + else: + balance = self._available + + if self.currency == CURRENCY_BTC: + return balance + else: + exchange_rates = self.coordinator.data.get("exchange_rates") + self._exchange_rate = exchange_rates.get(f"{CURRENCY_BTC}-{self.currency}") + return round(balance * self._exchange_rate, 2) + + @property + def icon(self): + """Sensor icon""" + if self.currency == CURRENCY_EUR: + return ICON_CURRENCY_EUR + elif self.currency == CURRENCY_USD: + return ICON_CURRENCY_USD + return ICON_CURRENCY_BTC + + @property + def unit_of_measurement(self): + """Sensor unit of measurement""" + return self.currency + + @property + def device_state_attributes(self): + """Sensor device state attributes""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "total": self._total_balance, + "available": self._available, + "pending": self._pending, + "exchange_rate": self._exchange_rate, + } + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Update entity""" + await self.coordinator.async_request_refresh() + + +class NiceHashRigTemperatureSensor(Entity): + """Displays highest temperature of active mining rig devices""" + + def __init__(self, coordinator, rig): + """Initialize the sensor""" + self.coordinator = coordinator + self._rig_id = rig["rigId"] + self._name = rig["name"] + self._temps = [] + self._num_devices = 0 + self._num_active_devices = 0 + _LOGGER.debug(f"Mining Rig Temperature Sensor: {self._name} ({self._rig_id})") + + @property + def name(self): + """Sensor name""" + return self._name + + @property + def unique_id(self): + """Unique entity id""" + return self._rig_id + + @property + def should_poll(self): + """No need to pool, Coordinator notifies entity of updates""" + return False + + @property + def available(self): + """Whether sensor is available""" + return self.coordinator.last_update_success + + @property + def state(self): + """Sensor state""" + mining_rigs = self.coordinator.data.get("miningRigs") + try: + rig_data = mining_rigs.get(self._rig_id) + devices = rig_data.get("devices") + highest_temp = 0 + self._temps = [] + self._num_devices = len(devices) + + if self._num_devices > 0: + _LOGGER.debug(f"{self._name}: Found {self._num_devices} devices") + for device in devices: + status = device.get("status").get("enumName") + # Ignore inactive devices + if status == DEVICE_STATUS_INACTIVE: + continue + temp = int(device.get("temperature")) + self._temps.append(temp) + if temp > highest_temp: + highest_temp = temp + + self._num_active_devices = len(self._temps) + return highest_temp + else: + _LOGGER.debug(f"{self._name}: No devices found") + self._num_devices = 0 + return 0 + except Exception as e: + _LOGGER.error(f"Unable to get mining rig {self._rig_id}\n{e}") + return 0 + + @property + def icon(self): + """Sensor icon""" + return ICON_TEMPERATURE + + @property + def unit_of_measurement(self): + """Sensor unit of measurement""" + # Not Celsius because then HA might convert to Fahrenheit + return "C" + + @property + def device_state_attributes(self): + """Sensor device state attributes""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + "temperatures": self._temps, + "active_devices": self._num_active_devices, + "total_devices": self._num_devices, + } + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self): + """Update entity""" + await self.coordinator.async_request_refresh()