Compare commits

...

10 Commits

Author SHA1 Message Date
Cathal O Cuimin
a7a91c9dfe
Add unit of measurement for rig speed 2023-07-03 17:52:53 +02:00
Cathal O Cuimin
6b85fe5e70
Add support for all currencies 2023-07-03 17:52:43 +02:00
Evan Lawrence
0e94842c9d
Added switches for each device which allow for enabling/disabling mining.
very basic, needs testing.

* To do: Set switch to reflect the current state on init. (Currently defaults to off after HA reboot.)
* Occasionally the custom component fails to load on HA start.  Usually fixed by restarting HA.
* When switches are toggled occasionally a false error is reported.  This could be due to async call.  The desired behavior still occurs though.
2023-07-03 17:51:08 +02:00
Newlance
bbe51fef8d
Update README.md 2023-07-03 17:47:22 +02:00
Evan Lawrence
991755ee4e
- Device switch status now properly reflects status of mining device (there is a delay for the status to update into Home Assistant)
- Added property to switches which contains the last response from the API.  This will reflect "Success!" or otherwise the error message that was provided by the API.
- Added additional properties 'status' and 'device' to switches.
2023-07-03 17:47:11 +02:00
Pascal Berski
2aca201d3b
added icon power 2023-07-03 17:45:11 +02:00
Pascal Berski
53f35f04ea
added class DevicePowerSensor 2023-07-03 17:45:03 +02:00
Pascal Berski
829d1cd3f7
added DevicePowerSensor (sensor.py / nicehash.py) 2023-07-03 17:44:54 +02:00
Pascal Berski
dde8ba7417
Update README.md 2023-07-03 17:44:45 +02:00
Pascal Berski
454ee1008d
Update README.md 2023-07-03 17:43:03 +02:00
11 changed files with 273 additions and 6 deletions

View File

@ -28,8 +28,10 @@ A [Home Assistant][homeassistant] integration that creates a collection of [Nice
- Algorithm - Algorithm
- Speed - Speed
- Temperature - Temperature
- HotSpot Temperature
- Load - Load
- RPM - RPM
- Power
- Most Recent Mining Payout - Most Recent Mining Payout
None of the sensors are added by default. See installation instructions for available configuration options. None of the sensors are added by default. See installation instructions for available configuration options.
@ -46,6 +48,8 @@ Supported API permissions and associated sensors
- Account balance sensors - Account balance sensors
- Mining Permissions > View mining data... - Mining Permissions > View mining data...
- Rig, device, and payout sensors - Rig, device, and payout sensors
- Mining Permissions > Manage Rigs
- Device start/stop switch
See this [repository](https://github.com/nicehash/rest-clients-demo) for further assistance generating an API key. See this [repository](https://github.com/nicehash/rest-clients-demo) for further assistance generating an API key.

View File

@ -118,6 +118,8 @@ async def async_setup(hass: HomeAssistant, config: Config):
hass.data[DOMAIN]["rigs_coordinator"] = rigs_coordinator hass.data[DOMAIN]["rigs_coordinator"] = rigs_coordinator
await discovery.async_load_platform(hass, "switch", DOMAIN, {}, config)
await discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config) await discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config)
return True return True

View File

