-
-
Notifications
You must be signed in to change notification settings - Fork 28.9k
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
history_average sensor - time-weighted average of state values from history #10717
Conversation
This is an updated version of this older pull request that went stale: I believe I've addressed the feedback in that pull request; happy to write the docs if you all are happy with the work here. From the comments on the other pull request, it sounds like some renaming may be in order if folks aren't happy with 'history_average'. I'm not wed to anything. |
In my local testing, I had one heck of a time getting the in-memory recorder to work correctly. Apparently I didn't succeed given the tox results here. I would appreciate any pointers that can be given as to how to setup my tests correctly so that I can leverage the history / recorder. I should mention that the tests pass locally when running py.test *test_history_average.py - I just don't know what's going on when I run tox and haven't had any luck debugging it. |
894987a
to
4132233
Compare
homeassistant/const.py
Outdated
MINOR_VERSION = 59 | ||
======= | ||
MINOR_VERSION = 53 | ||
>>>>>>> actually fixing merge conflicts. sigh. |
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.
missing whitespace around operator
homeassistant/const.py
Outdated
MINOR_VERSION = 59 | ||
======= |
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.
missing whitespace around operator
homeassistant/const.py
Outdated
@@ -1,7 +1,11 @@ | |||
# coding: utf-8 | |||
"""Constants used by Home Assistant components.""" | |||
MAJOR_VERSION = 0 | |||
<<<<<<< HEAD |
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.
SyntaxError: invalid syntax
missing whitespace around bitwise or shift operator
missing whitespace around operator
self._push_data(message, title, data, self.pushbullet) | ||
======= | ||
self._push_data(filepath, message, title, url, self.pushbullet) | ||
>>>>>>> actually fixing merge conflicts. sigh. |
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.
missing whitespace around operator
self._push_data(message, title, data, self.pushbullet) | ||
======= | ||
self._push_data(filepath, message, title, url, self.pushbullet) |
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.
unexpected indentation
self._push_data(message, title, data, self.pushbullet) | ||
======= |
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.
missing whitespace around operator
@@ -86,7 +86,11 @@ def send_message(self, message=None, **kwargs): | |||
|
|||
if not targets: | |||
# Backward compatibility, notify all devices in own account | |||
<<<<<<< HEAD |
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.
expected 2 blank lines after class or function definition, found 0
expected an indented block
IndentationError: expected an indented block
missing whitespace around bitwise or shift operator
missing whitespace around operator
f04fc15
to
811245f
Compare
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.
Realized I had a bunch of pending comments for this PR. Not sure if I fully reviewed it.
_LOGGER.warning(ex) | ||
return | ||
_LOGGER.error("Error parsing template for field %s", field) | ||
_LOGGER.error(ex) |
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.
Don' do this. If you want to print the stack trace use _LOGGER.exception("your message")
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.
Actually, looking at this again, this is how the template.py
implements the same check/warning:
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
# Common during HA startup - so just a warning
_LOGGER.warning('Could not render template %s,'
' the state is unknown.', self._name)
return
self._state = None
_LOGGER.error('Could not render template %s: %s', self._name, ex)
Do you still recommend _LOGGER.exception("message")
- just trying for consistency; best practice.
homeassistant/core.py
Outdated
@@ -608,6 +608,10 @@ def __eq__(self, other): | |||
self.state == other.state and | |||
self.attributes == other.attributes) | |||
|
|||
def __lt__(self, other): |
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.
Please remove this as it is incorrect. Not all states can be ordered, what if they are from different entities. It's unpredictable. If you want to sort a bunch of states, just pass in a lambda to use as key: sorted(my_states, key state: state.last_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.
My primary operation is to insert an item into an already sorted list - but bisect doesn't support passing in a lambda to use as a key AFAIK.
It felt wrong to put the item on the end and then sort the list, but I can do that to avoid needing the __lt__
on the State.
|
||
setup_component(self.hass, 'sensor', config) | ||
self.assertEqual(self.hass.states.get('sensor.test'), None) | ||
self.assertRaises(TypeError, |
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.
We should never raise a TypeError
on invalid 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.
This was a pattern I borrowed from the existing History Stats sensor tests - but I think I'm realizing that there's a lot in that test and that Sensor that may not be good to follow.
The TypeError
is raised from template parsing - can you point me to a recommendation / example of how to handle bad template parsing, and associated tests?
Thanks.
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.
You can remove this test case. It can never happen because we have a CONFIG_SCHEMA that protects from it.
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.
Got it. Done.
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.
You write "Done" but I still see this test case?
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.
Sorry - I missed a test case that was looking for a TypeError. Just pushed the removal of another that one. Are there other test cases you were talking about removing?
|
||
|
||
class HistoryAverageHelper: | ||
"""Static methods to make the HistoryAverageSensor code lighter.""" |
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.
Instead of static methods, just define them in the root of your module.
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 do. Thanks. Another place where I was following the existing History Stats sensor... I agree, this is ugly.
https://home-assistant.io/components/sensor.history_average/ | ||
""" | ||
import asyncio | ||
from operator import attrgetter |
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.
trailing whitespace
- help to save database space, writes that are somewhat useless (would be nice to keep them for sensor display w/o storing them as attributes in the DB, but that's not possible AFAIK)
+ no longer average values between sensor readings + remove datetime, homeassistant.util.dt + timestamps no longer rounded + bug fix on incorrect hass object reference + removed buggy "get first item" code that wasn't right at all...
- update to core.py to ordering of States in a list via bisect - known issue: tests don't trigger the async update of the sensor (reading from history is fine)
- move the starting of hass under test to a better location - add logging to testing - remove print() statements - pass pylint, flake8, etc.
- things pass locally; pushing to see if it fixes the CI will pass - patched recorder / history calls to avoid random, not-understood db errors that happen under test - removed various patching of datetime calls that are now not needed
async_track_state_change( | ||
self._hass, self._entity_id, state_listener) | ||
|
||
self._hass.async_add_job(self.async_update_ha_state(True)) |
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.
Why only do this here and not in async_added_to_hass
?
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 a little new to asyncio - I was attempting to follow patterns I've seen elsewhere in HASS code, so it's possible I wasn't following a great example. Are you saying I should be calling self.async_schedule_update_ha_state(True)
in three places?
sensor_startup
state_listener
async_added_to_hass
Or that it could be pulled out of sensor_startup
and have it two places: async_added_to_hass
and state_listener
?
"""Return the polling state.""" | ||
# Since the values can/do change with time, not just with | ||
# state changes, the sensor should poll | ||
return True |
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 like this 😢 I understand why this is, but it is spamming our state machine. Wonder if we should approach this problem differently.
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 open to suggestions. 🤷♂️
@property | ||
def device_state_attributes(self): | ||
"""Return the state attributes of the sensor.""" | ||
def period_in_seconds(period): |
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.
Very expensive to keep defining these functions, just move them out.
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.
Done.
return 0.0 | ||
return (period[1] - period[0]).total_seconds() | ||
|
||
def pretty_duration(period): |
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.
We should not have a pretty duration. Attributes should be machine readable. If pretty printend is necessary, the frontend should add 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 agree. However, I'm not sure what data type to return here to represent a length in time. I tried timedelta
, but it's not JSON serializable. Do you have a suggestion?
yield from self.asysnc_trim_history() | ||
start, end = self._period | ||
|
||
_LOGGER.info(" - period start: %s", start) |
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 is not info
log worthy. Max debug. Also limit to 1 statement instead of 2.
@robmarkcole the difference is that the filter would operate on state changes captured during operation. This sensor would include states fetched from the history. You're right, this could be added to the filter component and would probably be better. That way we can capture every use case. |
I've also seen this commit, and share your comments. Since I wouldn't like to bloat the existing PR with additional features, I'm leaving a possible merge for further ahead :) |
So what should we do? Should we wait for #12650 to get merged and add it as a filter to that platform? |
this is great PR. I really need this functionality. Question: did you implement your own capturing and keeping history of previous values of the sensor in memory and use this for computing a rolling average? From this it sounds as you just get data from database " sensor uses the history recorded in the database to calculate it's results" . How did you deal with the fact that SqlAlchemy does not flush data to DB for some time. Would your sensor work for periods of last 15 - 20 mins? (before SQLalchemy flushed data to database) I attempted to do something this with new SQL sensor and AVG() method over a specified period of time, but found that sqlalchemy does not flush data ot MariaDB often enough.. |
I think that we should close this PR and add weighted average to the new filter sensor. |
Having not been following the filter sensor development, I don't have a strong opinion either way. Is the development far enough along on the filter sensor to start adding this functionality? |
@kmwoley it's ready to go! (already released in 0.65) |
@dgomes @balloob The filters sensor doesn't exactly meet the need that this pull request accomplishes - i.e. relying on the history so that values are correct @ boot. If you all think incorporating history into the filters sensor is a good thing to pursue, I'd be happy to work on extending the filters sensor - but it's not clear that your intent with filters is to draw from history. What do you all think? |
Sweet! Thanks. |
Description:
A sensor which reports the weighted average of a sensor's recorded value over a specified period of time.
Motivation -
history_stats
andstatistics
didn't exactly meet the needs I had for a sensor - I wanted a sensor that could average my bathroom's humidity over the last day, so I could use it in an automation to kick on an exhaust fan when the relative humidity was a few % points above the recent average.Unlike the
statistics
sensor which samples a sensor's values to produce a mean (along with other stats), this sensor uses the history recorded in the database to calculate it's results. That allows the value to be useful across reboots, can leverage history to produce results over a much longer period of time, and doesn't rely upon picking a correct sampling interval for the data being averaged.Unlike
history_stats
sensor, this sensor doesn't look to count a specific discrete state, but averages the values of non-discrete states. Much of the inspiration, code for this sensor started withhistory_stats
Note:
I did look at just incorporating this into
history_stats
, but that sensor requires astate
attribute. Incorporating this functionality into that sensor would require making that attribute optional, which didn't feel right. I'd be happy to combine this sensor with another if folks felt that was a better path.Related issue (if applicable): fixes #
n/a
Pull request in home-assistant.github.io with documentation (if applicable): home-assistant/home-assistant.github.io#<home-assistant.github.io PR number goes here>
TBD - assuming folks like this as a stand alone sensor, I'll write up the docs.
Example entry for
configuration.yaml
(if applicable):Checklist:
If user exposed functionality or configuration variables are added/changed:
If the code does not interact with devices:
tox
run successfully. Your PR cannot be merged unless tests pass