Skip to content

Commit 48c293a

Browse files
committed
Optional CoinGecko API price precision
1 parent 30ecd32 commit 48c293a

File tree

7 files changed

+102
-25
lines changed

7 files changed

+102
-25
lines changed

Version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ Version: 1.0.5 20241219 - Updated help text
2525
Version: 1.0.6 20241222 - Added 14 day change and 1 year change
2626
Version: 1.0.7 20241227 - Added ath_price, ath_date and ath_change
2727
Version: 1.0.8 20260317 - Updated code to support future Home Assistant versions
28+
Version: 1.0.9 20260322 - Optional CoinGecko API price precision (config + /coins/markets)

custom_components/cryptoinfo/config_flow.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
#!/usr/bin/env python3
2-
"""
3-
Config flow component for Cryptoinfo
2+
"""Config flow for Cryptoinfo.
3+
44
Author: Johnny Visser
55
"""
66

77
from collections.abc import Mapping
88
from typing import Any
99

1010
import voluptuous as vol
11+
1112
from homeassistant import config_entries
1213
from homeassistant.helpers import config_validation as cv
1314

14-
from .helper.crypto_info_data import CryptoInfoData
15-
15+
from .config_validation import precision as cv_precision
1616
from .const.const import (
1717
_LOGGER,
1818
CONF_CRYPTOCURRENCY_IDS,
1919
CONF_CURRENCY_NAME,
2020
CONF_ID,
2121
CONF_MIN_TIME_BETWEEN_REQUESTS,
2222
CONF_MULTIPLIERS,
23+
CONF_PRECISION,
2324
CONF_UNIT_OF_MEASUREMENT,
2425
CONF_UPDATE_FREQUENCY,
2526
DOMAIN,
2627
)
28+
from .helper.crypto_info_data import CryptoInfoData
2729

2830

2931
class CryptoInfoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
32+
"""Config flow to add or reconfigure Cryptoinfo entries."""
33+
3034
VERSION = 1
3135

3236
def _validate_input(self, user_input: dict[str, Any]) -> dict[str, Any]:
@@ -51,7 +55,17 @@ def _validate_input(self, user_input: dict[str, Any]) -> dict[str, Any]:
5155

5256
return errors
5357

58+
def _precision_field_errors(self, user_input: dict[str, Any]) -> dict[str, str]:
59+
"""Validate CONF_PRECISION (UI schema must stay JSON-serializable)."""
60+
errors: dict[str, str] = {}
61+
try:
62+
user_input[CONF_PRECISION] = cv_precision(user_input.get(CONF_PRECISION, ""))
63+
except vol.Invalid:
64+
errors[CONF_PRECISION] = "invalid_precision"
65+
return errors
66+
5467
async def async_step_reconfigure(self, user_input: Mapping[str, Any] | None = None):
68+
"""Allow changing an existing Cryptoinfo config entry."""
5569
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
5670
assert entry
5771
if user_input:
@@ -63,6 +77,12 @@ async def async_step_reconfigure(self, user_input: Mapping[str, Any] | None = No
6377
user_input[CONF_ID] = ""
6478
if CONF_UNIT_OF_MEASUREMENT not in user_input:
6579
user_input[CONF_UNIT_OF_MEASUREMENT] = ""
80+
if CONF_PRECISION not in user_input:
81+
user_input[CONF_PRECISION] = ""
82+
83+
prec_errors = self._precision_field_errors(user_input)
84+
if prec_errors:
85+
return await self._redo_configuration(entry.data, prec_errors)
6686

6787
# Validate the input
6888
validation_result = self._validate_input(user_input)
@@ -132,6 +152,11 @@ async def _redo_configuration(
132152
"suggested_value": entry_data.get(CONF_UNIT_OF_MEASUREMENT, "")
133153
},
134154
): str,
155+
vol.Optional(
156+
CONF_PRECISION,
157+
default=entry_data.get(CONF_PRECISION, ""),
158+
description={"suggested_value": entry_data.get(CONF_PRECISION, "")},
159+
): str,
135160
vol.Required(
136161
CONF_UPDATE_FREQUENCY, default=entry_data[CONF_UPDATE_FREQUENCY]
137162
): cv.positive_float,
@@ -171,6 +196,7 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None):
171196
CONF_UNIT_OF_MEASUREMENT: "$",
172197
CONF_UPDATE_FREQUENCY: 1,
173198
CONF_MIN_TIME_BETWEEN_REQUESTS: default_min_time,
199+
CONF_PRECISION: "",
174200
}
175201

176202
# Update defaults with user input if it exists
@@ -197,6 +223,11 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None):
197223
"suggested_value": defaults.get(CONF_UNIT_OF_MEASUREMENT, "")
198224
},
199225
): str,
226+
vol.Optional(
227+
CONF_PRECISION,
228+
default="",
229+
description={"suggested_value": defaults.get(CONF_PRECISION, "")},
230+
): str,
200231
vol.Required(
201232
CONF_UPDATE_FREQUENCY, default=defaults[CONF_UPDATE_FREQUENCY]
202233
): cv.positive_float,
@@ -213,6 +244,15 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None):
213244
)
214245

215246
try:
247+
prec_errors = self._precision_field_errors(user_input)
248+
if prec_errors:
249+
errors.update(prec_errors)
250+
return self.async_show_form(
251+
step_id="user",
252+
data_schema=cryptoinfo_schema,
253+
errors=errors,
254+
)
255+
216256
# Validate the input
217257
validation_result = self._validate_input(user_input)
218258
if validation_result:
@@ -237,7 +277,7 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None):
237277
title=f"Cryptoinfo for {user_input[CONF_ID]}", data=user_input
238278
)
239279

240-
except Exception as ex:
280+
except Exception as ex: # noqa: BLE001
241281
_LOGGER.error(f"Error creating entry: {ex}")
242282
errors["base"] = f"Error creating entry: {ex}"
243283
return self.async_show_form(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Voluptuous validators for Cryptoinfo configuration."""
2+
3+
from typing import Any
4+
5+
import voluptuous as vol
6+
7+
8+
def precision(value: Any) -> str:
9+
"""Normalize CoinGecko `precision` (empty, full, or 0–18)."""
10+
if value is None:
11+
return ""
12+
v = str(value).strip().lower()
13+
if not v:
14+
return ""
15+
if v == "full":
16+
return "full"
17+
if v.isdigit():
18+
n = int(v)
19+
if 0 <= n <= 18:
20+
return str(n)
21+
raise vol.Invalid(
22+
"Must be empty (CoinGecko default), full, or a whole number from 0 to 18"
23+
)