@ -102,8 +102,11 @@ class BalanceSensor(Entity):
return ICON_CURRENCY_EUR return ICON_CURRENCY_EUR
elif self.currency == CURRENCY_USD: elif self.currency == CURRENCY_USD:
return ICON_CURRENCY_USD return ICON_CURRENCY_USD
elif self.currency == CURRENCY_BTC:
return ICON_CURRENCY_BTC return ICON_CURRENCY_BTC
return ICON_CURRENCY_USD
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Sensor unit of measurement""" """Sensor unit of measurement"""

View File

@ -19,6 +19,7 @@ ICON_PICKAXE = "mdi:pickaxe"
ICON_PULSE = "mdi:pulse" ICON_PULSE = "mdi:pulse"
ICON_THERMOMETER = "mdi:thermometer" ICON_THERMOMETER = "mdi:thermometer"
ICON_SPEEDOMETER = "mdi:speedometer" ICON_SPEEDOMETER = "mdi:speedometer"
ICON_POWER = "mdi:power-plug"
# Platforms # Platforms
SENSOR = "sensor" SENSOR = "sensor"
@ -57,6 +58,23 @@ NICEHASH_ATTRIBUTION = "Data provided by NiceHash"
CURRENCY_BTC = "BTC" CURRENCY_BTC = "BTC"
CURRENCY_USD = "USD" CURRENCY_USD = "USD"
CURRENCY_EUR = "EUR" CURRENCY_EUR = "EUR"
SUPPORTED_CURRENCIES = [
"ERN","HKD","GGP","RSD","SHP","USD","MYR","PYG","RON","DOP","TWD","AWG",
"CVE","BND","RUB","NGN","XCD","JEP","ZWL","HNL","NZD","AFN","MUR","DKK",
"CNY","JOD","CHF","COP","XAF","XAG","ZMK","GNF","ZMW","GIP","BTC","MKD",
"WST","IDR","IQD","BHD","YER","MAD","KGS","PHP","PEN","BMD","DJF","MVR",
"QAR","JPY","SCR","IMP","KRW","HRK","SOS","VUV","NIO","KYD","LAK","ISK",
"BOB","IRR","NPR","EGP","BBD","CAD","XAU","CUP","SDG","PKR","UZS","CLF",
"CUC","STD","MGA","FJD","DZD","TJS","EURKM","SZL","THB","SRD","BDT",
"BTN","CZK","AMD","UGX","TRY","AUD","UAH","HUF","SLL","VND","RWF","LBP",
"ANG","SAR","LVL","KHR","BYR","TTD","OMR","LTL","GTQ","ALL","MRO","MWK",
"LSL","SBD","BGN","LRD","JMD","CRC","ETB","NAD","GYD","LKR","INR","SEK",
"KES","KMF","VEF","ARS","HTG","BAM","BWP","GEL","KZT","AED","KWD","XDR",
"EUR","TND","MDL","LYD","BSD","GHS","MOP","PAB","ZAR","AZN","TOP","SVC",
"KPW","TMT","BZD","GMD","XOF","UYU","MNT","NOK","XPF","BIF","BYN","FKP",
"GBP","MXN","SYP","PGK","MZN","PLN","MMK","SGD","AOA","ILS","CLP","TZS",
"CDF","BRL"
]
# Balance type # Balance type
BALANCE_TYPE_AVAILABLE = "available" BALANCE_TYPE_AVAILABLE = "available"
BALANCE_TYPE_PENDING = "pending" BALANCE_TYPE_PENDING = "pending"

View File

@ -0,0 +1,89 @@
"""
NiceHash Rig controls
"""
from datetime import datetime
import logging
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity
from homeassistant.components.switch import SwitchEntity
from .coordinators import MiningRigsDataUpdateCoordinator
from .nicehash import MiningRig, MiningRigDevice, NiceHashPrivateClient
from .const import DOMAIN, NICEHASH_ATTRIBUTION
import asyncio
class DeviceSwitch(SwitchEntity):
def __init__(
self,
coordinator: MiningRigsDataUpdateCoordinator,
rig: MiningRig,
device: MiningRigDevice,
client: NiceHashPrivateClient,
):
"""Initialize the switch"""
self.coordinator = coordinator
self._rig_id = rig.id
self._rig_name = rig.name
self._device_id = device.id
self._device_name = device.name
self._client = client
self._status = device.status
def _get_device(self):
try:
mining_rigs = self.coordinator.data.get("miningRigs")
rig = MiningRig(mining_rigs.get(self._rig_id))
return rig.devices.get(self._device_id)
except Exception as e:
_LOGGER.error(f"Unable to get mining device ({self._device_id})\n{e}")
class GPUSwitch(DeviceSwitch):
_is_on = False
_last_response = "N/A"
@property
def name(self):
"""switch name"""
return f"{self._device_name} Switch"
@property
def unique_id(self):
"""Unique entity id"""
return f"{self._device_id}:switch"
@property
def is_on(self):
device = self._get_device()
if device.status == "Mining":
self._is_on = True
elif device:
self._is_on = False
else:
self._is_on = "unavailable"
return self._is_on
@property
def device_state_attributes(self):
"""Sensor device state attributes"""
return {
ATTR_ATTRIBUTION: NICEHASH_ATTRIBUTION,
"status": self._status,
"device": self._device_name,
"last_response": self._last_response,
}
def turn_on(self, **kwargs):
"""Turn the switch on."""
self._is_on = True
response = asyncio.run(self._client.toggle_device(self._device_id, "START", self._rig_id))
self._last_response = "Success!" if response["success"] else response["message"]
def turn_off(self, **kwargs):
"""Turn the switch off."""
self._is_on = False
response = asyncio.run(self._client.toggle_device(self._device_id, "STOP", self._rig_id))
self._last_response = "Success!" if response["success"] else response["message"]

View File

@ -12,6 +12,7 @@ from homeassistant.helpers.update_coordinator import (
from .const import ( from .const import (
CURRENCY_BTC, CURRENCY_BTC,
CURRENCY_USD,
DOMAIN, DOMAIN,
) )
from .nicehash import NiceHashPrivateClient, NiceHashPublicClient from .nicehash import NiceHashPrivateClient, NiceHashPublicClient
@ -41,13 +42,25 @@ class AccountsDataUpdateCoordinator(DataUpdateCoordinator):
accounts = await self._client.get_accounts() accounts = await self._client.get_accounts()
exchange_rates = await NiceHashPublicClient().get_exchange_rates() exchange_rates = await NiceHashPublicClient().get_exchange_rates()
rates_dict = dict() rates_dict = dict()
# NiceHash supports BTC->USD and BTC->EUR only.
for rate in exchange_rates: for rate in exchange_rates:
fromCurrency = rate.get("fromCurrency") fromCurrency = rate.get("fromCurrency")
# Only care about the Bitcoin exchange rates
if fromCurrency == CURRENCY_BTC: if fromCurrency == CURRENCY_BTC:
toCurrency = rate.get("toCurrency") toCurrency = rate.get("toCurrency")
exchange_rate = float(rate.get("exchangeRate")) exchange_rate = float(rate.get("exchangeRate"))
rates_dict[f"{fromCurrency}-{toCurrency}"] = exchange_rate rates_dict[f"{fromCurrency}-{toCurrency}"] = exchange_rate
# For non-USD/EUR currencies, get exchange rate using USD as an intermediary. e.g. BTC->USD->CAD
btc_to_usd_rate = rates_dict[f"BTC-USD"]
for rate in exchange_rates:
fromCurrency = rate.get("fromCurrency")
if fromCurrency == CURRENCY_USD:
toCurrency = rate.get("toCurrency")
exchange_rate = float(rate.get("exchangeRate")) * btc_to_usd_rate
if f"{CURRENCY_BTC}-{toCurrency}" not in rates_dict:
rates_dict[f"{CURRENCY_BTC}-{toCurrency}"] = exchange_rate
return { return {
"accounts": accounts, "accounts": accounts,
"exchange_rates": rates_dict, "exchange_rates": rates_dict,

View File

@ -17,6 +17,7 @@ from .const import (
ICON_PULSE, ICON_PULSE,
ICON_THERMOMETER, ICON_THERMOMETER,
ICON_SPEEDOMETER, ICON_SPEEDOMETER,
ICON_POWER,
NICEHASH_ATTRIBUTION, NICEHASH_ATTRIBUTION,
) )
from .coordinators import MiningRigsDataUpdateCoordinator from .coordinators import MiningRigsDataUpdateCoordinator
@ -381,3 +382,48 @@ class DeviceRPMSensor(DeviceSensor):
"rig": self._rig_name, "rig": self._rig_name,
} }
class DevicePowerSensor(DeviceSensor):
"""
Displays power of a mining rig device
"""
@property
def name(self):
"""Sensor name"""
return f"{self._device_name} Power"
@property
def unique_id(self):
"""Unique entity id"""
return f"{self._device_id}:power"
@property
def state(self):
"""Sensor state"""
device = self._get_device()
if device:
self._power = device.power
else:
self._power = 0
return self._power
@property
def icon(self):
"""Sensor icon"""
return ICON_POWER
@property
def unit_of_measurement(self):
"""Sensor unit of measurement"""
return "W"
@property
def device_state_attributes(self):
"""Sensor device state attributes"""
return {
ATTR_ATTRIBUTION: NICEHASH_ATTRIBUTION,
"power": self._power,
"rig": self._rig_name,
}

View File

@ -49,6 +49,7 @@ class MiningRigDevice:
self.load = float(data.get("load")) self.load = float(data.get("load"))
self.rpm = float(data.get("revolutionsPerMinute")) self.rpm = float(data.get("revolutionsPerMinute"))
self.speeds = data.get("speeds") self.speeds = data.get("speeds")
self.power = float(data.get("powerUsage"))
class MiningRig: class MiningRig:
@ -156,6 +157,13 @@ class NiceHashPrivateClient:
query = f"size={size}" query = f"size={size}"
return await self.request("GET", "/main/api/v2/mining/rigs/payouts", query) return await self.request("GET", "/main/api/v2/mining/rigs/payouts", query)
async def toggle_device(self, device_id, action, rig_id):
query = ""
body = {"deviceId":device_id,
"action":action,
"rigId":rig_id}
return await self.request("POST", "/main/api/v2/mining/rigs/status2", query, body)
async def request(self, method, path, query="", body=None): async def request(self, method, path, query="", body=None):
xtime = self.get_epoch_ms_from_now() xtime = self.get_epoch_ms_from_now()
xnonce = str(uuid.uuid4()) xnonce = str(uuid.uuid4())

View File

@ -378,7 +378,7 @@ class RigSpeedSensor(RigSensor):
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Sensor unit of measurement""" """Sensor unit of measurement"""
return None return f"{self._unit}"
@property @property
def device_state_attributes(self): def device_state_attributes(self):

