-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
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 optional location based region to dwd_weather_warnings #96027
Add optional location based region to dwd_weather_warnings #96027
Conversation
Hey there @runningman84, @stephan192, @hummel95, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. |
There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will be closed if no further activity occurs within 7 days. |
@andarotajo It's been concluded the approach you take in this PR is OK, but the PR needs a rebase as well as tests covering the new functionality. Please mark the PR ready for review once that's done. |
0fba940
to
3e2a00e
Compare
I just saw with the rebasing that I had added tests for the config flow GPS option in my initial commit. Does that not cover enough or what tests am I missing for this PR? |
It does not cover enough, only 75% of the added code is covered. You can easily generate a coverage report when running pytest locally:
|
I'm sorry I think I broke something while rebasing... Can I somehow salvage this? I followed the due to a conflict docs but probably missed something Edit: Fixed it luckily... |
try: | ||
position = get_position_data(hass, device_tracker) | ||
except (EntityNotFoundError, AttributeError) as err: | ||
# The provided device_tracker is not available or missing required attributes. | ||
LOGGER.error(f"Failed to setup dwd_weather_warnings: {repr(err)}") | ||
return False | ||
|
||
api = await hass.async_add_executor_job(DwdWeatherWarningsAPI, position) |
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.
All of this seems like duplicated logic which the coordinator also needs to handle, can you try to make it happen as a result of calling DwdWeatherWarningsCoordinator.async_config_entry_first_refresh
instead?
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.
I'll see what I can do, not familiar with that method though
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.
Took a few tries but I got it (see last 3 commits)
@@ -23,4 +28,14 @@ def __init__(self, hass: HomeAssistant, api: DwdWeatherWarningsAPI) -> None: | |||
|
|||
async def _async_update_data(self) -> None: | |||
"""Get the latest data from the DWD Weather Warnings API.""" | |||
await self.hass.async_add_executor_job(self.api.update) | |||
if device_tracker := self.config_entry.data.get(CONF_REGION_DEVICE_TRACKER): |
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.
I'd suggest to store device_tracker
in the object instead, and just do like this here:
if self._device_tracker:
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.
I'm not following completely. Do you mean having it defined in the __init__
as self.device_tracker = None
and doing the initialization once in async_config_entry_first_refresh
?
Edit: Nevermind got it
if all(k not in user_input for k in valid_options): | ||
errors["base"] = "no_identifier" | ||
elif all(k in user_input for k in valid_options): | ||
errors["base"] = "ambiguous_identifier" |
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.
This seems a bit unnecessary. Maybe just do a set intersection and check the length?
Not sure if my suggestion is more readable though.
EXCLUSIVE_OPTIONS = {CONF_REGION_IDENTIFIER, CONF_REGION_DEVICE_TRACKER}
options = user_input.keys() & EXCLUSIVE_OPTIONS
if not options:
...
elif len(options) == 2:
...
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.
In my opinion the current solution is easier to understand though not as efficient probably
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.
That's fine, it's a matter of taste 👍
DwdWeatherWarningsAPI, position | ||
) | ||
else: | ||
if self._api is None: |
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.
Can this happen? self._api
is unconditionally assigned in __init__
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.
It might not be possible anymore. I think ruff marked this line at one point but I can't check for around 3 weeks as I'm on vacation as of today.
Will check after that of course
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.
OK. I've marked the PR as draft, please mark it ready for review when you've had time to check 👍
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.
Removed it in df914c2 :)
"""Initialize the dwd_weather_warnings coordinator.""" | ||
super().__init__( | ||
hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL | ||
) | ||
|
||
self.api = api | ||
self._api = DwdWeatherWarningsAPI(None) |
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.
What's the meaning passing None
here?
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.
This initializes the API with an invalid state. I chose this instead of None
to avoid having to deal with a potential None
object in the sensor.py
where the data is primarily used
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.
I don't fully follow. The normal pattern for coordinator based integrations is that entities only access the coordinator after a successful refresh.
An option could be to not initialize self._api
in __init__
, instead just add a type annotation for it on class level:
_api: DwdWeatherWarningsAPI
If there's some bug causing sensors to access the coordinator before the first successful data update, an attribute error will be raised.
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.
Updated the initialization of self._api
in df914c2 as suggested
@@ -93,18 +95,18 @@ def __init__( | |||
entry_type=DeviceEntryType.SERVICE, | |||
) | |||
|
|||
self.api = coordinator.api | |||
self.api = coordinator._api |
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.
If the location is changed, the coordinator creates a new api object, I don't think it's valid to store a reference here, don't we need to access the api object via the coordinator instead?
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.
A reference of a complex object shares the memory with the original as far as I know so this should be fine. But using self.coordinator._api
directly instead of a reference is probably cleaner..
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.
A reference of a complex object shares the memory with the original as far as I know so this should be fine
That's absolutely not the case, as evident from this snippet:
$ python3
Python 3.11.5 (main, Oct 19 2023, 09:15:06) [GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Coordinator:
... api = object()
...
>>> coordinator = Coordinator()
>>> original_api = coordinator.api
>>> coordinator.api = object()
>>> original_api is coordinator.api
False
Don't the added tests cover creating new API object when location changes?
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.
The tests cover creating the API object initially but not a location change that would result in the API having a different city set.
Also noticed that using self.coordinator._api
doesn't sit well with Pylint. Is renaming the _api
variable to api
the way to go here?
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.
Yeah, name it api
if it's to be used from other classes.
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.
Updated in 848cfc5
self._api = await self.hass.async_add_executor_job( | ||
DwdWeatherWarningsAPI, position | ||
) |
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.
I think we should avoid creating a new object if the location has not changed.
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.
Do you mean the region that the API would determine using the coordinates or the coordinates directly? I'd have to create the API object to test the former.
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.
I meant that if the location of the device tracker has not changed, we should not create a new API object since it seems to be somewhat expensive? Or is the cost of just updating an existing API object similar to creating an API object, including the initial update?
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.
The initialization includes 5 API calls to get the required information based on what the object was initialized with, i.e. region ID, region name or GPS coordinates. So it's definitely more costly as the updates only do a singular API call because the required information is already there.
Is there some convenient method to verify if the location has changed instead of having to store the previous position?
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.
Just store the previous position. Maybe check if the position has changed more than 100m (or whatever is reasonable) to avoid recreating the API because of GPS inaccuracy. We have some helpers to help calculate the absolute distance between two points.
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.
Implemented in 848cfc5
After a bit of googling I chose 50m as the threshold. I've also added a normal API update in case the object doesn't get recreated as the data wouldn't update otherwise
Please mark the PR ready for review when the requested changes have been implemented 👍 |
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.
LGTM, thanks @andarotajo 👍
A follow-up PR which adds some additional tests of the coordinator would be great, in particular to assert a new API object is created when needed.
I'm pretty new to Python so thanks for all the help and patience! I'll make a follow-up PR for the tests and for error handling with repair issues too when I've got the time 👍 |
Proposed change
This PR adds the optional feature to have a dynamic region by configuring it to use a device tracker that contains the current location. This allows the user to have weather warnings wherever they go instead of using fixed locations.
Type of change
Additional information
Checklist
black --fast homeassistant tests
)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest
.requirements_all.txt
.Updated by running
python3 -m script.gen_requirements_all
..coveragerc
.To help with the load of incoming pull requests: