-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
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
Refactor Forecast.io #2217
Refactor Forecast.io #2217
Conversation
"Connection error " | ||
"Please check your settings for Forecast.io.") | ||
api_key = config.get(CONF_API_KEY, None) | ||
if None in (hass.config.latitude, hass.config.longitude, api_key): |
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.
Check out the validate_config function for this.
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 was able to use validate_config
for the API key, but couldn't figure out how to use it for a HA level config. All the example I found were just checking manually like above
Other than that one comment, it looks good to me. |
Sounds Good. I also want to double check that it's still not calling the API too many times. While I'm at it, I'll add a few code comments. |
"Please check your settings for Forecast.io.") | ||
api_key = config.get(CONF_API_KEY, None) | ||
if None in (hass.config.latitude, hass.config.longitude, api_key): | ||
_LOGGER.error("Latitude, longitude, or API key missing from config") |
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 still think that the latitude/longitude error should be separate from the API key one. The settings for the latitude and longitude are configuration entries for HA itself while the API key is related to the configuration of the sensor thus they have different positions in the configuration.yaml
file. This could mislead users.
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.
Makes perfect sense. I'll change that.
Still working on this. Currently working on making this more resilient to API changes like that. |
Does #2195 not fix the problem in the description? |
'pressure': round(getattr(data, 'pressure', 0), 1), | ||
'visibility': getattr(data, 'visibility', 0), | ||
'ozone': round(getattr(data, 'ozone', 0), 1) | ||
}.get(self.type, self._state) |
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 now works. catching PropertyUnavailable
no longer relevant.
Two things to discuss:
- I thought about converting snake case to camel case to further simplify the lookup. Problem is that half of the responses are treated like integers and need different operations done on them.
- defaults. For the numbers, defaulting to zero works fine, but I imagine there could be an edge case where an api error results in a return that there is "zero percent chance of rain". Suddenly someone home automation setup kicks off the sprinklers during a downpour. Not sure if there are any thoughts on that.
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 the python authority, but it seems like you'll update all integers with this call and then only use the correct, or not?
The if/then will only calculate if that type is 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 think you can break out the dict from the method by mapping the type string to a tuple holding the data string and a lambda function. The lambda should process the data attribute correctly. So when the method to get current state is called, it first does the lookup, then gets the attribute, then processes the attribute.
Regarding sane defaults, it's probably hard to catch all side effects, when the weather 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.
If you also do the camelcase conversion in the method you can skip the tuple and just map the type string to the lambda function.
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.
yea, I'm not a huge fan of lambda's when you have to put a bunch of them together like this.
I'm looking right now to see if there is anything else that can be done to tighten this up.
I do agree with @kellerza concern that each integer will get calculated every time. Good catch.
Seeing what I can do.
@oudeismetis Makes more sense now, would be good to add that to the description :-) |
@@ -124,80 +116,66 @@ def state(self): | |||
@property | |||
def unit_of_measurement(self): | |||
"""Return the unit of measurement of this entity, if any.""" | |||
return self._unit_of_measurement | |||
self.forecast_data.update_unit_of_measurement(self.type) |
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.
Will the unit of measurement ever change during the lifetime of this object?
You also probably need to lock this in some way if you do it this way, since another property updating in another thread can also call this and the forecasr_data.unit of measurement will be changed to that type
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.
As I understand it, no. Unit of measure is not likely to ever change for the running system.
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.
best to make this return the unit of measurement and save it in the sensor's __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.
I think you're right
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.
So once I looked at this, I realized that unit of measurement should NOT be on the data object as it is sensor type specific. So I moved it back up to the sensor level, but still implemented your recommendation and cleaned up some other things.
I think it makes more sense now. Let me know what you think @kellerza when I push in a minute.
I just merged the tests. Can you rebase on |
# same exact call, but thats fine. We cache results for a short period | ||
# of time to prevent hitting API limits. Note that forecast.io will | ||
# charge users for too many calls in 1 day, so take care when updating. | ||
self.forecast_data.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.
Is this call still needed if we hit minutely_summary
as that involves it's own update call?
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.
Looking at the SDK we call down into, calling update makes an API call to forecast.io which gets us everything we need. We then call update on each individual component. With those calls, the object we get back from SDK has a method to see if the data (minutely, hourly, etc.) already exists in the json object it got back from the API. If it doesn't, then it'll make a new call to forecast.io. Looking at it though, that new call is hitting the same endpoint for the same data, so for our simple implementation, there is no reason it wouldn't already have all the data it needs.
Those methods also have rate limits on them from our side of things. (2 minutes) Since the update all call occurs first and we cache the result for 2 minutes, it's probably unnecessary to throttle all of those update calls, but a nice safeguard in my opinion.
9cc87cb
to
f784073
Compare
with pytest.raises(HTTPError): | ||
forecast.setup_platform(self.hass, self.config, MagicMock()) | ||
response = forecast.setup_platform(self.hass, self.config, MagicMock()) | ||
self.assertFalse(response) |
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.
Not sure what is convention for hass. I wrapper the portion of the setup that calls forecast.io in a try/catch. In the catch, it logs an error message and just returns false. I've seen some components do that. Others return None
or [ ]
Could just as easily raise an error.
@balloob rebased and fixed tests. |
Looks good! 🐬 |
Description: Connect to #2195 - The refactors the component to improve readability and maintainability. #2227 is being worked in tandem to test this component. The ultimate goal is to ensure there are no accidental future code changes that result in users getting high rate charges ($$$) from forecast.io.
Related issue (if applicable): fixes #2190
Connected to #2195 and #2227
Pull request in home-assistant.io with documentation (if applicable): home-assistant/home-assistant.io#
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If code communicates with devices:
tox
run successfully. Your PR cannot be merged unless tests passREQUIREMENTS
variable (example).requirements_all.txt
by runningscript/gen_requirements_all.py
..coveragerc
.If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass