diff --git a/custom_components/fuel_prices/__init__.py b/custom_components/fuel_prices/__init__.py index 553e84f..15109ac 100644 --- a/custom_components/fuel_prices/__init__.py +++ b/custom_components/fuel_prices/__init__.py @@ -29,7 +29,7 @@ from .coordinator import FuelPricesCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.DEVICE_TRACKER] +PLATFORMS = [Platform.SENSOR] def _build_configured_areas(hass_areas: dict) -> list[dict]: @@ -74,6 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_listener(hass: HomeAssistant, entry: ConfigEntry): """Update listener.""" + await hass.data[DOMAIN][entry.entry_id].api.client_session.close() await hass.config_entries.async_reload(entry.entry_id) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -112,12 +113,12 @@ async def handle_fuel_location_lookup(call: ServiceCall) -> ServiceResponse: ) except ValueError as err: raise HomeAssistantError("Country not available for fuel data.") from err - locations_built = [] - for loc in locations: - await loc.dynamic_build_fuels() - locations_built.append(loc.__dict__()) - return {"items": locations_built, "sources": entry.data.get("sources", [])} + return {"items": locations, "sources": entry.data.get("sources", [])} + + async def handle_force_update(call: ServiceCall): + """Handle a request to force update.""" + await fuel_prices.update(force=True) hass.services.async_register( DOMAIN, @@ -133,14 +134,16 @@ async def handle_fuel_location_lookup(call: ServiceCall) -> ServiceResponse: supports_response=SupportsResponse.ONLY, ) + hass.services.async_register(DOMAIN, "force_update", handle_force_update) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" _LOGGER.debug("Unloading config entry %s", entry.entry_id) + await hass.data[DOMAIN][entry.entry_id].api.client_session.close() if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - await hass.data[DOMAIN][entry.entry_id].api.client_session.close() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/custom_components/fuel_prices/config_flow.py b/custom_components/fuel_prices/config_flow.py index e3f0ff2..2d583bc 100644 --- a/custom_components/fuel_prices/config_flow.py +++ b/custom_components/fuel_prices/config_flow.py @@ -107,6 +107,8 @@ async def async_step_sources(self, user_input: dict[str, Any] | None = None): """Set data source config.""" if user_input is not None: self.configured_sources = user_input[CONF_SOURCES] + self.timeout = user_input[CONF_TIMEOUT] + self.interval = user_input[CONF_SCAN_INTERVAL] return await self.async_step_main_menu(None) return self.async_show_form( step_id="sources", @@ -138,7 +140,7 @@ async def async_step_sources(self, user_input: dict[str, Any] | None = None): ): selector.NumberSelector( selector.NumberSelectorConfig( mode=selector.NumberSelectorMode.BOX, - min=120, + min=1, max=1440, unit_of_measurement="m", ) @@ -287,6 +289,8 @@ async def async_step_finished(self, user_input: dict[str, Any] | None = None): else: user_input[CONF_SOURCES] = list(SOURCE_MAP) user_input[CONF_AREAS] = self.configured_areas + user_input[CONF_SCAN_INTERVAL] = self.interval + user_input[CONF_TIMEOUT] = self.timeout return self.async_create_entry(title=NAME, data=user_input) return self.async_show_form(step_id="finished", errors=errors, last_step=True) @@ -304,12 +308,24 @@ class FuelPricesOptionsFlow(config_entries.OptionsFlow): configured_sources = [] configuring_area = {} configuring_index = -1 + timeout = 10 + interval = 1440 def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - self.configured_areas = self.config_entry.data.get(CONF_AREAS, []) - self.configured_sources = self.config_entry.data.get(CONF_SOURCES, []) + self.configured_areas = config_entry.options.get( + CONF_AREAS, config_entry.data.get(CONF_AREAS, []) + ) + self.configured_sources = config_entry.options.get( + CONF_SOURCES, config_entry.data.get(CONF_SOURCES, []) + ) + self.timeout = config_entry.options.get( + CONF_TIMEOUT, config_entry.data.get(CONF_TIMEOUT, 10) + ) + self.interval = config_entry.options.get( + CONF_SCAN_INTERVAL, config_entry.data.get(CONF_SCAN_INTERVAL, 1440) + ) @property def configured_area_names(self) -> list[str]: @@ -338,6 +354,8 @@ async def async_step_sources(self, user_input: dict[str, Any] | None = None): """Set data source config.""" if user_input is not None: self.configured_sources = user_input[CONF_SOURCES] + self.timeout = user_input[CONF_TIMEOUT] + self.interval = user_input[CONF_SCAN_INTERVAL] return await self.async_step_main_menu(None) return self.async_show_form( step_id="sources", @@ -351,7 +369,29 @@ async def async_step_sources(self, user_input: dict[str, Any] | None = None): options=list(SOURCE_MAP), multiple=True, ) - ) + ), + vol.Optional( + CONF_TIMEOUT, + default=self.timeout, + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + min=5, + max=60, + unit_of_measurement="s", + ) + ), + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.interval, + ): selector.NumberSelector( + selector.NumberSelectorConfig( + mode=selector.NumberSelectorMode.BOX, + min=1, + max=1440, + unit_of_measurement="m", + ) + ), } ), ) @@ -495,6 +535,8 @@ async def async_step_finished(self, user_input: dict[str, Any] | None = None): else list(SOURCE_MAP) ) user_input[CONF_AREAS] = self.configured_areas + user_input[CONF_SCAN_INTERVAL] = self.interval + user_input[CONF_TIMEOUT] = self.timeout return self.async_create_entry(title=NAME, data=user_input) return self.async_show_form(step_id="finished", errors=errors, last_step=True) diff --git a/custom_components/fuel_prices/device_tracker.py b/custom_components/fuel_prices/device_tracker.py deleted file mode 100644 index abe7bfb..0000000 --- a/custom_components/fuel_prices/device_tracker.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Device tracker for fuel prices.""" -from __future__ import annotations - -import logging -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_NAME -from homeassistant.components.device_tracker.config_entry import ( - BaseTrackerEntity, - SourceType, - ATTR_SOURCE_TYPE, - ATTR_LATITUDE, - ATTR_LONGITUDE, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from pyfuelprices.const import PROP_FUEL_LOCATION_SOURCE -from .const import CONF_AREAS, DOMAIN -from .entity import FeulStationEntity -from .coordinator import FuelPricesCoordinator - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Integration platform creation.""" - cooridinator: FuelPricesCoordinator = hass.data[DOMAIN][entry.entry_id] - areas = entry.data[CONF_AREAS] - entities = [] - found_entities = [] - for area in areas: - _LOGGER.debug("Registering entities for area %s", area[CONF_NAME]) - for station in await cooridinator.api.find_fuel_locations_from_point( - coordinates=(area[CONF_LATITUDE], area[CONF_LONGITUDE]), - radius=area[CONF_RADIUS], - ): - if station.id not in found_entities: - entities.append( - FeulStationTracker( - coordinator=cooridinator, - fuel_station_id=station.id, - entity_id="devicetracker", - source=station.props[PROP_FUEL_LOCATION_SOURCE], - ) - ) - found_entities.append(station.id) - - async_add_entities(entities, True) - - -class FeulStationTracker(FeulStationEntity, BaseTrackerEntity): - """A fuel station tracker entity.""" - - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._fuel_station.name - - @property - def location_accuracy(self) -> int: - """Return the location accuracy of the device. - - Value in meters. - """ - return 0 - - @property - def state(self) -> str | None: - """Return the state of the device.""" - if self.location_name is not None: - return self.location_name - - @property - def _get_fuels(self) -> dict: - """Return list of fuels.""" - output = {} - for fuel in self._fuel_station.available_fuels: - output[fuel.fuel_type] = fuel.cost - return output - - @property - def latitude(self) -> float: - """Return the latitude.""" - return self._fuel_station.lat - - @property - def longitude(self) -> float: - """Return the longitude.""" - return self._fuel_station.long - - @property - def location_name(self) -> str: - """Return the name of the location.""" - return self._fuel_station.name - - @property - def source_type(self) -> SourceType: - """Return the source type.""" - return SourceType.GPS - - @property - def state_attributes(self) -> dict[str, StateType]: - """Return the fuel location prices.""" - attr: dict[str, StateType] = { - ATTR_SOURCE_TYPE: self.source_type, - **self._get_fuels, - **self._fuel_station.__dict__(), - } - if self.latitude is not None and self.longitude is not None: - attr[ATTR_LATITUDE] = self.latitude - attr[ATTR_LONGITUDE] = self.longitude - - return attr diff --git a/custom_components/fuel_prices/entity.py b/custom_components/fuel_prices/entity.py index df3fdcc..6ef0d32 100644 --- a/custom_components/fuel_prices/entity.py +++ b/custom_components/fuel_prices/entity.py @@ -1,4 +1,5 @@ """Fuel Price entity base type.""" + from __future__ import annotations from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -6,7 +7,7 @@ from .coordinator import FuelPricesCoordinator -class FeulStationEntity(CoordinatorEntity): +class FuelStationEntity(CoordinatorEntity): """Represents a fuel station.""" def __init__( diff --git a/custom_components/fuel_prices/manifest.json b/custom_components/fuel_prices/manifest.json index 044de5b..cbd42a3 100644 --- a/custom_components/fuel_prices/manifest.json +++ b/custom_components/fuel_prices/manifest.json @@ -12,7 +12,7 @@ "requirements": [ "reverse-geocode==1.4.1", "these-united-states==1.1.0.21", - "pyfuelprices==2.1.11" + "pyfuelprices==2.2.9" ], "ssdp": [], "version": "0.0.0", diff --git a/custom_components/fuel_prices/sensor.py b/custom_components/fuel_prices/sensor.py new file mode 100644 index 0000000..a8725dd --- /dev/null +++ b/custom_components/fuel_prices/sensor.py @@ -0,0 +1,81 @@ +"""Sensor for fuel prices.""" + +from __future__ import annotations + + +import logging + +from collections.abc import Mapping +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from pyfuelprices.const import PROP_FUEL_LOCATION_SOURCE +from .const import CONF_AREAS, DOMAIN +from .entity import FuelStationEntity +from .coordinator import FuelPricesCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Integration platform creation.""" + cooridinator: FuelPricesCoordinator = hass.data[DOMAIN][entry.entry_id] + areas = entry.data[CONF_AREAS] + entities = [] + found_entities = [] + for area in areas: + _LOGGER.debug("Registering entities for area %s", area[CONF_NAME]) + for station in await cooridinator.api.find_fuel_locations_from_point( + coordinates=(area[CONF_LATITUDE], area[CONF_LONGITUDE]), + radius=area[CONF_RADIUS], + ): + if station["id"] not in found_entities: + entities.append( + FeulStationTracker( + coordinator=cooridinator, + fuel_station_id=station["id"], + entity_id="devicetracker", + source=station["props"][PROP_FUEL_LOCATION_SOURCE], + ) + ) + found_entities.append(station["id"]) + + async_add_entities(entities, True) + + +class FeulStationTracker(FuelStationEntity, SensorEntity): + """A fuel station entity.""" + + @property + def native_value(self) -> str: + """Return the native value of the entity.""" + return self._fuel_station.name + + @property + def _get_fuels(self) -> dict: + """Return list of fuels.""" + output = {} + for fuel in self._fuel_station.available_fuels: + output[fuel.fuel_type] = fuel.cost + return output + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return extra state attributes.""" + return {**self._fuel_station.__dict__(), **self._get_fuels} + + @property + def icon(self) -> str: + """Return entity icon.""" + return "mdi:gas-station" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._fuel_station.name diff --git a/custom_components/fuel_prices/strings.json b/custom_components/fuel_prices/strings.json index ae7a634..1f4dd84 100644 --- a/custom_components/fuel_prices/strings.json +++ b/custom_components/fuel_prices/strings.json @@ -16,7 +16,8 @@ } }, "area_menu": { - "title": "Configure areas to register devices and sensors, by default your home location has already been added automatically with a radius of 20 miles, this can be removed or changed if needed.", + "title": "Configure areas to register devices and sensors.", + "description": "By default your home location has already been added automatically with a radius of 20 miles, this can be removed or changed if needed.", "menu_options": { "area_create": "Create an area", "area_update_select": "Update an area", diff --git a/custom_components/fuel_prices/translations/en.json b/custom_components/fuel_prices/translations/en.json index ae7a634..1f4dd84 100644 --- a/custom_components/fuel_prices/translations/en.json +++ b/custom_components/fuel_prices/translations/en.json @@ -16,7 +16,8 @@ } }, "area_menu": { - "title": "Configure areas to register devices and sensors, by default your home location has already been added automatically with a radius of 20 miles, this can be removed or changed if needed.", + "title": "Configure areas to register devices and sensors.", + "description": "By default your home location has already been added automatically with a radius of 20 miles, this can be removed or changed if needed.", "menu_options": { "area_create": "Create an area", "area_update_select": "Update an area", diff --git a/requirements.txt b/requirements.txt index 429674c..18e3cbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ colorlog==6.7.0 homeassistant==2023.8.0 pip>=21.0,<23.2 ruff==0.0.292 -pyfuelprices==2.1.10 \ No newline at end of file +pyfuelprices==2.2.9 \ No newline at end of file