Skip to content

Commit

Permalink
Switch to async/aiohttp, add reload service
Browse files Browse the repository at this point in the history
  • Loading branch information
TheHolyRoger committed Apr 24, 2023
1 parent 913c8ae commit 4ab10aa
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 60 deletions.
3 changes: 2 additions & 1 deletion Version
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ Version: 0.2.0 20230422 - Huge refactor to add sub sensors and loads more chain
Version: 0.2.1 20230422 - Fix pool control unique IDs
Version: 0.2.2 20230423 - Allow list of pool prefixes for chain control sensor
Version: 0.2.3 20230423 - Add mempool.space stats
Version: 0.2.4 20230423 - Add calculated difficulty and mempool size child sensors
Version: 0.2.4 20230423 - Add calculated difficulty and mempool size child sensors
Version: 0.2.5 20230425 - Switch to asyncio platform setup/aiohttp, add reload service, halving sensors.
2 changes: 1 addition & 1 deletion custom_components/cryptoinfo/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.4"
__version__ = "0.2.5"
6 changes: 6 additions & 0 deletions custom_components/cryptoinfo/const/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import logging
from homeassistant.const import Platform

DOMAIN = "cryptoinfo"
PLATFORMS = [
Platform.SENSOR,
]

CONF_CRYPTOCURRENCY_NAME = "cryptocurrency_name"
CONF_CURRENCY_NAME = "currency_name"
Expand Down
119 changes: 67 additions & 52 deletions custom_components/cryptoinfo/crypto_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
Author: Johnny Visser
"""

import requests
import aiohttp
import asyncio
import async_timeout
import json
import time
import traceback
from datetime import datetime
Expand Down Expand Up @@ -83,6 +86,7 @@
from homeassistant.components.sensor import (
CONF_STATE_CLASS,
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import (
Expand All @@ -91,12 +95,12 @@
CONF_UNIT_OF_MEASUREMENT,
)
from homeassistant.exceptions import TemplateError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.template import Template
from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity


class CryptoinfoSensor(Entity):
class CryptoinfoSensor(SensorEntity):
def __init__(
self,
hass,
Expand All @@ -122,6 +126,7 @@ def __init__(
):
# Internal Properties
self.hass = hass
self._session = async_get_clientsession(hass) if hass is not None else None
self.data = None
self.cryptocurrency_name = cryptocurrency_name
self.currency_name = currency_name
Expand All @@ -143,7 +148,7 @@ def __init__(
self._child_sensor_config = extra_sensors

# HASS Attributes
self.update = Throttle(update_frequency)(self._update)
self.async_update = Throttle(update_frequency)(self._async_update)
self._attr_unique_id = unique_id if unique_id is not None and len(unique_id) else self._build_unique_id()
self._name = self._build_name()
self._state = None
Expand Down Expand Up @@ -477,7 +482,7 @@ def all_extra_sensor_keys(self):

@classmethod
def get_valid_extra_sensor_keys(cls):
empty_sensor = CryptoinfoSensor(*["" for x in range(7)])
empty_sensor = CryptoinfoSensor(None, *["" for x in range(6)])
keys = list(empty_sensor.all_extra_sensor_keys)
del empty_sensor

Expand Down Expand Up @@ -576,31 +581,34 @@ def check_valid_config(self, raise_error=True):

return True

def _log_api_error(self, error, r, tb):
def _log_api_error(self, error, tb):
_LOGGER.error(
"Cryptoinfo error fetching update: "
f"Cryptoinfo error fetching update for {self.name}: "
+ f"{type(error).__name__}: {error}"
+ " - response status: "
+ str(r.status_code if r is not None else None)
+ " - "
+ str(r.reason if r is not None else None)
)
_LOGGER.error(tb)

def _api_fetch(self, api_data, url, extract_data, extract_primary):
r = None
async def _async_api_fetch(self, api_data, url, extract_data, extract_primary, encoding="utf-8"):
try:
if api_data is None:
_LOGGER.warning(f"Fetching data for {self.name}")
r = requests.get(url=url)
api_data = extract_data(r.json())

async with async_timeout.timeout(30):
response = await self._session.get(url)
if response.status == 200:
resp_text = await response.text(encoding=encoding)
api_data = extract_data(json.loads(resp_text))
primary_data = extract_primary(api_data)
self.data = api_data

except asyncio.TimeoutError:
_LOGGER.error("Timeout fetching update for %s", self.name)
primary_data, api_data = None, None
except aiohttp.ClientError as err:
_LOGGER.error(
"Error fetching update for %s: %r", self.name, err
)
primary_data, api_data = None, None
except Exception as error:
tb = traceback.format_exc()
self._log_api_error(error, r, tb)
self._log_api_error(error, tb)
primary_data, api_data = None, None

return primary_data, api_data
Expand Down Expand Up @@ -699,11 +707,11 @@ def _extract_data_mempool_stats_full(self, json_data):
def _extract_data_mempool_stats_primary(self, api_data):
return int(api_data["vsize"])

def _fetch_price_data_main(self, api_data=None):
async def _fetch_price_data_main(self, api_data=None):
if not self._fetch_type == CryptoInfoDataFetchType.PRICE_MAIN:
raise ValueError()

price_data, api_data = self._api_fetch(
price_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_PRICE_MAIN.format(API_BASE_URL_COINGECKO, self.cryptocurrency_name, self.currency_name),
self._extract_data_price_main_full, self._extract_data_price_main_primary
Expand Down Expand Up @@ -733,11 +741,11 @@ def _fetch_price_data_main(self, api_data=None):

return self.data

def _fetch_price_data_alternate(self, api_data=None):
async def _fetch_price_data_alternate(self, api_data=None):
if self._fetch_type not in CryptoInfoEntityManager.instance().fetch_price_types:
raise ValueError()

price_data, api_data = self._api_fetch(
price_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_PRICE_ALT.format(API_BASE_URL_COINGECKO, self.cryptocurrency_name, self.currency_name),
self._extract_data_price_simple_full, self._extract_data_price_simple_primary
Expand All @@ -757,8 +765,8 @@ def _fetch_price_data_alternate(self, api_data=None):

return self.data

def _fetch_dominance(self, api_data=None):
dominance_data, api_data = self._api_fetch(
async def _fetch_dominance(self, api_data=None):
dominance_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_DOMINANCE.format(API_BASE_URL_COINGECKO),
self._extract_data_dominance_full,
Expand All @@ -776,12 +784,13 @@ def _fetch_dominance(self, api_data=None):

return self.data

def _fetch_chain_summary(self, api_data=None):
summary_data, api_data = self._api_fetch(
async def _fetch_chain_summary(self, api_data=None):
summary_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_CHAIN_SUMMARY.format(API_BASE_URL_CRYPTOID),
self._extract_data_chain_summary_full,
self._extract_data_chain_summary_primary
self._extract_data_chain_summary_primary,
encoding="latin-1"
)

if summary_data is not None:
Expand All @@ -797,12 +806,13 @@ def _fetch_chain_summary(self, api_data=None):

return self.data

def _fetch_chain_control(self, api_data=None):
control_data, api_data = self._api_fetch(
async def _fetch_chain_control(self, api_data=None):
control_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_CHAIN_CONTROL.format(API_BASE_URL_CRYPTOID, self.cryptocurrency_name),
self._extract_data_chain_control_full,
self._extract_data_chain_control_primary
self._extract_data_chain_control_primary,
encoding="latin-1"
)

if control_data is not None:
Expand All @@ -817,12 +827,13 @@ def _fetch_chain_control(self, api_data=None):

return self.data

def _fetch_chain_orphans(self, api_data=None):
orphans_data, api_data = self._api_fetch(
async def _fetch_chain_orphans(self, api_data=None):
orphans_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_CHAIN_ORPHANS.format(API_BASE_URL_CRYPTOID, self.cryptocurrency_name),
self._extract_data_chain_orphans_full,
self._extract_data_chain_orphans_primary
self._extract_data_chain_orphans_primary,
encoding="latin-1"
)

if orphans_data is not None:
Expand All @@ -835,7 +846,7 @@ def _fetch_chain_orphans(self, api_data=None):

return self.data

def _fetch_chain_block_time(self, api_data=None):
async def _fetch_chain_block_time(self, api_data=None):
(block_height_arg, ) = self._get_fetch_args()

try:
Expand All @@ -854,11 +865,12 @@ def _fetch_chain_block_time(self, api_data=None):
if self._state is not None and self._state > 0 and self._block_height == block_height:
api_data = self._state

block_time_data, api_data = self._api_fetch(
block_time_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_CHAIN_BLOCK_TIME.format(API_BASE_URL_CRYPTOID, self.cryptocurrency_name, block_height),
self._extract_data_chain_block_time_full,
self._extract_data_chain_block_time_primary
self._extract_data_chain_block_time_primary,
encoding="latin-1"
)

if block_time_data is not None:
Expand All @@ -872,10 +884,10 @@ def _fetch_chain_block_time(self, api_data=None):

return self.data

def _fetch_nomp_pool_stats(self, api_data=None):
async def _fetch_nomp_pool_stats(self, api_data=None):
self.check_valid_config()

hashrate_data, api_data = self._api_fetch(
hashrate_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_NOMP_POOL_STATS.format(self._api_domain_name),
self._extract_data_nomp_pool_stats_full,
Expand All @@ -899,10 +911,10 @@ def _fetch_nomp_pool_stats(self, api_data=None):

return self.data

def _fetch_mempool_stats(self, api_data=None):
async def _fetch_mempool_stats(self, api_data=None):
self.check_valid_config()

mempool_data, api_data = self._api_fetch(
mempool_data, api_data = await self._async_api_fetch(
api_data,
API_ENDPOINT_MEMPOOL_STATS.format(API_BASE_URL_MEMPOOLSPACE),
self._extract_data_mempool_stats_full,
Expand Down Expand Up @@ -936,7 +948,7 @@ def _render_fetch_args(self):
if args_compiled:
try:
args_to_render = {"arguments": args}
rendered_args = args_compiled.render(args_to_render)
rendered_args = args_compiled.async_render(args_to_render)
except TemplateError as ex:
_LOGGER.exception("Error rendering args template: %s", ex)
return
Expand Down Expand Up @@ -1077,40 +1089,40 @@ def _get_child_sensors(self):

return child_sensors

def _update(self):
async def _async_update(self):
api_data = None

if not CryptoInfoEntityManager.instance().should_fetch_entity(self):
api_data = CryptoInfoEntityManager.instance().fetch_cached_entity_data(self)

try:
if self._fetch_type == CryptoInfoDataFetchType.DOMINANCE:
api_data = self._fetch_dominance(api_data)
api_data = await self._fetch_dominance(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.CHAIN_SUMMARY:
api_data = self._fetch_chain_summary(api_data)
api_data = await self._fetch_chain_summary(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.CHAIN_CONTROL:
api_data = self._fetch_chain_control(api_data)
api_data = await self._fetch_chain_control(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.CHAIN_ORPHANS:
api_data = self._fetch_chain_orphans(api_data)
api_data = await self._fetch_chain_orphans(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.CHAIN_BLOCK_TIME:
api_data = self._fetch_chain_block_time(api_data)
api_data = await self._fetch_chain_block_time(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.NOMP_POOL_STATS:
api_data = self._fetch_nomp_pool_stats(api_data)
api_data = await self._fetch_nomp_pool_stats(api_data)

elif self._fetch_type == CryptoInfoDataFetchType.MEMPOOL_STATS:
api_data = self._fetch_mempool_stats(api_data)
api_data = await self._fetch_mempool_stats(api_data)

else:
api_data = self._fetch_price_data_main(api_data)
api_data = await self._fetch_price_data_main(api_data)

except ValueError:
try:
api_data = self._fetch_price_data_alternate(api_data)
api_data = await self._fetch_price_data_alternate(api_data)
except ValueError:
self._update_all_properties(available=False)
return
Expand Down Expand Up @@ -1156,6 +1168,9 @@ def __init__(
def attribute_key(self):
return self._attribute_key

async def _async_update(self):
self._update()

def _update(self):
new_state = self._parent_sensor.get_child_data(self)

Expand Down
3 changes: 2 additions & 1 deletion custom_components/cryptoinfo/manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"domain": "cryptoinfo",
"name": "Cryptoinfo",
"version": "0.2.4",
"version": "0.2.5",
"documentation": "https://github.com/TheHolyRoger/hass-cryptoinfo",
"dependencies": [],
"iot_class": "cloud_polling",
"codeowners": [
"@heyajohnny",
"@TheHolyRoger"
Expand Down
15 changes: 10 additions & 5 deletions custom_components/cryptoinfo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

import voluptuous as vol
from datetime import timedelta
import urllib.error

from .const.const import (
_LOGGER,
DOMAIN,
PLATFORMS,
CONF_CRYPTOCURRENCY_NAME,
CONF_CURRENCY_NAME,
CONF_MULTIPLIER,
Expand Down Expand Up @@ -41,9 +42,13 @@
CONF_UNIT_OF_MEASUREMENT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.reload import async_setup_reload_service


def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):

await async_setup_reload_service(hass, DOMAIN, PLATFORMS)

_LOGGER.debug("Setup Cryptoinfo sensor")

id_name = config.get(CONF_ID)
Expand Down Expand Up @@ -92,11 +97,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if new_sensor.check_valid_config(False):
entities.append(new_sensor)
entities.extend(new_sensor._get_child_sensors())
except urllib.error.HTTPError as error:
_LOGGER.error(error.reason)
except Exception as error:
_LOGGER.error(f"{type(error).__name__}: {error}")
return False

add_entities(entities)
async_add_entities(entities)
CryptoInfoEntityManager.instance().add_entities(entities)


Expand Down
Loading

0 comments on commit 4ab10aa

Please sign in to comment.