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:
Brian Berg 2020-06-16 02:21:14 +00:00
parent b0d1102f80
commit fe8f8233a8
7 changed files with 481 additions and 3 deletions

View 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

View 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"

View 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"
]
}

View 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)

View 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

View 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
View File

@ -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 -->
<!---->