Skip to content

Commit

Permalink
Feature: Cheapest Fuel Station Sensors (#18)
Browse files Browse the repository at this point in the history
* start #6

* add required config options for #6

* implement #6

* fix lint errors
  • Loading branch information
pantherale0 authored Jun 24, 2024
1 parent c0d2b50 commit f7a0d29
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 40 deletions.
56 changes: 45 additions & 11 deletions custom_components/fuel_prices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
CONF_RADIUS,
CONF_TIMEOUT,
CONF_SCAN_INTERVAL,
CONF_NAME
)
from homeassistant.core import (
HomeAssistant,
Expand All @@ -25,7 +26,7 @@
)
from homeassistant.exceptions import HomeAssistantError

from .const import DOMAIN, CONF_AREAS, CONF_SOURCES
from .const import DOMAIN, CONF_AREAS, CONF_SOURCES, CONF_CHEAPEST_SENSORS, CONF_CHEAPEST_SENSORS_COUNT, CONF_CHEAPEST_SENSORS_FUEL_TYPE
from .coordinator import FuelPricesCoordinator

_LOGGER = logging.getLogger(__name__)
Expand All @@ -48,7 +49,8 @@ def _build_configured_areas(hass_areas: dict) -> list[dict]:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Create ConfigEntry."""
_LOGGER.debug("Got request to setup entry.")
sources = entry.options.get(CONF_SOURCES, entry.data.get(CONF_SOURCES, None))
sources = entry.options.get(
CONF_SOURCES, entry.data.get(CONF_SOURCES, None))
areas = entry.options.get(CONF_AREAS, entry.data.get(CONF_AREAS, None))
timeout = entry.options.get(CONF_TIMEOUT, entry.data.get(CONF_TIMEOUT, 30))
update_interval = entry.options.get(
Expand All @@ -72,13 +74,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error(err)
raise CannotConnect from err

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))

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

async def handle_fuel_lookup(call: ServiceCall) -> ServiceResponse:
Expand All @@ -97,7 +92,8 @@ async def handle_fuel_lookup(call: ServiceCall) -> ServiceResponse:
)
}
except ValueError as err:
raise HomeAssistantError("Country not available for fuel data.") from err
raise HomeAssistantError(
"Country not available for fuel data.") from err

async def handle_fuel_location_lookup(call: ServiceCall) -> ServiceResponse:
"""Handle a fuel location lookup call."""
Expand All @@ -112,7 +108,8 @@ async def handle_fuel_location_lookup(call: ServiceCall) -> ServiceResponse:
(lat, long), radius
)
except ValueError as err:
raise HomeAssistantError("Country not available for fuel data.") from err
raise HomeAssistantError(
"Country not available for fuel data.") from err

return {"items": locations, "sources": entry.data.get("sources", [])}

Expand All @@ -136,6 +133,12 @@ async def handle_force_update(call: ServiceCall):

hass.services.async_register(DOMAIN, "force_update", handle_force_update)

async def update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Update listener."""
await hass.config_entries.async_reload(entry.entry_id)

entry.async_on_unload(entry.add_update_listener(update_listener))

return True


Expand All @@ -148,6 +151,37 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

return unload_ok

# Example migration function


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating configuration from version %s",
config_entry.version)

if config_entry.version > 1:
# This means the user has downgraded from a future version
return False

if config_entry.version == 1:

new_data = {**config_entry.data}
if config_entry.options:
new_data = {**config_entry.options}
for area in new_data[CONF_AREAS]:
_LOGGER.debug("Upgrading area definition for %s", area[CONF_NAME])
area[CONF_CHEAPEST_SENSORS] = False
area[CONF_CHEAPEST_SENSORS_COUNT] = 5
area[CONF_CHEAPEST_SENSORS_FUEL_TYPE] = ""

hass.config_entries.async_update_entry(
config_entry, data=new_data, version=2)

_LOGGER.debug("Migration to configuration version %s successful",
config_entry.version)

return True


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
88 changes: 76 additions & 12 deletions custom_components/fuel_prices/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
CONF_SCAN_INTERVAL,
)