View File

@ -20,6 +20,7 @@ from .const import (
DEVICE_RPM, DEVICE_RPM,
DEVICE_SPEED_RATE, DEVICE_SPEED_RATE,
DEVICE_SPEED_ALGORITHM, DEVICE_SPEED_ALGORITHM,
SUPPORTED_CURRENCIES,
) )
from .nicehash import ( from .nicehash import (
MiningRig, MiningRig,
@ -39,6 +40,7 @@ from .rig_sensors import (
) )
from .device_sensors import ( from .device_sensors import (
DeviceAlgorithmSensor, DeviceAlgorithmSensor,
DevicePowerSensor,
DeviceSpeedSensor, DeviceSpeedSensor,
DeviceStatusSensor, DeviceStatusSensor,
DeviceLoadSensor, DeviceLoadSensor,
@ -121,7 +123,7 @@ def create_balance_sensors(organization_id, currency, coordinator):
balance_type=BALANCE_TYPE_TOTAL, balance_type=BALANCE_TYPE_TOTAL,
), ),
] ]
if currency == CURRENCY_USD or currency == CURRENCY_EUR: if currency in SUPPORTED_CURRENCIES:
_LOGGER.debug(f"Creating {currency} account balance sensors") _LOGGER.debug(f"Creating {currency} account balance sensors")
balance_sensors.append( balance_sensors.append(
BalanceSensor( BalanceSensor(
@ -148,8 +150,7 @@ def create_balance_sensors(organization_id, currency, coordinator):
) )
) )
else: else:
_LOGGER.warn("Invalid currency: must be EUR or USD") _LOGGER.warn("Currency is invalid.")
return balance_sensors return balance_sensors
@ -192,5 +193,6 @@ def create_device_sensors(mining_rigs, coordinator):
device_sensors.append(DeviceTemperatureSensor(coordinator, rig, device)) device_sensors.append(DeviceTemperatureSensor(coordinator, rig, device))
device_sensors.append(DeviceLoadSensor(coordinator, rig, device)) device_sensors.append(DeviceLoadSensor(coordinator, rig, device))
device_sensors.append(DeviceRPMSensor(coordinator, rig, device)) device_sensors.append(DeviceRPMSensor(coordinator, rig, device))
device_sensors.append(DevicePowerSensor(coordinator, rig, device))
return device_sensors return device_sensors

