Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sensor.nsw_fuel_station component #14757

Merged
merged 16 commits into from Jun 14, 2018
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -644,6 +644,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
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -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
Expand Down
174 changes: 174 additions & 0 deletions homeassistant/components/sensor/nsw_fuel_station.py
@@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Please return None explicitly if no price is found.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!


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()
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -579,6 +579,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

Expand Down
117 changes: 117 additions & 0 deletions tests/components/sensor/test_nsw_fuel_station.py
@@ -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)