from .const import DOMAIN, NAME, CONF_AREAS, CONF_SOURCES, CONF_STATE_VALUE
from .const import DOMAIN, NAME, CONF_AREAS, CONF_SOURCES, CONF_STATE_VALUE, CONF_CHEAPEST_SENSORS, CONF_CHEAPEST_SENSORS_COUNT, CONF_CHEAPEST_SENSORS_FUEL_TYPE

_LOGGER = logging.getLogger(__name__)

Expand All @@ -44,14 +44,24 @@
vol.Inclusive(
CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together"
): cv.longitude,
vol.Optional(CONF_CHEAPEST_SENSORS, default=False): selector.BooleanSelector(),
vol.Optional(CONF_CHEAPEST_SENSORS_COUNT, default=5): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
min=1,
max=10,
step=1
)
),
vol.Optional(CONF_CHEAPEST_SENSORS_FUEL_TYPE, default=""): selector.TextSelector(),
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

VERSION = 1
VERSION = 2
configured_areas: list[dict] = []
configured_sources = []
configuring_area = {}
Expand Down Expand Up @@ -172,6 +182,9 @@ async def async_step_area_create(self, user_input: dict[str, Any] | None = None)
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
CONF_RADIUS: user_input[CONF_RADIUS],
CONF_CHEAPEST_SENSORS: user_input[CONF_CHEAPEST_SENSORS],
CONF_CHEAPEST_SENSORS_COUNT: user_input[CONF_CHEAPEST_SENSORS_COUNT],
CONF_CHEAPEST_SENSORS_FUEL_TYPE: user_input[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
}
)
return await self.async_step_area_menu()
Expand Down Expand Up @@ -217,6 +230,9 @@ async def async_step_area_update(self, user_input: dict[str, Any] | None = None)
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
CONF_RADIUS: user_input[CONF_RADIUS],
CONF_CHEAPEST_SENSORS: user_input[CONF_CHEAPEST_SENSORS],
CONF_CHEAPEST_SENSORS_COUNT: user_input[CONF_CHEAPEST_SENSORS_COUNT],
CONF_CHEAPEST_SENSORS_FUEL_TYPE: user_input[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
}
)
return await self.async_step_area_menu()
Expand Down Expand Up @@ -250,6 +266,25 @@ async def async_step_area_update(self, user_input: dict[str, Any] | None = None)
"Latitude and longitude must exist together",
default=self.configuring_area[CONF_LONGITUDE],
): cv.longitude,
vol.Optional(
CONF_CHEAPEST_SENSORS,
default=self.configuring_area[CONF_CHEAPEST_SENSORS]
): selector.BooleanSelector(),
vol.Optional(
CONF_CHEAPEST_SENSORS_COUNT,
default=self.configuring_area[CONF_CHEAPEST_SENSORS_COUNT]
): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
min=1,
max=10,
step=1
)
),
vol.Optional(
CONF_CHEAPEST_SENSORS_FUEL_TYPE,
default=self.configuring_area[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
): selector.TextSelector()
}
),
errors=errors,
Expand Down Expand Up @@ -342,6 +377,19 @@ def configured_area_names(self) -> list[str]:
items.append(area["name"])
return items

async def _async_create_entry(self) -> config_entries.FlowResult:
"""Create an entry."""
return self.async_create_entry(
title=self.config_entry.title,
data={
CONF_AREAS: self.configured_areas,
CONF_SOURCES: self.configured_sources,
CONF_SCAN_INTERVAL: self.interval,
CONF_TIMEOUT: self.timeout,
CONF_STATE_VALUE: self.state_value
}
)

