1 """A sensor that monitors trends in other components."""
3 from __future__
import annotations
5 from collections
import deque
6 from collections.abc
import Mapping
12 import voluptuous
as vol
15 DEVICE_CLASSES_SCHEMA,
17 PLATFORM_SCHEMA
as BINARY_SENSOR_PLATFORM_SCHEMA,
18 BinarySensorDeviceClass,
46 from .
import PLATFORMS
61 DEFAULT_SAMPLE_DURATION,
65 _LOGGER = logging.getLogger(__name__)
70 CONF_MIN_SAMPLES
in data
71 and CONF_MAX_SAMPLES
in data
72 and data[CONF_MAX_SAMPLES] < data[CONF_MIN_SAMPLES]
74 raise vol.Invalid(
"min_samples must be smaller than or equal to max_samples")
78 SENSOR_SCHEMA = vol.All(
81 vol.Required(CONF_ENTITY_ID): cv.entity_id,
82 vol.Optional(CONF_ATTRIBUTE): cv.string,
83 vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
84 vol.Optional(CONF_FRIENDLY_NAME): cv.string,
85 vol.Optional(CONF_INVERT, default=
False): cv.boolean,
86 vol.Optional(CONF_MAX_SAMPLES, default=2): cv.positive_int,
87 vol.Optional(CONF_MIN_GRADIENT, default=0.0): vol.Coerce(float),
88 vol.Optional(CONF_SAMPLE_DURATION, default=0): cv.positive_int,
89 vol.Optional(CONF_MIN_SAMPLES, default=2): cv.positive_int,
95 PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
96 {vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA)}
103 async_add_entities: AddEntitiesCallback,
104 discovery_info: DiscoveryInfoType |
None =
None,
106 """Set up the trend sensors."""
110 for sensor_name, sensor_config
in config[CONF_SENSORS].items():
113 name=sensor_config.get(CONF_FRIENDLY_NAME, sensor_name),
114 entity_id=sensor_config[CONF_ENTITY_ID],
115 attribute=sensor_config.get(CONF_ATTRIBUTE),
116 invert=sensor_config[CONF_INVERT],
117 sample_duration=sensor_config[CONF_SAMPLE_DURATION],
118 min_gradient=sensor_config[CONF_MIN_GRADIENT],
119 min_samples=sensor_config[CONF_MIN_SAMPLES],
120 max_samples=sensor_config[CONF_MAX_SAMPLES],
121 device_class=sensor_config.get(CONF_DEVICE_CLASS),
123 ENTITY_ID_FORMAT, sensor_name, hass=hass
134 async_add_entities: AddEntitiesCallback,
136 """Set up trend sensor from config entry."""
140 entry.options[CONF_ENTITY_ID],
147 entity_id=entry.options[CONF_ENTITY_ID],
148 attribute=entry.options.get(CONF_ATTRIBUTE),
149 invert=entry.options[CONF_INVERT],
150 sample_duration=entry.options.get(
151 CONF_SAMPLE_DURATION, DEFAULT_SAMPLE_DURATION
153 min_gradient=entry.options.get(CONF_MIN_GRADIENT, DEFAULT_MIN_GRADIENT),
154 min_samples=entry.options.get(CONF_MIN_SAMPLES, DEFAULT_MIN_SAMPLES),
155 max_samples=entry.options.get(CONF_MAX_SAMPLES, DEFAULT_MAX_SAMPLES),
156 unique_id=entry.entry_id,
157 device_info=device_info,
164 """Representation of a trend Sensor."""
166 _attr_should_poll =
False
168 _state: bool |
None =
None
174 attribute: str |
None,
176 sample_duration: int,
180 unique_id: str |
None =
None,
181 device_class: BinarySensorDeviceClass |
None =
None,
182 sensor_entity_id: str |
None =
None,
183 device_info: dr.DeviceInfo |
None =
None,
185 """Initialize the sensor."""
192 self.samples: deque = deque(maxlen=
int(max_samples))
204 """Return the state attributes of the sensor."""
207 ATTR_FRIENDLY_NAME: self.
_attr_name_attr_name,
209 ATTR_INVERT: self.
_invert_invert,
211 ATTR_SAMPLE_COUNT: len(self.samples),
216 """Complete device setup after being added to hass."""
219 def trend_sensor_state_listener(
220 event: Event[EventStateChangedData],
222 """Handle state changes on the observed device."""
223 if (new_state := event.data[
"new_state"])
is None:
227 state = new_state.attributes.get(self.
_attribute_attribute)
229 state = new_state.state
231 if state
in (STATE_UNKNOWN, STATE_UNAVAILABLE):
235 sample = (new_state.last_updated.timestamp(),
float(state))
236 self.samples.append(sample)
239 except (ValueError, TypeError)
as ex:
244 self.
hasshass, [self.
_entity_id_entity_id], trend_sensor_state_listener
250 if state.state
in {STATE_UNKNOWN, STATE_UNAVAILABLE}:
255 """Get the latest data and update the states."""
259 while self.samples
and self.samples[0][0] < cutoff:
260 self.samples.popleft()
278 """Compute the linear trend gradient of the current samples.
280 This need run inside executor.
282 timestamps = np.array([t
for t, _
in self.samples])
283 values = np.array([s
for _, s
in self.samples])
284 coeffs = np.polyfit(timestamps, values, 1)
None __init__(self, str name, str entity_id, str|None attribute, bool invert, int sample_duration, float min_gradient, int min_samples, int max_samples, str|None unique_id=None, BinarySensorDeviceClass|None device_class=None, str|None sensor_entity_id=None, dr.DeviceInfo|None device_info=None)
Mapping[str, Any] extra_state_attributes(self)
None async_added_to_hass(self)
None _calculate_gradient(self)
None async_schedule_update_ha_state(self, bool force_refresh=False)
None async_on_remove(self, CALLBACK_TYPE func)
State|None async_get_last_state(self)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
dict[str, Any] _validate_min_max(dict[str, Any] data)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
dr.DeviceInfo|None async_device_info_to_link_from_entity(HomeAssistant hass, str entity_id_or_uuid)
str generate_entity_id(str entity_id_format, str|None name, list[str]|None current_ids=None, HomeAssistant|None hass=None)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)