Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Start.ca Bandwidth Monitor."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import timedelta
7 from http import HTTPStatus
8 import logging
9 from xml.parsers.expat import ExpatError
10 
11 import voluptuous as vol
12 import xmltodict
13 
15  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
16  SensorDeviceClass,
17  SensorEntity,
18  SensorEntityDescription,
19 )
20 from homeassistant.const import (
21  CONF_API_KEY,
22  CONF_MONITORED_VARIABLES,
23  CONF_NAME,
24  PERCENTAGE,
25  UnitOfInformation,
26 )
27 from homeassistant.core import HomeAssistant
28 from homeassistant.helpers.aiohttp_client import async_get_clientsession
30 from homeassistant.helpers.entity_platform import AddEntitiesCallback
31 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
32 from homeassistant.util import Throttle
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 DEFAULT_NAME = "Start.ca"
37 CONF_TOTAL_BANDWIDTH = "total_bandwidth"
38 
39 MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
40 REQUEST_TIMEOUT = 5 # seconds
41 
42 SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
44  key="usage",
45  name="Usage Ratio",
46  native_unit_of_measurement=PERCENTAGE,
47  icon="mdi:percent",
48  ),
50  key="usage_gb",
51  name="Usage",
52  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
53  device_class=SensorDeviceClass.DATA_SIZE,
54  icon="mdi:download",
55  ),
57  key="limit",
58  name="Data limit",
59  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
60  device_class=SensorDeviceClass.DATA_SIZE,
61  icon="mdi:download",
62  ),
64  key="used_download",
65  name="Used Download",
66  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
67  device_class=SensorDeviceClass.DATA_SIZE,
68  icon="mdi:download",
69  ),
71  key="used_upload",
72  name="Used Upload",
73  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
74  device_class=SensorDeviceClass.DATA_SIZE,
75  icon="mdi:upload",
76  ),
78  key="used_total",
79  name="Used Total",
80  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
81  device_class=SensorDeviceClass.DATA_SIZE,
82  icon="mdi:download",
83  ),
85  key="grace_download",
86  name="Grace Download",
87  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
88  device_class=SensorDeviceClass.DATA_SIZE,
89  icon="mdi:download",
90  ),
92  key="grace_upload",
93  name="Grace Upload",
94  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
95  device_class=SensorDeviceClass.DATA_SIZE,
96  icon="mdi:upload",
97  ),
99  key="grace_total",
100  name="Grace Total",
101  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
102  device_class=SensorDeviceClass.DATA_SIZE,
103  icon="mdi:download",
104  ),
106  key="total_download",
107  name="Total Download",
108  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
109  device_class=SensorDeviceClass.DATA_SIZE,
110  icon="mdi:download",
111  ),
113  key="total_upload",
114  name="Total Upload",
115  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
116  device_class=SensorDeviceClass.DATA_SIZE,
117  icon="mdi:download",
118  ),
120  key="used_remaining",
121  name="Remaining",
122  native_unit_of_measurement=UnitOfInformation.GIGABYTES,
123  device_class=SensorDeviceClass.DATA_SIZE,
124  icon="mdi:download",
125  ),
126 )
127 
128 SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
129 
130 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
131  {
132  vol.Required(CONF_MONITORED_VARIABLES): vol.All(
133  cv.ensure_list, [vol.In(SENSOR_KEYS)]
134  ),
135  vol.Required(CONF_API_KEY): cv.string,
136  vol.Required(CONF_TOTAL_BANDWIDTH): cv.positive_int,
137  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
138  }
139 )
140 
141 
143  hass: HomeAssistant,
144  config: ConfigType,
145  async_add_entities: AddEntitiesCallback,
146  discovery_info: DiscoveryInfoType | None = None,
147 ) -> None:
148  """Set up the sensor platform."""
149  websession = async_get_clientsession(hass)
150  apikey = config[CONF_API_KEY]
151  bandwidthcap = config[CONF_TOTAL_BANDWIDTH]
152 
153  ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap)
154  ret = await ts_data.async_update()
155  if ret is False:
156  _LOGGER.error("Invalid Start.ca API key: %s", apikey)
157  return
158 
159  name = config[CONF_NAME]
160  monitored_variables = config[CONF_MONITORED_VARIABLES]
161  if bandwidthcap <= 0:
162  monitored_variables = list(
163  filter(
164  lambda itm: itm not in {"limit", "usage", "used_remaining"},
165  monitored_variables,
166  )
167  )
168  entities = [
169  StartcaSensor(ts_data, name, description)
170  for description in SENSOR_TYPES
171  if description.key in monitored_variables
172  ]
173  async_add_entities(entities, True)
174 
175 
177  """Representation of Start.ca Bandwidth sensor."""
178 
179  def __init__(self, startcadata, name, description: SensorEntityDescription) -> None:
180  """Initialize the sensor."""
181  self.entity_descriptionentity_description = description
182  self.startcadatastartcadata = startcadata
183 
184  self._attr_name_attr_name = f"{name} {description.name}"
185 
186  async def async_update(self) -> None:
187  """Get the latest data from Start.ca and update the state."""
188  await self.startcadatastartcadata.async_update()
189  sensor_type = self.entity_descriptionentity_description.key
190  if sensor_type in self.startcadatastartcadata.data:
191  self._attr_native_value_attr_native_value = round(self.startcadatastartcadata.data[sensor_type], 2)
192 
193 
195  """Get data from Start.ca API."""
196 
197  def __init__(self, loop, websession, api_key, bandwidth_cap):
198  """Initialize the data object."""
199  self.looploop = loop
200  self.websessionwebsession = websession
201  self.api_keyapi_key = api_key
202  self.bandwidth_capbandwidth_cap = bandwidth_cap
203  # Set unlimited users to infinite, otherwise the cap.
204  self.datadata = {}
205  if self.bandwidth_capbandwidth_cap > 0:
206  self.datadata["limit"] = self.bandwidth_capbandwidth_cap
207 
208  @staticmethod
209  def bytes_to_gb(value):
210  """Convert from bytes to GB.
211 
212  :param value: The value in bytes to convert to GB.
213  :return: Converted GB value
214  """
215  return float(value) * 10**-9
216 
217  @Throttle(MIN_TIME_BETWEEN_UPDATES)
218  async def async_update(self):
219  """Get the Start.ca bandwidth data from the web service."""
220  _LOGGER.debug("Updating Start.ca usage data")
221  url = f"https://www.start.ca/support/usage/api?key={self.api_key}"
222  async with asyncio.timeout(REQUEST_TIMEOUT):
223  req = await self.websessionwebsession.get(url)
224  if req.status != HTTPStatus.OK:
225  _LOGGER.error("Request failed with status: %u", req.status)
226  return False
227 
228  data = await req.text()
229  try:
230  xml_data = xmltodict.parse(data)
231  except ExpatError:
232  return False
233 
234  used_dl = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["used"]["download"])
235  used_ul = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["used"]["upload"])
236  grace_dl = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["grace"]["download"])
237  grace_ul = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["grace"]["upload"])
238  total_dl = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["total"]["download"])
239  total_ul = self.bytes_to_gbbytes_to_gb(xml_data["usage"]["total"]["upload"])
240 
241  if self.bandwidth_capbandwidth_cap > 0:
242  self.datadata["usage"] = 100 * used_dl / self.bandwidth_capbandwidth_cap
243  self.datadata["used_remaining"] = self.datadata["limit"] - used_dl
244  self.datadata["usage_gb"] = used_dl
245  self.datadata["used_download"] = used_dl
246  self.datadata["used_upload"] = used_ul
247  self.datadata["used_total"] = used_dl + used_ul
248  self.datadata["grace_download"] = grace_dl
249  self.datadata["grace_upload"] = grace_ul
250  self.datadata["grace_total"] = grace_dl + grace_ul
251  self.datadata["total_download"] = total_dl
252  self.datadata["total_upload"] = total_ul
253 
254  return True
def __init__(self, loop, websession, api_key, bandwidth_cap)
Definition: sensor.py:197
None __init__(self, startcadata, name, SensorEntityDescription description)
Definition: sensor.py:179
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:147
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)