View File

@ -0,0 +1,82 @@
"""
Sensor platform for NiceHash
"""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import Config, HomeAssistant
from .const import (
BALANCE_TYPE_AVAILABLE,
BALANCE_TYPE_PENDING,
BALANCE_TYPE_TOTAL,
CURRENCY_BTC,
CURRENCY_EUR,
CURRENCY_USD,
DOMAIN,
DEVICE_LOAD,
DEVICE_RPM,
DEVICE_SPEED_RATE,
DEVICE_SPEED_ALGORITHM,
)
from .nicehash import (
MiningRig,
MiningRigDevice,
NiceHashPrivateClient,
NiceHashPublicClient,
)
from .control_switches import (
GPUSwitch
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant, config: Config, async_add_entities, discovery_info=None
):
"""Setup NiceHash sensor platform"""
_LOGGER.debug("Creating new NiceHash switch components")
data = hass.data[DOMAIN]
# Configuration
organization_id = data.get("organization_id")
client = data.get("client")
# Options
currency = data.get("currency")
balances_enabled = data.get("balances_enabled")
payouts_enabled = data.get("payouts_enabled")
rigs_enabled = data.get("rigs_enabled")
devices_enabled = data.get("devices_enabled")
# Mining rig and device sensors
if rigs_enabled or devices_enabled:
rigs_coordinator = data.get("rigs_coordinator")
rig_data = await client.get_mining_rigs()
mining_rigs = rig_data.get("miningRigs")
_LOGGER.debug(f"Found {len(mining_rigs)} rigs")
if devices_enabled:
_LOGGER.debug("Device sensors enabled")
device_switches = create_device_switches(mining_rigs, rigs_coordinator,client)
async_add_entities(device_switches, True)
def create_device_switches(mining_rigs, coordinator, client):
device_switches = []
for rig_data in mining_rigs:
rig = MiningRig(rig_data)
devices = rig.devices.values()
_LOGGER.debug(
f"Found {len(devices)} device switches(s) for {rig.name} ({rig.id})"
)
for device in devices:
_LOGGER.debug(f"Creating {device.name} ({device.id}) switches")
device_switches.append(GPUSwitch(coordinator, rig, device, client))
return device_switches