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
fabaff
merged 16 commits into
home-assistant:dev
from
nickw444:nwhyte/nsw-fuel-station-price
Jun 14, 2018
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
93d08e5
Add sensor.nsw_fuel_station component
nickw444 fc22abb
bump dependency
nickw444 835305f
PR Changes
nickw444 74ce4dc
flake8
nickw444 627fc8e
Use MockPrice
nickw444 282ef94
Fix requirements
nickw444 02ed7a6
Fix tests
nickw444 b5b7e70
line length
nickw444 8ca9f0e
wip
nickw444 877f47f
Handle errors and show persistent notification
nickw444 eb041a3
update tests
nickw444 2320fa3
Address @MartinHjelmare's comments
nickw444 b7433a1
Fetch station name from API
nickw444 8c5899f
Update tests
nickw444 9f9f23a
Update requirements
nickw444 8f942ab
Address comments
nickw444 File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!