async def async_step_init(self, _: None = None):
"""User init option flow."""
return await self.async_step_main_menu()
Expand Down Expand Up @@ -435,6 +483,9 @@ async def async_step_area_create(self, user_input: dict[str, Any] | None = None)
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
CONF_RADIUS: user_input[CONF_RADIUS],
CONF_CHEAPEST_SENSORS: user_input[CONF_CHEAPEST_SENSORS],
CONF_CHEAPEST_SENSORS_COUNT: user_input[CONF_CHEAPEST_SENSORS_COUNT],
CONF_CHEAPEST_SENSORS_FUEL_TYPE: user_input[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
}
)
return await self.async_step_area_menu()
Expand Down Expand Up @@ -480,6 +531,9 @@ async def async_step_area_update(self, user_input: dict[str, Any] | None = None)
CONF_LATITUDE: user_input[CONF_LATITUDE],
CONF_LONGITUDE: user_input[CONF_LONGITUDE],
CONF_RADIUS: user_input[CONF_RADIUS],
CONF_CHEAPEST_SENSORS: user_input[CONF_CHEAPEST_SENSORS],
CONF_CHEAPEST_SENSORS_COUNT: user_input[CONF_CHEAPEST_SENSORS_COUNT],
CONF_CHEAPEST_SENSORS_FUEL_TYPE: user_input[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
}
)
return await self.async_step_area_menu()
Expand Down Expand Up @@ -513,6 +567,25 @@ async def async_step_area_update(self, user_input: dict[str, Any] | None = None)
"Latitude and longitude must exist together",
default=self.configuring_area[CONF_LONGITUDE],
): cv.longitude,
vol.Optional(
CONF_CHEAPEST_SENSORS,
default=self.configuring_area[CONF_CHEAPEST_SENSORS]
): selector.BooleanSelector(),
vol.Optional(
CONF_CHEAPEST_SENSORS_COUNT,
default=self.configuring_area[CONF_CHEAPEST_SENSORS_COUNT]
): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
min=1,
max=10,
step=1
)
),
vol.Optional(
CONF_CHEAPEST_SENSORS_FUEL_TYPE,
default=self.configuring_area[CONF_CHEAPEST_SENSORS_FUEL_TYPE]
): selector.TextSelector()
}
),
errors=errors,
Expand Down Expand Up @@ -546,16 +619,7 @@ async def async_step_finished(self, user_input: dict[str, Any] | None = None):
"""Save configuration."""
errors: dict[str, str] = {}
if user_input is not None:
user_input[CONF_SOURCES] = (
self.configured_sources
if len(self.configured_sources) > 0
else list(SOURCE_MAP)
)
user_input[CONF_AREAS] = self.configured_areas
user_input[CONF_SCAN_INTERVAL] = self.interval
user_input[CONF_TIMEOUT] = self.timeout
user_input[CONF_STATE_VALUE] = self.state_value
return self.async_create_entry(title=NAME, data=user_input)
return await self._async_create_entry()
return self.async_show_form(step_id="finished", errors=errors, last_step=True)


Expand Down
4 changes: 4 additions & 0 deletions custom_components/fuel_prices/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
CONF_SOURCES = "sources"

CONF_STATE_VALUE = "state"

CONF_CHEAPEST_SENSORS = "cheapest_stations"
CONF_CHEAPEST_SENSORS_COUNT = "cheapest_stations_count"
CONF_CHEAPEST_SENSORS_FUEL_TYPE = "cheapest_stations_fuel_type"
20 changes: 20 additions & 0 deletions custom_components/fuel_prices/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ def _fuel_station(self):
def unique_id(self) -> str | None:
"""Return unique ID."""
return f"fuelprices_{self._fuel_station_id}_{self._entity_id}"


class CheapestFuelEntity(CoordinatorEntity):
"""Represents a fuel."""

def __init__(
self, coordinator: FuelPricesCoordinator, count: str, area: str, fuel: str, coords: tuple, radius: float):
"""Initialize."""
super().__init__(coordinator)
self.coordinator: FuelPricesCoordinator = coordinator
self._count = count
self._area = area
self._coords = coords
self._radius = radius
self._fuel = fuel

@property
def unique_id(self) -> str | None:
"""Return unique ID."""
return f"fuelprices_cheapest_{self._fuel}_{self._count}_{self._area}"
2 changes: 1 addition & 1 deletion custom_components/fuel_prices/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"issue_tracker": "https://github.com/pantherale0/ha-fuelprices/issues",
"requirements": [
"these-united-states==1.1.0.21",
"pyfuelprices==2.3.4"
"pyfuelprices==2.5.1"
],
"ssdp": [],
"version": "0.0.0",
Expand Down
Loading

0 comments on commit f7a0d29

Please sign in to comment.