-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add new integration here_weather #28910
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
Closed
Closed
Changes from 53 commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
c8c0749
wip here_weather
eifinger 31e7c2e
first full version of here_weather
eifinger bdad7e7
execute script.hassfest
eifinger acd3e67
fix tests
eifinger e1b26d8
remove redundant update interval constants
eifinger b639ca8
Migrate to api_key
eifinger 493ac35
script.gen_requirements_all
eifinger 5eff618
fix mock url
eifinger 5e32391
Implement Config Flow
eifinger 75bf364
bump herepy to 3.0.1
eifinger 0d9e999
Increase CodeCov
eifinger f868a90
Remove weather platform
eifinger cae2ca3
bump herepy to 3.0.2
eifinger 37b3e6d
update fixture for here_travel_time for 3.0.2
eifinger 18b7dbb
Use more common strings
eifinger 0f460ed
Apply suggestions from code review
eifinger 3c26fc3
fix import patch
eifinger 1f481c8
Apply suggestions
eifinger a667352
Add weather platform and multiple coordinators
eifinger e044a4b
Clean up unused entries in config_flow
eifinger 0efd552
Remove mode in tests
eifinger 622a3b6
Test unload correctly
eifinger a8b9396
Add iot_class to manifest
eifinger 59af2af
Fix mypy
eifinger 131f995
Fix tests
eifinger de80121
Apply suggestions from code review
eifinger 5b9f991
style: rename config_entry to entry
eifinger b617a61
remove None check in get_attribute_from_here_data
eifinger 1d828e7
simplify except KeyError
eifinger 8ed8ce3
Return correct types for weather attributes
eifinger b7a2ed7
Only call async_add_entities once
eifinger 6703563
Rename config_entry to entry
eifinger 8fadeb3
Use .items() to iterate
eifinger 8c3d47d
Use generator expression
eifinger 5e0c878
remove title from strings.json
eifinger bf7f9ad
remove fire start event
eifinger b8b8e74
Move test_unload_entry
eifinger ff12f84
Inherit from SensorEntity
eifinger 34104ed
Use known api keys from entries
eifinger 1315639
Set unique id
eifinger 8a0dd23
remove CONNECTION_CLASS
eifinger 76f1ce5
remove localTime
eifinger 4094957
add device_class
eifinger 388ee89
Remove options
eifinger a1ae5ba
Fix unique_id and state
eifinger c95d5e4
herepy 3.5.3 for here_travel_time
eifinger 9a9f9fe
Add test for observation
eifinger ac010a6
Simplify try block
eifinger e1b7683
Use PERCENTAGE
eifinger 32ed66d
Use relative import
eifinger c4a62e0
Use custom UpdateCoordinator
eifinger c8e5b2b
Fix known_api_keys for configflow
eifinger b89c110
Remove unused constants
eifinger 8f53aa9
fix typo
eifinger 6fe8749
Remove known_api_key feature
eifinger 1720b37
Remove update_rate throttling
eifinger File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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,129 @@ | ||
| """The here_weather component.""" | ||
| from __future__ import annotations | ||
|
|
||
| from datetime import timedelta | ||
| import logging | ||
|
|
||
| import async_timeout | ||
| import herepy | ||
|
|
||
| from homeassistant.config_entries import ConfigEntry | ||
| from homeassistant.const import ( | ||
| CONF_API_KEY, | ||
| CONF_LATITUDE, | ||
| CONF_LONGITUDE, | ||
| CONF_UNIT_SYSTEM_METRIC, | ||
| ) | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
|
||
| from .const import ( | ||
| CONF_MODES, | ||
| DEFAULT_SCAN_INTERVAL, | ||
| DOMAIN, | ||
| MAX_UPDATE_RATE_FOR_ONE_CLIENT, | ||
| ) | ||
| from .utils import active_here_clients | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PLATFORMS = ["sensor", "weather"] | ||
|
|
||
|
|
||
| async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Set up here_weather from a config entry.""" | ||
| here_weather_coordinators = {} | ||
| for mode in CONF_MODES: | ||
| coordinator = HEREWeatherDataUpdateCoordinator(hass, entry, mode) | ||
| await coordinator.async_config_entry_first_refresh() | ||
| here_weather_coordinators[mode] = coordinator | ||
| hass.data.setdefault(DOMAIN, {})[entry.entry_id] = here_weather_coordinators | ||
|
|
||
| hass.config_entries.async_setup_platforms(entry, PLATFORMS) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
| """Unload a config entry.""" | ||
| unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
| if unload_ok: | ||
| hass.data[DOMAIN].pop(entry.entry_id) | ||
|
|
||
| return unload_ok | ||
|
|
||
|
|
||
| class HEREWeatherDataUpdateCoordinator(DataUpdateCoordinator): | ||
| """Get the latest data from HERE.""" | ||
|
|
||
| def __init__(self, hass: HomeAssistant, entry: ConfigEntry, mode: str) -> None: | ||
| """Initialize the data object.""" | ||
| self.here_client = herepy.DestinationWeatherApi(entry.data[CONF_API_KEY]) | ||
| self.latitude = entry.data[CONF_LATITUDE] | ||
| self.longitude = entry.data[CONF_LONGITUDE] | ||
| self.weather_product_type = herepy.WeatherProductType[mode] | ||
|
|
||
| super().__init__( | ||
| hass, | ||
| _LOGGER, | ||
| name=DOMAIN, | ||
| update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), | ||
| ) | ||
|
|
||
| def number_of_listeners(self) -> int: | ||
| """Return the number ob active listeners registered against this coordinator.""" | ||
| return len(self._listeners) | ||
|
|
||
| async def _async_update_data(self) -> list: | ||
| """Perform data update.""" | ||
| try: | ||
| async with async_timeout.timeout(10): | ||
| data = await self.hass.async_add_executor_job(self._get_data) | ||
| self._set_update_interval() | ||
| return data | ||
| except herepy.InvalidRequestError as error: | ||
| raise UpdateFailed( | ||
| f"Unable to fetch data from HERE: {error.message}" | ||
| ) from error | ||
|
|
||
| def _get_data(self): | ||
| """Get the latest data from HERE.""" | ||
| is_metric = self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC | ||
| data = self.here_client.weather_for_coordinates( | ||
| self.latitude, | ||
| self.longitude, | ||
| self.weather_product_type, | ||
| metric=is_metric, | ||
| ) | ||
| return extract_data_from_payload_for_product_type( | ||
| data, self.weather_product_type | ||
| ) | ||
|
|
||
| def _set_update_interval(self) -> int: | ||
| """Throttle the default update rate based on the number of active clients.""" | ||
| if ( | ||
| update_interval := ( | ||
| active_here_clients(self.hass) * MAX_UPDATE_RATE_FOR_ONE_CLIENT * 2 | ||
| ) | ||
| ) > DEFAULT_SCAN_INTERVAL: | ||
| _LOGGER.debug("Setting update_interval to %s", update_interval) | ||
| return update_interval | ||
| return DEFAULT_SCAN_INTERVAL | ||
|
|
||
|
|
||
| def extract_data_from_payload_for_product_type( | ||
| data: herepy.DestinationWeatherResponse, product_type: herepy.WeatherProductType | ||
| ) -> list: | ||
| """Extract the actual data from the HERE payload.""" | ||
| if product_type == herepy.WeatherProductType.forecast_astronomy: | ||
| return data.astronomy["astronomy"] | ||
| if product_type == herepy.WeatherProductType.observation: | ||
| return data.observations["location"][0]["observation"] | ||
| if product_type == herepy.WeatherProductType.forecast_7days: | ||
| return data.forecasts["forecastLocation"]["forecast"] | ||
| if product_type == herepy.WeatherProductType.forecast_7days_simple: | ||
| return data.dailyForecasts["forecastLocation"]["forecast"] | ||
| if product_type == herepy.WeatherProductType.forecast_hourly: | ||
| return data.hourlyForecasts["forecastLocation"]["forecast"] | ||
| _LOGGER.debug("Payload malformed: %s", data) | ||
| raise UpdateFailed("Payload malformed") |
This file contains hidden or 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,87 @@ | ||
| """Config flow for here_weather integration.""" | ||
| from __future__ import annotations | ||
|
|
||
| import herepy | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import config_entries | ||
| from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME | ||
| from homeassistant.core import HomeAssistant | ||
| import homeassistant.helpers.config_validation as cv | ||
|
|
||
| from .const import DEFAULT_MODE, DOMAIN | ||
|
|
||
|
|
||
| async def async_validate_user_input(hass: HomeAssistant, user_input: dict) -> None: | ||
| """Validate the user_input containing coordinates.""" | ||
| here_client = herepy.DestinationWeatherApi(user_input[CONF_API_KEY]) | ||
| await hass.async_add_executor_job( | ||
| here_client.weather_for_coordinates, | ||
| user_input[CONF_LATITUDE], | ||
| user_input[CONF_LONGITUDE], | ||
| herepy.WeatherProductType[DEFAULT_MODE], | ||
| ) | ||
|
|
||
|
|
||
| class HereWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for here_weather.""" | ||
|
|
||
| VERSION = 1 | ||
|
|
||
| async def async_step_user(self, user_input=None): | ||
| """Handle the initial step.""" | ||
| errors = {} | ||
| if user_input is not None: | ||
| await self.async_set_unique_id(_unique_id(user_input)) | ||
| self._abort_if_unique_id_configured() | ||
| try: | ||
| await async_validate_user_input(self.hass, user_input) | ||
| except herepy.InvalidRequestError: | ||
| errors["base"] = "invalid_request" | ||
| except herepy.UnauthorizedError: | ||
| errors["base"] = "unauthorized" | ||
| else: | ||
| return self.async_create_entry( | ||
| title=user_input[CONF_NAME], data=user_input | ||
| ) | ||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=self._get_schema(user_input), | ||
| errors=errors, | ||
| ) | ||
|
|
||
| def _get_schema(self, user_input: dict | None) -> vol.Schema: | ||
| known_api_keys = { | ||
| entry.data[CONF_API_KEY] for entry in self._async_current_entries() | ||
| } | ||
| if user_input is not None: | ||
| return vol.Schema( | ||
| { | ||
| vol.Required( | ||
| CONF_API_KEY, default=user_input[CONF_API_KEY] | ||
| ): vol.In(known_api_keys), | ||
| vol.Required(CONF_NAME, default=user_input[CONF_NAME]): str, | ||
| vol.Required( | ||
| CONF_LATITUDE, default=user_input[CONF_LATITUDE] | ||
| ): cv.latitude, | ||
| vol.Required( | ||
| CONF_LONGITUDE, default=user_input[CONF_LONGITUDE] | ||
| ): cv.longitude, | ||
| } | ||
| ) | ||
| return vol.Schema( | ||
| { | ||
| vol.Required(CONF_API_KEY): vol.In(known_api_keys), | ||
| vol.Required(CONF_NAME, default=DOMAIN): str, | ||
| vol.Required( | ||
| CONF_LATITUDE, default=self.hass.config.latitude | ||
| ): cv.latitude, | ||
| vol.Required( | ||
| CONF_LONGITUDE, default=self.hass.config.longitude | ||
| ): cv.longitude, | ||
| } | ||
| ) | ||
|
|
||
|
|
||
| def _unique_id(user_input: dict) -> str: | ||
| return f"{user_input[CONF_LATITUDE]}_{user_input[CONF_LONGITUDE]}" | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.