Add sensor.nsw_fuel_station component (#14757)

* Add sensor.nsw_fuel_station component

* bump dependency

* PR Changes

* flake8

* Use MockPrice

* Fix requirements

* Fix tests

* line length

* wip

* Handle errors and show persistent notification

* update tests

* Address @MartinHjelmare's comments

* Fetch station name from API

* Update tests

* Update requirements

* Address comments
This commit is contained in:
Nick Whyte 2018-06-14 21:56:04 +10:00 committed by Fabian Affolter
parent cccd0deb65
commit cdd111df49
5 changed files with 296 additions and 0 deletions

View File

@ -655,6 +655,7 @@ omit =
homeassistant/components/sensor/nederlandse_spoorwegen.py
homeassistant/components/sensor/netdata.py
homeassistant/components/sensor/neurio_energy.py
homeassistant/components/sensor/nsw_fuel_station.py
homeassistant/components/sensor/nut.py
homeassistant/components/sensor/nzbget.py
homeassistant/components/sensor/ohmconnect.py

View File

@ -70,6 +70,7 @@ homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/qnap.py @colinodell
homeassistant/components/sensor/sma.py @kellerza

View File

@ -0,0 +1,174 @@
"""
Sensor platform to display the current fuel prices at a NSW fuel station.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.nsw_fuel_station/
"""
import datetime
import logging
from typing import Optional
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.light import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['nsw-fuel-api-client==1.0.10']
_LOGGER = logging.getLogger(__name__)
ATTR_STATION_ID = 'station_id'
ATTR_STATION_NAME = 'station_name'
CONF_STATION_ID = 'station_id'
CONF_FUEL_TYPES = 'fuel_types'
CONF_ALLOWED_FUEL_TYPES = ["E10", "U91", "E85", "P95", "P98", "DL",
"PDL", "B20", "LPG", "CNG", "EV"]
CONF_DEFAULT_FUEL_TYPES = ["E10", "U91"]
CONF_ATTRIBUTION = "Data provided by NSW Government FuelCheck"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION_ID): cv.positive_int,
vol.Optional(CONF_FUEL_TYPES, default=CONF_DEFAULT_FUEL_TYPES):
vol.All(cv.ensure_list, [vol.In(CONF_ALLOWED_FUEL_TYPES)]),
})
MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(hours=1)
NOTIFICATION_ID = 'nsw_fuel_station_notification'
NOTIFICATION_TITLE = 'NSW Fuel Station Sensor Setup'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the NSW Fuel Station sensor."""
from nsw_fuel import FuelCheckClient
station_id = config[CONF_STATION_ID]
fuel_types = config[CONF_FUEL_TYPES]
client = FuelCheckClient()
station_data = StationPriceData(client, station_id)
station_data.update()
if station_data.error is not None:
message = (
'Error: {}. Check the logs for additional information.'
).format(station_data.error)
hass.components.persistent_notification.create(
message,
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return
available_fuel_types = station_data.get_available_fuel_types()
add_devices([
StationPriceSensor(station_data, fuel_type)
for fuel_type in fuel_types
if fuel_type in available_fuel_types
])
class StationPriceData(object):
"""An object to store and fetch the latest data for a given station."""
def __init__(self, client, station_id: int) -> None:
"""Initialize the sensor."""
self.station_id = station_id
self._client = client
self._data = None
self._reference_data = None
self.error = None
self._station_name = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update the internal data using the API client."""
from nsw_fuel import FuelCheckError
if self._reference_data is None:
try:
self._reference_data = self._client.get_reference_data()
except FuelCheckError as exc:
self.error = str(exc)
_LOGGER.error(
'Failed to fetch NSW Fuel station reference data. %s', exc)
return
try:
self._data = self._client.get_fuel_prices_for_station(
self.station_id)
except FuelCheckError as exc:
self.error = str(exc)
_LOGGER.error(
'Failed to fetch NSW Fuel station price data. %s', exc)
def for_fuel_type(self, fuel_type: str):
"""Return the price of the given fuel type."""
if self._data is None:
return None
return next((price for price
in self._data if price.fuel_type == fuel_type), None)
def get_available_fuel_types(self):
"""Return the available fuel types for the station."""
return [price.fuel_type for price in self._data]
def get_station_name(self) -> str:
"""Return the name of the station."""
if self._station_name is None:
name = None
if self._reference_data is not None:
name = next((station.name for station
in self._reference_data.stations
if station.code == self.station_id), None)
self._station_name = name or 'station {}'.format(self.station_id)
return self._station_name
class StationPriceSensor(Entity):
"""Implementation of a sensor that reports the fuel price for a station."""
def __init__(self, station_data: StationPriceData, fuel_type: str):
"""Initialize the sensor."""
self._station_data = station_data
self._fuel_type = fuel_type
@property
def name(self) -> str:
"""Return the name of the sensor."""
return '{} {}'.format(
self._station_data.get_station_name(), self._fuel_type)
@property
def state(self) -> Optional[float]:
"""Return the state of the sensor."""
price_info = self._station_data.for_fuel_type(self._fuel_type)
if price_info:
return price_info.price
return None
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes of the device."""
return {
ATTR_STATION_ID: self._station_data.station_id,
ATTR_STATION_NAME: self._station_data.get_station_name(),
ATTR_ATTRIBUTION: CONF_ATTRIBUTION
}
@property
def unit_of_measurement(self) -> str:
"""Return the units of measurement."""
return '¢/L'
def update(self):
"""Update current conditions."""
self._station_data.update()

View File

@ -597,6 +597,9 @@ neurio==0.3.1
# homeassistant.components.sensor.nederlandse_spoorwegen
nsapi==2.7.4
# homeassistant.components.sensor.nsw_fuel_station
nsw-fuel-api-client==1.0.10
# homeassistant.components.nuheat
nuheat==0.3.0

View File

@ -0,0 +1,117 @@
"""The tests for the NSW Fuel Station sensor platform."""
import unittest
from unittest.mock import patch
from homeassistant.components import sensor
from homeassistant.setup import setup_component
from tests.common import (
get_test_home_assistant, assert_setup_component, MockDependency)
VALID_CONFIG = {
'platform': 'nsw_fuel_station',
'station_id': 350,
'fuel_types': ['E10', 'P95'],
}
class MockPrice():
"""Mock Price implementation."""
def __init__(self, price, fuel_type, last_updated,
price_unit, station_code):
"""Initialize a mock price instance."""
self.price = price
self.fuel_type = fuel_type
self.last_updated = last_updated
self.price_unit = price_unit
self.station_code = station_code
class MockStation():
"""Mock Station implementation."""
def __init__(self, name, code):
"""Initialize a mock Station instance."""
self.name = name
self.code = code
class MockGetReferenceDataResponse():
"""Mock GetReferenceDataResponse implementation."""
def __init__(self, stations):
"""Initialize a mock GetReferenceDataResponse instance."""
self.stations = stations
class FuelCheckClientMock():
"""Mock FuelCheckClient implementation."""
def get_fuel_prices_for_station(self, station):
"""Return a fake fuel prices response."""
return [
MockPrice(
price=150.0,
fuel_type='P95',
last_updated=None,
price_unit=None,
station_code=350
),
MockPrice(
price=140.0,
fuel_type='E10',
last_updated=None,
price_unit=None,
station_code=350
)
]
def get_reference_data(self):
"""Return a fake reference data response."""
return MockGetReferenceDataResponse(
stations=[
MockStation(code=350, name="My Fake Station")
]
)
class TestNSWFuelStation(unittest.TestCase):
"""Test the NSW Fuel Station sensor platform."""
def setUp(self):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.config = VALID_CONFIG
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@MockDependency('nsw_fuel')
@patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock)
def test_setup(self, mock_nsw_fuel):
"""Test the setup with custom settings."""
with assert_setup_component(1, sensor.DOMAIN):
self.assertTrue(setup_component(self.hass, sensor.DOMAIN, {
'sensor': VALID_CONFIG}))
fake_entities = [
'my_fake_station_p95',
'my_fake_station_e10'
]
for entity_id in fake_entities:
state = self.hass.states.get('sensor.{}'.format(entity_id))
self.assertIsNotNone(state)
@MockDependency('nsw_fuel')
@patch('nsw_fuel.FuelCheckClient', new=FuelCheckClientMock)
def test_sensor_values(self, mock_nsw_fuel):
"""Test retrieval of sensor values."""
self.assertTrue(setup_component(
self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG}))
self.assertEqual('140.0', self.hass.states.get(
'sensor.my_fake_station_e10').state)
self.assertEqual('150.0', self.hass.states.get(
'sensor.my_fake_station_p95').state)