feat: add platform integration
- add configuration support - add english config translations for later - add account balance sensor - add mining rig temperature sensors
This commit is contained in:
parent
b0d1102f80
commit
fe8f8233a8
69
custom_components/nicehash/__init__.py
Normal file
69
custom_components/nicehash/__init__.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Integrates NiceHash with Home Assistant
|
||||
|
||||
For more details about this integration, please refer to
|
||||
https://github.com/brianberg/ha-nicehash
|
||||
"""
|
||||
import asyncio
|
||||
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.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import (
|
||||
CONF_API_KEY,
|
||||
CONF_API_SECRET,
|
||||
CONF_CURRENCY,
|
||||
CONF_ORGANIZATION_ID,
|
||||
CURRENCY_BTC,
|
||||
DOMAIN,
|
||||
STARTUP_MESSAGE,
|
||||
)
|
||||
from .nicehash import NiceHashPrivateClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ORGANIZATION_ID): cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_SECRET): cv.string,
|
||||
vol.Required(CONF_CURRENCY, default=CURRENCY_BTC): cv.string,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config):
|
||||
"""Set up this integration"""
|
||||
if hass.data.get(DOMAIN) is None:
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
_LOGGER.debug(STARTUP_MESSAGE)
|
||||
|
||||
nicehash_config = config[DOMAIN]
|
||||
organization_id = nicehash_config.get(CONF_ORGANIZATION_ID)
|
||||
api_key = nicehash_config.get(CONF_API_KEY)
|
||||
api_secret = nicehash_config.get(CONF_API_SECRET)
|
||||
currency = nicehash_config.get(CONF_CURRENCY)
|
||||
|
||||
client = NiceHashPrivateClient(organization_id, api_key, api_secret)
|
||||
|
||||
try:
|
||||
await client.get_accounts()
|
||||
hass.data[DOMAIN]["client"] = client
|
||||
hass.data[DOMAIN]["currency"] = currency
|
||||
await discovery.async_load_platform(hass, "sensor", DOMAIN, {}, config)
|
||||
return True
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Unable to access NiceHash accounts\n{err}")
|
||||
return False
|
49
custom_components/nicehash/const.py
Normal file
49
custom_components/nicehash/const.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""
|
||||
Constants for NiceHash
|
||||
"""
|
||||
# Base component constants
|
||||
NAME = "NiceHash"
|
||||
DOMAIN = "nicehash"
|
||||
DOMAIN_DATA = f"{DOMAIN}_data"
|
||||
VERSION = "0.1.0"
|
||||
|
||||
ISSUE_URL = "https://github.com/brianberg/ha-nicehash/issues"
|
||||
|
||||
# Icons
|
||||
ICON = "mdi:pickaxe"
|
||||
ICON_CURRENCY_USD = "mdi:currency-usd"
|
||||
ICON_CURRENCY_BTC = "mdi:currency-btc"
|
||||
ICON_TEMPERATURE = "mdi:thermometer"
|
||||
|
||||
# Platforms
|
||||
SENSOR = "sensor"
|
||||
PLATFORMS = [SENSOR]
|
||||
|
||||
|
||||
# Configuration and options
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_API_KEY = "api_key"
|
||||
CONF_API_SECRET = "api_secret"
|
||||
CONF_ORGANIZATION_ID = "organization_id"
|
||||
CONF_CURRENCY = "currency"
|
||||
|
||||
|
||||
# Defaults
|
||||
DEFAULT_NAME = NAME
|
||||
|
||||
# Startup
|
||||
STARTUP_MESSAGE = f"""
|
||||
-------------------------------------------------------------------
|
||||
{NAME}
|
||||
Version: {VERSION}
|
||||
This is a custom integration!
|
||||
If you have any issues with this you need to open an issue here:
|
||||
{ISSUE_URL}
|
||||
-------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
# NiceHash
|
||||
NICEHASH_API_URL = "https://api2.nicehash.com"
|
||||
CURRENCY_BTC = "BTC"
|
||||
CURRENCY_USD = "USD"
|
||||
CURRENCY_EUR = "EUR"
|
13
custom_components/nicehash/manifest.json
Normal file
13
custom_components/nicehash/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "nicehash",
|
||||
"name": "NiceHash",
|
||||
"documentation": "https://github.com/brianberg/ha-nicehash",
|
||||
"issue_tracker": "https://github.com/brianberg/ha-nicehash/issues",
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@brianberg"
|
||||
],
|
||||
"requirements": [
|
||||
"requests_async"
|
||||
]
|
||||
}
|
110
custom_components/nicehash/nicehash.py
Normal file
110
custom_components/nicehash/nicehash.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""
|
||||
NiceHash API interface
|
||||
|
||||
https://github.com/nicehash/rest-clients-demo/blob/master/python/nicehash.py
|
||||
"""
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
import hmac
|
||||
import json
|
||||
import requests_async as requests
|
||||
import sys
|
||||
from time import mktime
|
||||
import uuid
|
||||
|
||||
from .const import NICEHASH_API_URL
|
||||
|
||||
|
||||
class NiceHashPublicClient:
|
||||
async def get_exchange_rates(self):
|
||||
exchange_data = await self.request("GET", "/main/api/v2/exchangeRate/list")
|
||||
return exchange_data["list"]
|
||||
|
||||
async def request(self, method, path, query=None, body=None):
|
||||
url = NICEHASH_API_URL + path
|
||||
|
||||
if query is not None:
|
||||
url += f"?{query}"
|
||||
|
||||
async with requests.Session() as session:
|
||||
if body:
|
||||
data = json.dumps(body)
|
||||
response = await session.request(method, url, data=data)
|
||||
else:
|
||||
response = await session.request(method, url)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
err_messages = [str(response.status_code), response.reason]
|
||||
if response.content:
|
||||
err_messages.append(str(response.content))
|
||||
raise Exception(": ".join(err_messages))
|
||||
|
||||
|
||||
class NiceHashPrivateClient:
|
||||
def __init__(self, organization_id, key, secret, verbose=False):
|
||||
self.organization_id = organization_id
|
||||
self.key = key
|
||||
self.secret = secret
|
||||
self.verbose = verbose
|
||||
|
||||
async def get_accounts(self):
|
||||
return await self.request("GET", "/main/api/v2/accounting/accounts2")
|
||||
|
||||
async def get_mining_rigs(self):
|
||||
return await self.request("GET", "/main/api/v2/mining/rigs2")
|
||||
|
||||
async def get_mining_rig(self, rig_id):
|
||||
return await self.request("GET", f"/main/api/v2/mining/rig2/{rig_id}")
|
||||
|
||||
async def request(self, method, path, query="", body=None):
|
||||
xtime = self.get_epoch_ms_from_now()
|
||||
xnonce = str(uuid.uuid4())
|
||||
|
||||
message = f"{self.key}\00{str(xtime)}\00{xnonce}\00\00{self.organization_id}\00\00{method}\00{path}\00{query}"
|
||||
|
||||
data = None
|
||||
if body:
|
||||
data = json.dumps(body)
|
||||
message += f"\00{data}"
|
||||
|
||||
digest = hmac.new(self.secret.encode(), message.encode(), sha256).hexdigest()
|
||||
xauth = f"{self.key}:{digest}"
|
||||
|
||||
headers = {
|
||||
"X-Time": str(xtime),
|
||||
"X-Nonce": xnonce,
|
||||
"X-Auth": xauth,
|
||||
"Content-Type": "application/json",
|
||||
"X-Organization-Id": self.organization_id,
|
||||
"X-Request-Id": str(uuid.uuid4()),
|
||||
}
|
||||
|
||||
async with requests.Session() as session:
|
||||
session.headers = headers
|
||||
|
||||
url = NICEHASH_API_URL + path
|
||||
if query:
|
||||
url += "?" + query
|
||||
|
||||
if self.verbose:
|
||||
print(method, url)
|
||||
|
||||
if data:
|
||||
response = await session.request(method, url, data=data)
|
||||
else:
|
||||
response = await session.request(method, url)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
err_messages = [str(response.status_code), response.reason]
|
||||
if response.content:
|
||||
err_messages.append(str(response.content))
|
||||
raise Exception(": ".join(err_messages))
|
||||
|
||||
def get_epoch_ms_from_now(self):
|
||||
now = datetime.now()
|
||||
now_ec_since_epoch = mktime(now.timetuple()) + now.microsecond / 1000000.0
|
||||
return int(now_ec_since_epoch * 1000)
|
192
custom_components/nicehash/sensor.py
Normal file
192
custom_components/nicehash/sensor.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""
|
||||
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.core import Config, HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
CURRENCY_BTC,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
ICON_CURRENCY_BTC,
|
||||
ICON_TEMPERATURE,
|
||||
)
|
||||
from .nicehash import NiceHashPrivateClient, NiceHashPublicClient
|
||||
|
||||
FORMAT_DATETIME = "%d-%m-%Y %H:%M"
|
||||
SCAN_INTERVAL_RIGS = timedelta(minutes=1)
|
||||
SCAN_INTERVAL_ACCOUNTS = timedelta(minutes=60)
|
||||
|
||||
_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 sensor components")
|
||||
|
||||
client = hass.data[DOMAIN]["client"]
|
||||
currency = hass.data[DOMAIN]["currency"]
|
||||
|
||||
# Add account balance sensor
|
||||
async_add_entities(
|
||||
[NiceHashBalanceSensor(client, currency, SCAN_INTERVAL_ACCOUNTS)], True
|
||||
)
|
||||
|
||||
# Add mining rig sensors
|
||||
rig_data = await client.get_mining_rigs()
|
||||
mining_rigs = rig_data["miningRigs"]
|
||||
# Add temperature sensors
|
||||
async_add_entities(
|
||||
[
|
||||
NiceHashRigTemperatureSensor(client, rig, SCAN_INTERVAL_RIGS)
|
||||
for rig in mining_rigs
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class NiceHashBalanceSensor(Entity):
|
||||
"""NiceHash Account Balance Sensor"""
|
||||
|
||||
def __init__(self, client, currency, update_frequency):
|
||||
"""Initialize the sensor"""
|
||||
_LOGGER.debug(f"Account Balance Sensor: {currency}")
|
||||
self._client = client
|
||||
self._public_client = NiceHashPublicClient()
|
||||
self._currency = currency
|
||||
self._state = None
|
||||
self._last_update = None
|
||||
self.async_update = Throttle(update_frequency)(self._async_update)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Sensor name"""
|
||||
return f"{DEFAULT_NAME} Account Balance"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Unique entity id"""
|
||||
return f"{self._client.organization_id}:{self._currency}"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Sensor icon"""
|
||||
return ICON_CURRENCY_BTC
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Sensor state"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Sensor unit of measurement"""
|
||||
return self._currency
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Sensor device attributes"""
|
||||
return {"last_update": self._last_update}
|
||||
|
||||
async def _async_update(self):
|
||||
|
||||
try:
|
||||
account_data = await self._client.get_accounts()
|
||||
available = float(account_data["total"]["available"])
|
||||
|
||||
if self._currency == CURRENCY_BTC:
|
||||
# Account balance is in BTC
|
||||
self._state = available
|
||||
else:
|
||||
# Convert to selected currency via exchange rates
|
||||
exchange_rates = await self._public_client.get_exchange_rates()
|
||||
self._last_update = datetime.today().strftime(FORMAT_DATETIME)
|
||||
for rate in exchange_rates:
|
||||
isBTC = rate["fromCurrency"] == CURRENCY_BTC
|
||||
toCurrency = rate["toCurrency"]
|
||||
if isBTC and toCurrency == self._currency:
|
||||
rate = float(rate["exchangeRate"])
|
||||
self._state = round(available * rate, 2)
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Unable to get account balance\n{err}")
|
||||
pass
|
||||
|
||||
|
||||
class NiceHashRigTemperatureSensor(Entity):
|
||||
"""NichHash Mining Rig Temperature Sensor"""
|
||||
|
||||
def __init__(self, client, rig, update_frequency):
|
||||
"""Initialize the sensor"""
|
||||
self._client = client
|
||||
self._rig_id = rig["rigId"]
|
||||
self._name = rig["name"]
|
||||
_LOGGER.debug(f"Mining Rig Temperature Sensor: {self._name} ({self._rig_id})")
|
||||
self._state = None
|
||||
self._last_update = None
|
||||
self.async_update = Throttle(update_frequency)(self._async_update)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Sensor name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Unique entity id"""
|
||||
return self._rig_id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Sensor state"""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Sensor icon"""
|
||||
return ICON_TEMPERATURE
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Sensor unit of measurement"""
|
||||
return "C"
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Sensore device state attributes"""
|
||||
return {
|
||||
"last_update": self._last_update,
|
||||
}
|
||||
|
||||
async def _async_update(self):
|
||||
try:
|
||||
data = await self._client.get_mining_rig(self._rig_id)
|
||||
self._last_update = datetime.today().strftime(FORMAT_DATETIME)
|
||||
devices = data["devices"]
|
||||
highest_temp = 0
|
||||
|
||||
if len(devices) > 0:
|
||||
_LOGGER.debug(f"{self._name}: Found {len(devices)} devices")
|
||||
for device in devices:
|
||||
temp = int(device["temperature"])
|
||||
if temp < 0:
|
||||
# Ignore inactive devices
|
||||
continue
|
||||
if temp > highest_temp:
|
||||
highest_temp = temp
|
||||
self._state = highest_temp
|
||||
else:
|
||||
_LOGGER.debug(f"{self._name}: No devices found")
|
||||
self._state = None
|
||||
|
||||
except Exception as err:
|
||||
_LOGGER.error(f"Unable to get mining rig {self._rig_id}\n{err}")
|
||||
pass
|
32
custom_components/nicehash/translations/en.json
Normal file
32
custom_components/nicehash/translations/en.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "NiceHash",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "NiceHash",
|
||||
"description": "If you need help with the configuration have a look here: https://github.com/brianberg/ha-nicehash",
|
||||
"data": {
|
||||
"organization_id": "Organization ID",
|
||||
"api_key": "API Key Code",
|
||||
"api_secret": "API Secret Key Code"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"auth": "Credentials are invalid",
|
||||
"name_exists": "Configuration already exists"
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "Only one instance of NiceHash is allowed"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"sensor": "Sensor enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
info.md
19
info.md
@ -20,13 +20,26 @@ Platform | Description
|
||||
{% if not installed %}
|
||||
## Installation
|
||||
|
||||
1. Click install
|
||||
1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "NiceHash"
|
||||
<!-- 1. Click install
|
||||
1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "NiceHash" -->
|
||||
|
||||
1. Generate [NiceHash][nicehash] API key
|
||||
- Supported API Permissions
|
||||
- Wallet Permissions > View balances...
|
||||
- Mining Permissions > View mining data...
|
||||
- See this [repository](https://github.com/nicehash/rest-clients-demo) for assistance
|
||||
1. Add `nicehash` to `configuration.yaml`
|
||||
```
|
||||
nicehash:
|
||||
organization_id: <org_id>
|
||||
api_key: <api_key_code>
|
||||
api_secret: <api_secret_key_code>
|
||||
```
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
## Configuration is done in the UI
|
||||
<!-- ## Configuration is done in the UI -->
|
||||
|
||||
<!---->
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user