Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ omit =
homeassistant/components/sensor/sytadin.py
homeassistant/components/sensor/tank_utility.py
homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/teksavvy.py
homeassistant/components/sensor/temper.py
homeassistant/components/sensor/tibber.py
homeassistant/components/sensor/time_date.py
Expand Down
168 changes: 168 additions & 0 deletions homeassistant/components/sensor/teksavvy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Support for TekSavvy.

Get bandwidth comsumption data using Teksavvy API
You can get your API key here:
https://myaccount.teksavvy.com/ApiKey/ApiKeyManagement

TekSavvy only counts download only as part of the bandwidth

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs to the documentation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.


For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.teksavvy/
"""
import logging
from datetime import timedelta
import http.client

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'http.client' imported but unused

import json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'json' imported but unused

import asyncio
import async_timeout
from homeassistant.helpers.aiohttp_client import async_get_clientsession

import voluptuous as vol
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_API_KEY, CONF_NAME, CONF_MONITORED_VARIABLES)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the import groups and sorted. Use isort if you don't want to do it manually.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks for the isort tip.


from homeassistant.util import Throttle

_LOGGER = logging.getLogger(__name__)

DEFAULT_NAME = 'TekSavvy'
CONF_TOTAL_BANDWIDTH = 'total_bandwidth'

GIGABYTES = 'GB' # type: str
PERCENT = '%' # type: str

MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
REQUEST_TIMEOUT = 5 # seconds

SENSOR_TYPES = {
'usage': ['Usage', PERCENT, 'mdi:percent'],
'usage_gb': ['Usage', GIGABYTES, 'mdi:download'],
'limit': ['Data limit', GIGABYTES, 'mdi:download'],
'onpeak_download': ['On Peak Download', GIGABYTES, 'mdi:download'],
'onpeak_upload': ['On Peak Upload ', GIGABYTES, 'mdi:upload'],
'onpeak_total': ['On Peak Total', GIGABYTES, 'mdi:download'],
'offpeak_download': ['Off Peak download', GIGABYTES, 'mdi:download'],
'offpeak_upload': ['Off Peak Upload', GIGABYTES, 'mdi:upload'],
'offpeak_total': ['Off Peak Total', GIGABYTES, 'mdi:download'],
'onpeak_remaining': ['Remaining', GIGABYTES, 'mdi:download']
}

API_HA_MAP = (
('OnPeakDownload', 'onpeak_download'),
('OnPeakUpload', 'onpeak_upload'),
('OffPeakDownload', 'offpeak_download'),
('OffPeakUpload', 'offpeak_upload'))

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_TOTAL_BANDWIDTH): cv.string,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only allow integers here. This way you don't need the casting on line 73.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Allows only positive integer now.

vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})

@asyncio.coroutine

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the sensor platform."""
websession = async_get_clientsession(hass)
apikey = config.get(CONF_API_KEY)
bandwidthcapstr = config.get(CONF_TOTAL_BANDWIDTH)
try:
bandwidthcap = int(bandwidthcapstr)
ts_data = TekSavvyData(hass.loop, websession, apikey, bandwidthcap)
ret = yield from ts_data.async_update()
except ValueError as error:
_LOGGER.error("Failed conversion %s %s", CONF_TOTAL_BANDWIDTH, error)
if ret == False:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparison to False should be 'if cond is False:' or 'if not cond:'

return

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should log an invalid API key. I guess that for the users it's not very helpful that a conversation failed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the conversion since we no longer have the cast. Also, added an error message if the user has an invalid API key.


name = config.get(CONF_NAME)
sensors = []
for variable in config[CONF_MONITORED_VARIABLES]:
sensors.append(TekSavvySensor(ts_data, variable, name))
async_add_devices(sensors, True)

class TekSavvySensor(Entity):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

"""TekSavvy Bandwidth sensor."""

def __init__(self, teksavvydata, sensor_type, name):
"""Initialize the sensor."""
self.client_name = name
self.type = sensor_type
self._name = SENSOR_TYPES[sensor_type][0]
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self._icon = SENSOR_TYPES[sensor_type][2]
self.teksavvydata = teksavvydata
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format(self.client_name, self._name)

@property
def state(self):
"""Return the state of the sensor."""
return self._state

@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement

@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon

@asyncio.coroutine
def async_update(self):
"""Get the latest data from TekSavvy and update the state."""
yield from self.teksavvydata.async_update()
if self.type in self.teksavvydata.data:
self._state = round(self.teksavvydata.data[self.type], 2)

class TekSavvyData(object):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected 2 blank lines, found 1

"""Get data from TekSavvy API."""

def __init__(self, loop, websession, api_key, bandwidth_cap):
"""Initialize the data object."""
self.loop = loop
self.websession = websession
self.api_key = api_key
self.bandwidth_cap = bandwidth_cap
self.data = dict()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use {} here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

self.data["limit"] = self.bandwidth_cap

@asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def async_update(self):
"""Get the TekSavvy bandwidth data from the web service."""
headers = {"TekSavvy-APIKey": self.api_key}
_LOGGER.info("Updaing TekSavvy data")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this _LOGGER.debug. Less verbose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

url = "https://api.teksavvy.com/"\
"web/Usage/UsageSummaryRecords?$filter=IsCurrent%20eq%20true"
with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop):
req = yield from self.websession.get(url, headers = headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unexpected spaces around keyword / parameter equals

if req.status != 200:
_LOGGER.error("Request failed with status: %u", req.status)
return False
else:
data = yield from req.json()
for (api, ha_name) in API_HA_MAP:
self.data[ha_name] = float(data["value"][0][api])
on_peak_download = self.data["onpeak_download"]
on_peak_upload = self.data["onpeak_upload"]
off_peak_download = self.data["offpeak_download"]
off_peak_upload = self.data["offpeak_upload"]
limit = self.data["limit"]
self.data["usage"] = 100*on_peak_download/self.bandwidth_cap
self.data["usage_gb"] = on_peak_download
self.data["onpeak_total"] = on_peak_download + on_peak_upload
self.data["offpeak_total"] = off_peak_download + off_peak_upload
self.data["onpeak_remaining"] = limit - on_peak_download
return True