1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 16:36:08 +01:00
Files
core/homeassistant/components/startca/sensor.py
2026-03-18 15:43:50 +01:00

259 lines
8.4 KiB
Python

"""Support for Start.ca Bandwidth Monitor."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from http import HTTPStatus
import logging
from xml.parsers.expat import ExpatError
from aiohttp import ClientSession
import voluptuous as vol
import xmltodict
from homeassistant.components.sensor import (
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import (
CONF_API_KEY,
CONF_MONITORED_VARIABLES,
CONF_NAME,
PERCENTAGE,
UnitOfInformation,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Start.ca"
CONF_TOTAL_BANDWIDTH = "total_bandwidth"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
REQUEST_TIMEOUT = 5 # seconds
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="usage",
name="Usage Ratio",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:percent",
),
SensorEntityDescription(
key="usage_gb",
name="Usage",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="limit",
name="Data limit",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="used_download",
name="Used Download",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="used_upload",
name="Used Upload",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:upload",
),
SensorEntityDescription(
key="used_total",
name="Used Total",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="grace_download",
name="Grace Download",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="grace_upload",
name="Grace Upload",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:upload",
),
SensorEntityDescription(
key="grace_total",
name="Grace Total",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="total_download",
name="Total Download",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="total_upload",
name="Total Upload",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
SensorEntityDescription(
key="used_remaining",
name="Remaining",
native_unit_of_measurement=UnitOfInformation.GIGABYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
),
)
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_MONITORED_VARIABLES): vol.All(
cv.ensure_list, [vol.In(SENSOR_KEYS)]
),
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_TOTAL_BANDWIDTH): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the sensor platform."""
websession = async_get_clientsession(hass)
apikey = config[CONF_API_KEY]
bandwidthcap = config[CONF_TOTAL_BANDWIDTH]
ts_data = StartcaData(websession, apikey, bandwidthcap)
ret = await ts_data.async_update()
if ret is False:
_LOGGER.error("Invalid Start.ca API key: %s", apikey)
return
name = config[CONF_NAME]
monitored_variables = config[CONF_MONITORED_VARIABLES]
if bandwidthcap <= 0:
monitored_variables = list(
filter(
lambda itm: itm not in {"limit", "usage", "used_remaining"},
monitored_variables,
)
)
entities = [
StartcaSensor(ts_data, name, description)
for description in SENSOR_TYPES
if description.key in monitored_variables
]
async_add_entities(entities, True)
class StartcaSensor(SensorEntity):
"""Representation of Start.ca Bandwidth sensor."""
def __init__(
self, startcadata: StartcaData, name: str, description: SensorEntityDescription
) -> None:
"""Initialize the sensor."""
self.entity_description = description
self.startcadata = startcadata
self._attr_name = f"{name} {description.name}"
async def async_update(self) -> None:
"""Get the latest data from Start.ca and update the state."""
await self.startcadata.async_update()
sensor_type = self.entity_description.key
if sensor_type in self.startcadata.data:
self._attr_native_value = round(self.startcadata.data[sensor_type], 2)
class StartcaData:
"""Get data from Start.ca API."""
def __init__(
self, websession: ClientSession, api_key: str, bandwidth_cap: int
) -> None:
"""Initialize the data object."""
self.websession = websession
self.api_key = api_key
self.bandwidth_cap = bandwidth_cap
# Set unlimited users to infinite, otherwise the cap.
self.data = {}
if self.bandwidth_cap > 0:
self.data["limit"] = self.bandwidth_cap
@staticmethod
def bytes_to_gb(value):
"""Convert from bytes to GB.
:param value: The value in bytes to convert to GB.
:return: Converted GB value
"""
return float(value) * 10**-9
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> bool:
"""Get the Start.ca bandwidth data from the web service."""
_LOGGER.debug("Updating Start.ca usage data")
url = f"https://www.start.ca/support/usage/api?key={self.api_key}"
async with asyncio.timeout(REQUEST_TIMEOUT):
req = await self.websession.get(url)
if req.status != HTTPStatus.OK:
_LOGGER.error("Request failed with status: %u", req.status)
return False
data = await req.text()
try:
xml_data = xmltodict.parse(data)
except ExpatError:
return False
used_dl = self.bytes_to_gb(xml_data["usage"]["used"]["download"])
used_ul = self.bytes_to_gb(xml_data["usage"]["used"]["upload"])
grace_dl = self.bytes_to_gb(xml_data["usage"]["grace"]["download"])
grace_ul = self.bytes_to_gb(xml_data["usage"]["grace"]["upload"])
total_dl = self.bytes_to_gb(xml_data["usage"]["total"]["download"])
total_ul = self.bytes_to_gb(xml_data["usage"]["total"]["upload"])
if self.bandwidth_cap > 0:
self.data["usage"] = 100 * used_dl / self.bandwidth_cap
self.data["used_remaining"] = self.data["limit"] - used_dl
self.data["usage_gb"] = used_dl
self.data["used_download"] = used_dl
self.data["used_upload"] = used_ul
self.data["used_total"] = used_dl + used_ul
self.data["grace_download"] = grace_dl
self.data["grace_upload"] = grace_ul
self.data["grace_total"] = grace_dl + grace_ul
self.data["total_download"] = total_dl
self.data["total_upload"] = total_ul
return True