mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 01:37:08 +00:00
Add Tankerkoenig integration (#28661)
* Initial version Parse configuration, but return a fixed value * Add basic functionality Request real data from the server Currently the prices are not getting updated, but the petrol station data is real * Update values regularly The tankerkoenig values get updated regularly with real data. * Move base functionality for the sensor to a base class And move that to an own file, so that it can be inherited * Reduce calls to tankerkoenig api Use a master/slave concept for sensors; one master gets the data and updates it into the slaves. * Update requirements files * Update all gas stations at once * Remove tests directory Currently there are no tests for the integration; will be added in a future commit. * Fix slaves not being updated Let the base class regularly poll, so that slaves are also updated * Refactor entity creation Create an auxiliary method to add a station to the entity list, in preparation to allowing extra stations. * Add possibility to manually add stations Add a new configuration option "stations" to manually add extra stations * Fix style issues Make the code more pythonic * Remove redundant code Implement suggestions from the code review * Change to platform component Remove the master/slave concept, in favor of a platform with dummy sensors. The platform takes care of contacting the server and fetching updates atomically, and updating the data on the sensors. * Rename ATTR_STATE Rename the attribute to "IS_OPEN", to avoid confusion with the sensor state. * Minor updates Combine two consecutive error logs into a single one. Update the sensor's icon * Separate address into different fields * Style updates Use "[]" syntax instead of ".get()" for required parameters Use warning log level for not available fuel types * Implement review comments Fix style issues Improve error messages Remove redundant options * Refactor using DataUpdateCoordinator Use the new DataUpdateCoordinator to fetch the global data from the API, instead of implementing an own method. Also fix comments from the PR * Implement PR comments Implement suggestions to improve code readability and keep the Home Assistant style. Also separate fetching data to an async thread
This commit is contained in:
parent
0431b983d2
commit
f53c94ed2a
@ -701,6 +701,7 @@ omit =
|
||||
homeassistant/components/tado/device_tracker.py
|
||||
homeassistant/components/tahoma/*
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/*
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
homeassistant/components/tautulli/sensor.py
|
||||
homeassistant/components/ted5000/sensor.py
|
||||
|
@ -347,6 +347,7 @@ homeassistant/components/synology_srm/* @aerialls
|
||||
homeassistant/components/syslog/* @fabaff
|
||||
homeassistant/components/tado/* @michaelarnauts
|
||||
homeassistant/components/tahoma/* @philklei
|
||||
homeassistant/components/tankerkoenig/* @guillempages
|
||||
homeassistant/components/tautulli/* @ludeeus
|
||||
homeassistant/components/tellduslive/* @fredrike
|
||||
homeassistant/components/template/* @PhracturedBlue @tetienne
|
||||
|
203
homeassistant/components/tankerkoenig/__init__.py
Executable file
203
homeassistant/components/tankerkoenig/__init__.py
Executable file
@ -0,0 +1,203 @@
|
||||
"""Ask tankerkoenig.de for petrol price information."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import pytankerkoenig
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_RADIUS,
|
||||
CONF_SCAN_INTERVAL,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
|
||||
from .const import CONF_FUEL_TYPES, CONF_STATIONS, DOMAIN, FUEL_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_RADIUS = 2
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
||||
): cv.time_period,
|
||||
vol.Optional(CONF_FUEL_TYPES, default=FUEL_TYPES): vol.All(
|
||||
cv.ensure_list, [vol.In(FUEL_TYPES)]
|
||||
),
|
||||
vol.Inclusive(
|
||||
CONF_LATITUDE,
|
||||
"coordinates",
|
||||
"Latitude and longitude must exist together",
|
||||
): cv.latitude,
|
||||
vol.Inclusive(
|
||||
CONF_LONGITUDE,
|
||||
"coordinates",
|
||||
"Latitude and longitude must exist together",
|
||||
): cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.All(
|
||||
cv.positive_int, vol.Range(min=1)
|
||||
),
|
||||
vol.Optional(CONF_STATIONS, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set the tankerkoenig component up."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
_LOGGER.debug("Setting up integration")
|
||||
|
||||
tankerkoenig = TankerkoenigData(hass, conf)
|
||||
|
||||
latitude = conf.get(CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = conf.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
radius = conf[CONF_RADIUS]
|
||||
additional_stations = conf[CONF_STATIONS]
|
||||
|
||||
setup_ok = await hass.async_add_executor_job(
|
||||
tankerkoenig.setup, latitude, longitude, radius, additional_stations
|
||||
)
|
||||
if not setup_ok:
|
||||
_LOGGER.error("Could not setup integration")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = tankerkoenig
|
||||
|
||||
hass.async_create_task(
|
||||
async_load_platform(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
DOMAIN,
|
||||
discovered=tankerkoenig.stations,
|
||||
hass_config=conf,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class TankerkoenigData:
|
||||
"""Get the latest data from the API."""
|
||||
|
||||
def __init__(self, hass, conf):
|
||||
"""Initialize the data object."""
|
||||
self._api_key = conf[CONF_API_KEY]
|
||||
self.stations = {}
|
||||
self.fuel_types = conf[CONF_FUEL_TYPES]
|
||||
self.update_interval = conf[CONF_SCAN_INTERVAL]
|
||||
self._hass = hass
|
||||
|
||||
def setup(self, latitude, longitude, radius, additional_stations):
|
||||
"""Set up the tankerkoenig API.
|
||||
|
||||
Read the initial data from the server, to initialize the list of fuel stations to monitor.
|
||||
"""
|
||||
_LOGGER.debug("Fetching data for (%s, %s) rad: %s", latitude, longitude, radius)
|
||||
try:
|
||||
data = pytankerkoenig.getNearbyStations(
|
||||
self._api_key, latitude, longitude, radius, "all", "dist"
|
||||
)
|
||||
except pytankerkoenig.customException as err:
|
||||
data = {"ok": False, "message": err, "exception": True}
|
||||
_LOGGER.debug("Received data: %s", data)
|
||||
if not data["ok"]:
|
||||
_LOGGER.error(
|
||||
"Setup for sensors was unsuccessful. Error occurred while fetching data from tankerkoenig.de: %s",
|
||||
data["message"],
|
||||
)
|
||||
return False
|
||||
|
||||
# Add stations found via location + radius
|
||||
nearby_stations = data["stations"]
|
||||
if not nearby_stations:
|
||||
if not additional_stations:
|
||||
_LOGGER.error(
|
||||
"Could not find any station in range."
|
||||
"Try with a bigger radius or manually specify stations in additional_stations"
|
||||
)
|
||||
return False
|
||||
_LOGGER.warning(
|
||||
"Could not find any station in range. Will only use manually specified stations"
|
||||
)
|
||||
else:
|
||||
for station in data["stations"]:
|
||||
self.add_station(station)
|
||||
|
||||
# Add manually specified additional stations
|
||||
for station_id in additional_stations:
|
||||
try:
|
||||
additional_station_data = pytankerkoenig.getStationData(
|
||||
self._api_key, station_id
|
||||
)
|
||||
except pytankerkoenig.customException as err:
|
||||
additional_station_data = {
|
||||
"ok": False,
|
||||
"message": err,
|
||||
"exception": True,
|
||||
}
|
||||
|
||||
if not additional_station_data["ok"]:
|
||||
_LOGGER.error(
|
||||
"Error when adding station %s:\n %s",
|
||||
station_id,
|
||||
additional_station_data["message"],
|
||||
)
|
||||
return False
|
||||
self.add_station(additional_station_data["station"])
|
||||
return True
|
||||
|
||||
async def fetch_data(self):
|
||||
"""Get the latest data from tankerkoenig.de."""
|
||||
_LOGGER.debug("Fetching new data from tankerkoenig.de")
|
||||
station_ids = list(self.stations)
|
||||
data = await self._hass.async_add_executor_job(
|
||||
pytankerkoenig.getPriceList, self._api_key, station_ids
|
||||
)
|
||||
|
||||
if data["ok"]:
|
||||
_LOGGER.debug("Received data: %s", data)
|
||||
if "prices" not in data:
|
||||
_LOGGER.error("Did not receive price information from tankerkoenig.de")
|
||||
raise TankerkoenigError("No prices in data")
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Error fetching data from tankerkoenig.de: %s", data["message"]
|
||||
)
|
||||
raise TankerkoenigError(data["message"])
|
||||
return data["prices"]
|
||||
|
||||
def add_station(self, station: dict):
|
||||
"""Add fuel station to the entity list."""
|
||||
station_id = station["id"]
|
||||
if station_id in self.stations:
|
||||
_LOGGER.warning(
|
||||
"Sensor for station with id %s was already created", station_id
|
||||
)
|
||||
return
|
||||
|
||||
self.stations[station_id] = station
|
||||
_LOGGER.debug("add_station called for station: %s", station)
|
||||
|
||||
|
||||
class TankerkoenigError(HomeAssistantError):
|
||||
"""An error occurred while contacting tankerkoenig.de."""
|
9
homeassistant/components/tankerkoenig/const.py
Normal file
9
homeassistant/components/tankerkoenig/const.py
Normal file
@ -0,0 +1,9 @@
|
||||
"""Constants for the tankerkoenig integration."""
|
||||
|
||||
DOMAIN = "tankerkoenig"
|
||||
NAME = "tankerkoenig"
|
||||
|
||||
CONF_FUEL_TYPES = "fuel_types"
|
||||
CONF_STATIONS = "stations"
|
||||
|
||||
FUEL_TYPES = ["e5", "e10", "diesel"]
|
10
homeassistant/components/tankerkoenig/manifest.json
Executable file
10
homeassistant/components/tankerkoenig/manifest.json
Executable file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "tankerkoenig",
|
||||
"name": "Tankerkoenig",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tankerkoenig",
|
||||
"requirements": ["pytankerkoenig==0.0.6"],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@guillempages"
|
||||
]
|
||||
}
|
150
homeassistant/components/tankerkoenig/sensor.py
Executable file
150
homeassistant/components/tankerkoenig/sensor.py
Executable file
@ -0,0 +1,150 @@
|
||||
"""Tankerkoenig sensor integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, NAME
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BRAND = "brand"
|
||||
ATTR_CITY = "city"
|
||||
ATTR_FUEL_TYPE = "fuel_type"
|
||||
ATTR_HOUSE_NUMBER = "house_number"
|
||||
ATTR_IS_OPEN = "is_open"
|
||||
ATTR_POSTCODE = "postcode"
|
||||
ATTR_STATION_NAME = "station_name"
|
||||
ATTR_STREET = "street"
|
||||
ATTRIBUTION = "Data provided by https://creativecommons.tankerkoenig.de"
|
||||
|
||||
ICON = "mdi:gas-station"
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the tankerkoenig sensors."""
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
tankerkoenig = hass.data[DOMAIN]
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
return await tankerkoenig.fetch_data()
|
||||
except LookupError:
|
||||
raise UpdateFailed
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=NAME,
|
||||
update_method=async_update_data,
|
||||
update_interval=tankerkoenig.update_interval,
|
||||
)
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_refresh()
|
||||
|
||||
stations = discovery_info.values()
|
||||
entities = []
|
||||
for station in stations:
|
||||
for fuel in tankerkoenig.fuel_types:
|
||||
if fuel not in station:
|
||||
_LOGGER.warning(
|
||||
"Station %s does not offer %s fuel", station["id"], fuel
|
||||
)
|
||||
continue
|
||||
sensor = FuelPriceSensor(
|
||||
fuel, station, coordinator, f"{NAME}_{station['name']}_{fuel}"
|
||||
)
|
||||
entities.append(sensor)
|
||||
_LOGGER.debug("Added sensors %s", entities)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class FuelPriceSensor(Entity):
|
||||
"""Contains prices for fuel in a given station."""
|
||||
|
||||
def __init__(self, fuel_type, station, coordinator, name):
|
||||
"""Initialize the sensor."""
|
||||
self._station = station
|
||||
self._station_id = station["id"]
|
||||
self._fuel_type = fuel_type
|
||||
self._coordinator = coordinator
|
||||
self._name = name
|
||||
self._latitude = station["lat"]
|
||||
self._longitude = station["lng"]
|
||||
self._city = station["place"]
|
||||
self._house_number = station["houseNumber"]
|
||||
self._postcode = station["postCode"]
|
||||
self._street = station["street"]
|
||||
self._price = station[fuel_type]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend."""
|
||||
return ICON
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return unit of measurement."""
|
||||
return "€"
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll. Coordinator notifies of updates."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
# key Fuel_type is not available when the fuel station is closed, use "get" instead of "[]" to avoid exceptions
|
||||
return self._coordinator.data[self._station_id].get(self._fuel_type)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the attributes of the device."""
|
||||
data = self._coordinator.data[self._station_id]
|
||||
|
||||
attrs = {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
ATTR_BRAND: self._station["brand"],
|
||||
ATTR_FUEL_TYPE: self._fuel_type,
|
||||
ATTR_STATION_NAME: self._station["name"],
|
||||
ATTR_STREET: self._street,
|
||||
ATTR_HOUSE_NUMBER: self._house_number,
|
||||
ATTR_POSTCODE: self._postcode,
|
||||
ATTR_CITY: self._city,
|
||||
ATTR_LATITUDE: self._latitude,
|
||||
ATTR_LONGITUDE: self._longitude,
|
||||
}
|
||||
if data is not None and "status" in data:
|
||||
attrs[ATTR_IS_OPEN] = data["status"] == "open"
|
||||
return attrs
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return if entity is available."""
|
||||
return self._coordinator.last_update_success
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""When entity is added to hass."""
|
||||
self._coordinator.async_add_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""When entity will be removed from hass."""
|
||||
self._coordinator.async_remove_listener(self.async_write_ha_state)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the entity."""
|
||||
await self._coordinator.async_request_refresh()
|
@ -1547,6 +1547,9 @@ pysupla==0.0.3
|
||||
# homeassistant.components.syncthru
|
||||
pysyncthru==0.5.0
|
||||
|
||||
# homeassistant.components.tankerkoenig
|
||||
pytankerkoenig==0.0.6
|
||||
|
||||
# homeassistant.components.tautulli
|
||||
pytautulli==0.5.0
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user