custom_components/cryptoinfo/const/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
CONF_UPDATE_FREQUENCY = "update_frequency"
1010
CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement"
1111
CONF_MIN_TIME_BETWEEN_REQUESTS = "min_time_between_requests"
12+
CONF_PRECISION = "precision"
1213

1314
SENSOR_PREFIX = "Cryptoinfo "
1415
ATTR_LAST_UPDATE = "last_update"

custom_components/cryptoinfo/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "cryptoinfo",
33
"name": "Cryptoinfo",
4-
"version": "1.0.8",
4+
"version": "1.0.9",
55
"config_flow": true,
66
"iot_class": "cloud_polling",
77
"documentation": "https://github.com/heyajohnny/cryptoinfo",

custom_components/cryptoinfo/sensor.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66

77
from datetime import datetime, timedelta
88
import urllib.error
9+
from urllib.parse import urlencode
910

1011
from aiohttp import ClientError
1112

1213
from homeassistant import config_entries
1314
from homeassistant.components.sensor import SensorEntity
14-
from homeassistant.components.sensor.const import SensorDeviceClass, SensorStateClass
15+
from homeassistant.components.sensor.const import SensorStateClass
1516
from homeassistant.core import HomeAssistant
1617
from homeassistant.helpers import aiohttp_client
1718
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -50,6 +51,7 @@
5051
CONF_ID,
5152
CONF_MIN_TIME_BETWEEN_REQUESTS,
5253
CONF_MULTIPLIERS,
54+
CONF_PRECISION,
5355
CONF_UNIT_OF_MEASUREMENT,
5456
CONF_UPDATE_FREQUENCY,
5557
SENSOR_PREFIX,
@@ -75,6 +77,7 @@ async def async_setup_entry(
7577
min_time_between_requests = timedelta(
7678
minutes=(float(config.get(CONF_MIN_TIME_BETWEEN_REQUESTS)))
7779
)
80+
precision = (config.get(CONF_PRECISION) or "").strip().lower()
7881

7982
# Create coordinator for centralized data fetching
8083
coordinator = CryptoDataCoordinator(
@@ -84,6 +87,7 @@ async def async_setup_entry(
8487
update_frequency,
8588
min_time_between_requests,
8689
id_name,
90+
precision,
8791
)
8892

8993
# Wait for coordinator to do first update
@@ -138,6 +142,7 @@ def __init__(
138142
update_frequency: timedelta,
139143
min_time_between_requests: timedelta,
140144
id_name: str,
145+
precision: str,
141146
):
142147
"""Initialize the coordinator."""
143148
super().__init__(
@@ -156,6 +161,18 @@ def __init__(
156161
self.id_name = id_name
157162
self.min_time_between_requests = min_time_between_requests
158163
self.update_frequency = update_frequency
164+
self.precision = precision
165+
166+
def _markets_url(self) -> str:
167+
"""Build the CoinGecko /coins/markets request URL."""
168+
params = [
169+
("vs_currency", self.currency_name),
170+
("ids", self.cryptocurrency_ids),
171+
("price_change_percentage", "1h,24h,7d,14d,30d,1y"),
172+
]
173+
if self.precision:
174+
params.append(("precision", self.precision))
175+
return f"{API_ENDPOINT}coins/markets?{urlencode(params)}"
159176

160177
async def async_will_remove_from_hass(self) -> None:
161178
"""Handle removal from Home Assistant."""
@@ -178,12 +195,7 @@ async def _async_update_data(self):
178195
f"First request, fetching data for sensor: {self.id_name} instance_id: {self.instance_id} cryptocurrency_ids: {self.cryptocurrency_ids}"
179196
)
180197

181-
url = (
182-
f"{API_ENDPOINT}coins/markets"
183-
f"?ids={self.cryptocurrency_ids}"
184-
f"&vs_currency={self.currency_name}"
185-
f"&price_change_percentage=1h%2C24h%2C7d%2C14d%2C30d%2C1y"
186-
)
198+
url = self._markets_url()
187199

188200
try:
189201
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -234,12 +246,7 @@ async def _async_update_data(self):
234246
f"Fetch data from API endpoint, sensor: {self.id_name} instance_id: {self.instance_id} cryptocurrency_ids: {self.cryptocurrency_ids}"
235247
)
236248

237-
url = (
238-
f"{API_ENDPOINT}coins/markets"
239-
f"?ids={self.cryptocurrency_ids}"
240-
f"&vs_currency={self.currency_name}"
241-
f"&price_change_percentage=1h%2C24h%2C7d%2C14d%2C30d%2C1y"
242-
)
249+
url = self._markets_url()
243250

244251
try:
245252
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -274,7 +281,7 @@ def __init__(
274281
self.cryptocurrency_id = cryptocurrency_id
275282
self.currency_name = currency_name
276283
self.multiplier = multiplier
277-
self._attr_device_class = SensorDeviceClass.MONETARY
284+
# MONETARY + MEASUREMENT is invalid in Home Assistant; spot price is a measurement.
278285
self._attr_state_class = SensorStateClass.MEASUREMENT
279286
self._attr_native_unit_of_measurement = unit_of_measurement or None
280287
# Let Home Assistant generate a valid entity ID (CoinGecko ids can contain '-')

custom_components/cryptoinfo/translations/en.json

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"unit_of_measurement": "Unit of measurement",
1212
"multipliers": "Multipliers",
1313
"update_frequency": "Update Frequency (minutes)",
14-
"min_time_between_requests": "Minimum time between requests (minutes)"
14+
"min_time_between_requests": "Minimum time between requests (minutes)",
15+
"precision": "Price precision"
1516
},
1617
"data_description": {
1718
"id": "Unique name for the sensor.",
@@ -20,7 +21,8 @@
2021
"unit_of_measurement": "Do you want to use a currency symbol? ([Symbol list](https://en.wikipedia.org/wiki/Currency_symbol#List_of_currency_symbols_currently_in_use))",
2122
"multipliers": "The number of coins/tokens (separated by a comma). The number of multipliers must match the number of cryptocurrency IDs.",
2223
"update_frequency": "How often should the value be refreshed? Beware of the [CoinGecko rate limit](https://support.coingecko.com/hc/en-us/articles/4538771776153-What-is-the-rate-limit-for-CoinGecko-API-public-plan) when tracking multiple cryptocurrencies.",
23-
"min_time_between_requests": "The minimum time between the other entities and this entity to make a data request to the API. (This property is shared and the same for every entity.)"
24+
"min_time_between_requests": "The minimum time between the other entities and this entity to make a data request to the API. (This property is shared and the same for every entity.)",
25+
"precision": "Optional. CoinGecko `precision` for currency prices: leave empty for API default, use `full` for full precision, or `0`–`18` for decimal places. See [coins/markets](https://docs.coingecko.com/reference/coins-markets)."
2426
}
2527
},
2628
"reconfigure": {
@@ -33,7 +35,8 @@
3335
"unit_of_measurement": "Unit of measurement",
3436
"multipliers": "Multipliers",
3537
"update_frequency": "Update Frequency (minutes)",
36-
"min_time_between_requests": "Minimum time between requests (minutes)"
38+
"min_time_between_requests": "Minimum time between requests (minutes)",
39+
"precision": "Price precision"
3740
},
3841
"data_description": {
3942
"id": "Unique name for the sensor.",
@@ -42,7 +45,8 @@
4245
"unit_of_measurement": "Do you want to use a currency symbol? ([Symbol list](https://en.wikipedia.org/wiki/Currency_symbol#List_of_currency_symbols_currently_in_use))",
4346
"multipliers": "The number of coins/tokens (separated by a comma). The number of multipliers must match the number of cryptocurrency IDs.",
4447
"update_frequency": "How often should the value be refreshed? Beware of the [CoinGecko rate limit](https://support.coingecko.com/hc/en-us/articles/4538771776153-What-is-the-rate-limit-for-CoinGecko-API-public-plan) when tracking multiple cryptocurrencies.",
45-
"min_time_between_requests": "The minimum time between the other entities and this entity to make a data request to the API. (This property is shared and the same for every entity.)"
48+
"min_time_between_requests": "The minimum time between the other entities and this entity to make a data request to the API. (This property is shared and the same for every entity.)",
49+
"precision": "Optional. CoinGecko `precision` for currency prices: leave empty for API default, use `full` for full precision, or `0`–`18` for decimal places. See [coins/markets](https://docs.coingecko.com/reference/coins-markets)."
4650
}
4751
}
4852
},
@@ -52,7 +56,8 @@
5256
},
5357
"error": {
5458
"cannot_connect": "Error: Cannot connect",
55-
"mismatch_values": "The number of cryptocurrency id's ({crypto_count}) does not match the number of multipliers ({multiplier_count})"
59+
"mismatch_values": "The number of cryptocurrency id's ({crypto_count}) does not match the number of multipliers ({multiplier_count})",
60+
"invalid_precision": "Precision must be empty, full, or a whole number from 0 to 18"
5661
}
5762
}
5863
}

0 commit comments

Comments
 (0)