Skip to content
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 preview to Threshold config & option flow #111043

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3b5fccd
Add preview to Threshold config & option flow
jpbede Feb 20, 2024
884c3ea
Fix import
jpbede Mar 16, 2024
7c52537
Fix import
jpbede Mar 16, 2024
f849410
Merge branch 'dev' into threshold-config-flow-preview
emontnemery May 8, 2024
189c07d
Correct state class of ecowitt hourly rain rate sensors (#110475)
pantonvich May 8, 2024
7862596
Add `open` state to LockEntity (#111968)
gjohansson-ST May 8, 2024
92b246f
Fix nibe_heatpump climate for models without cooling support (#114599)
tizianodeg May 8, 2024
84a91a8
Improve config entry has already been setup error message (#117091)
bdraco May 8, 2024
6b3ffad
Fix nws blocking startup (#117094)
MatthewFlamm May 8, 2024
20b2924
Make the mqtt discovery update tasks eager and fix race (#117105)
jbouwh May 8, 2024
159f0fc
Migrate baf to use config entry runtime_data (#117081)
bdraco May 8, 2024
840d8cb
Add open and opening state support to MQTT lock (#117110)
jbouwh May 8, 2024
1d833d3
Avoid storing Bluetooth scanner in hass.data (#117074)
bdraco May 8, 2024
8c37b3a
Migrate govee_ble to use config entry runtime_data (#117076)
bdraco May 8, 2024
ead69af
Avoid creating a task to clear the hass instance at test teardown (#1…
bdraco May 8, 2024
03dcede
Avoid creating inner tasks to load storage (#117099)
bdraco May 8, 2024
6eeeafa
Speed up tests by making mock_get_source_ip session scoped (#117096)
bdraco May 8, 2024
8464c95
Migrate yalexs_ble to use config entry runtime_data (#117082)
bdraco May 8, 2024
0015088
Migrate elkm1 to use config entry runtime_data (#117077)
bdraco May 8, 2024
6da432a
Bump python-roborock to 2.1.1 (#117078)
Lash-L May 8, 2024
589104f
Export MQTT subscription helpers at integration level (#116150)
jbouwh May 8, 2024
ac54cdc
Enable Ruff RUF010 (#115371)
autinerd May 8, 2024
fe9e5e4
Ignore Ruff SIM103 (#115732)
autinerd May 8, 2024
ac9b8cc
Add a missing `addon_name` placeholder to the SkyConnect config flow …
puddly May 8, 2024
89049bc
Fix config entry _async_process_on_unload being called for forwarded …
bdraco May 8, 2024
12759b5
Store runtime data inside the config entry in Tuya (#116822)
mib1185 May 8, 2024
b60c90e
Goodwe Increase max value of export limit to 200% (#117090)
mletenay May 8, 2024
412e9bb
Add test data for Zeo and Dyad devices to Roborock (#117054)
Lash-L May 8, 2024
f9413fc
Bump goodwe to 0.3.5 (#117115)
mletenay May 8, 2024
a77add1
Add better testing to vacuum platform (#112523)
Lash-L May 8, 2024
04c0b7d
Use HassKey for importlib helper (#117116)
cdce8p May 8, 2024
19c26b7
Move available property in BasePassiveBluetoothCoordinator to Passive…
bdraco May 8, 2024
32061d4
Bump github/codeql-action from 3.25.3 to 3.25.4 (#117127)
dependabot[bot] May 9, 2024
6485973
Add airgradient integration (#114113)
joostlek May 9, 2024
b30a02d
Add base entity for Airgradient (#117135)
joostlek May 9, 2024
c1f0ebe
Add screenlogic service tests (#116356)
dieselrabbit May 9, 2024
333d5a9
Speed up test teardown when no config entries are loaded (#117095)
bdraco May 9, 2024
82e1205
Fix typo in xiaomi_ble translation strings (#117144)
jbouwh May 9, 2024
3fa2db8
Catch auth exception in husqvarna automower (#115365)
Thomas55555 May 9, 2024
e4a3cab
Bump ruff to 0.4.4 (#117154)
autinerd May 9, 2024
4138c7a
Handle tilt position being None in HKC (#117141)
bdraco May 10, 2024
d4fbaef
Raise ServiceValidationError in Nibe climate services (#117171)
tizianodeg May 10, 2024
8c54587
Improve base entity state in Vogel's MotionMount integration (#109043)
RJPoelstra May 10, 2024
11f5b48
Add standard deviation calculation to group (#112076)
CoRfr May 10, 2024
e0edea6
Add preview to Threshold config & option flow
jpbede Feb 20, 2024
210cd23
Merge branch 'threshold-config-flow-preview' of github.com:jpbede/hom…
jpbede May 10, 2024
b34991f
Move guard to `async_start_preview`
jpbede May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 57 additions & 7 deletions homeassistant/components/threshold/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections.abc import Callable, Mapping
import logging
from typing import Any

Expand All @@ -22,7 +23,13 @@
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
callback,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
Expand Down Expand Up @@ -111,7 +118,6 @@ async def async_setup_entry(
async_add_entities(
[
ThresholdSensor(
hass,
entity_id,
name,
lower,
Expand Down Expand Up @@ -145,7 +151,7 @@ async def async_setup_platform(
async_add_entities(
[
ThresholdSensor(
hass, entity_id, name, lower, upper, hysteresis, device_class, None
entity_id, name, lower, upper, hysteresis, device_class, None
)
],
)
Expand All @@ -167,7 +173,6 @@ class ThresholdSensor(BinarySensorEntity):

def __init__(
self,
hass: HomeAssistant,
entity_id: str,
name: str,
lower: float | None,
Expand All @@ -178,6 +183,7 @@ def __init__(
device_info: DeviceInfo | None = None,
) -> None:
"""Initialize the Threshold sensor."""
self._preview_callback: Callable[[str, Mapping[str, Any]], None] | None = None
self._attr_unique_id = unique_id
self._attr_device_info = device_info
self._entity_id = entity_id
Expand All @@ -193,9 +199,17 @@ def __init__(
self._state: bool | None = None
self.sensor_value: float | None = None

async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
self._async_setup_sensor()

@callback
def _async_setup_sensor(self) -> None:
"""Set up the sensor and start tracking state changes."""

def _update_sensor_state() -> None:
"""Handle sensor state changes."""
if (new_state := hass.states.get(self._entity_id)) is None:
if (new_state := self.hass.states.get(self._entity_id)) is None:
return

try:
Expand All @@ -210,17 +224,26 @@ def _update_sensor_state() -> None:

self._update_state()

if self._preview_callback:
calculated_state = self._async_calculate_state()
self._preview_callback(
calculated_state.state, calculated_state.attributes
)

@callback
def async_threshold_sensor_state_listener(
event: Event[EventStateChangedData],
) -> None:
"""Handle sensor state changes."""
_update_sensor_state()
self.async_write_ha_state()

# only write state to the state machine if we are not in preview mode
if not self._preview_callback:
self.async_write_ha_state()

self.async_on_remove(
async_track_state_change_event(
hass, [entity_id], async_threshold_sensor_state_listener
self.hass, [self._entity_id], async_threshold_sensor_state_listener
)
)
_update_sensor_state()
Expand Down Expand Up @@ -260,6 +283,14 @@ def above(sensor_value: float, threshold: float) -> bool:
self._state = None
return

# guard against the case where the thresholds are not set
if not hasattr(self, "_threshold_lower") and not hasattr(
self, "_threshold_upper"
):
self._state_position = POSITION_UNKNOWN
self._state = None
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this case added? If it's only needed for preview, it should be possible to handle it in async_start_preview, if it's a bug fix it should be moved to a separate PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that's just the case for preview. I moved this to async_start_preview. See the new PR as I screwed up the rebase


if self.threshold_type == TYPE_LOWER:
if self._state is None:
self._state = False
Expand Down Expand Up @@ -305,3 +336,22 @@ def above(sensor_value: float, threshold: float) -> bool:
self._state_position = POSITION_IN_RANGE
self._state = True
return

@callback
def async_start_preview(
self,
preview_callback: Callable[[str, Mapping[str, Any]], None],
) -> CALLBACK_TYPE:
"""Render a preview."""
# abort early if there is no entity_id
# as without we can't track changes
if not self._entity_id:
self._attr_available = False
calculated_state = self._async_calculate_state()
preview_callback(calculated_state.state, calculated_state.attributes)
return self._call_on_remove_callbacks

self._preview_callback = preview_callback

self._async_setup_sensor()
return self._call_on_remove_callbacks
70 changes: 68 additions & 2 deletions homeassistant/components/threshold/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
Expand All @@ -17,6 +20,7 @@
SchemaFlowFormStep,
)

from .binary_sensor import ThresholdSensor
from .const import CONF_HYSTERESIS, CONF_LOWER, CONF_UPPER, DEFAULT_HYSTERESIS, DOMAIN


Expand Down Expand Up @@ -61,11 +65,15 @@ async def _validate_mode(
).extend(OPTIONS_SCHEMA.schema)

CONFIG_FLOW = {
"user": SchemaFlowFormStep(CONFIG_SCHEMA, validate_user_input=_validate_mode)
"user": SchemaFlowFormStep(
CONFIG_SCHEMA, preview="threshold", validate_user_input=_validate_mode
)
}

OPTIONS_FLOW = {
"init": SchemaFlowFormStep(OPTIONS_SCHEMA, validate_user_input=_validate_mode)
"init": SchemaFlowFormStep(
OPTIONS_SCHEMA, preview="threshold", validate_user_input=_validate_mode
)
}


Expand All @@ -79,3 +87,61 @@ def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
name: str = options[CONF_NAME]
return name

@staticmethod
async def async_setup_preview(hass: HomeAssistant) -> None:
"""Set up preview WS API."""
websocket_api.async_register_command(hass, ws_start_preview)


@websocket_api.websocket_command(
{
vol.Required("type"): "threshold/start_preview",
vol.Required("flow_id"): str,
vol.Required("flow_type"): vol.Any("config_flow", "options_flow"),
vol.Required("user_input"): dict,
}
)
@callback
def ws_start_preview(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Generate a preview."""

if msg["flow_type"] == "config_flow":
entity_id = msg["user_input"][CONF_ENTITY_ID]
name = msg["user_input"][CONF_NAME]
else:
flow_status = hass.config_entries.options.async_get(msg["flow_id"])
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
if not config_entry:
raise HomeAssistantError("Config entry not found")
entity_id = config_entry.options[CONF_ENTITY_ID]
name = config_entry.options[CONF_NAME]

@callback
def async_preview_updated(state: str, attributes: Mapping[str, Any]) -> None:
"""Forward config entry state events to websocket."""
connection.send_message(
websocket_api.event_message(
msg["id"], {"attributes": attributes, "state": state}
)
)

preview_entity = ThresholdSensor(
entity_id,
name,
msg["user_input"].get(CONF_LOWER),
msg["user_input"].get(CONF_UPPER),
msg["user_input"].get(CONF_HYSTERESIS),
None,
None,
)
preview_entity.hass = hass

connection.send_result(msg["id"])
connection.subscriptions[msg["id"]] = preview_entity.async_start_preview(
async_preview_updated
)
54 changes: 54 additions & 0 deletions tests/components/threshold/snapshots/test_config_flow.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# serializer version: 1
# name: test_config_flow_preview_success[missing_entity_id]
dict({
'attributes': dict({
'friendly_name': '',
}),
'state': 'unavailable',
})
# ---
# name: test_config_flow_preview_success[missing_upper_lower]
dict({
'attributes': dict({
'entity_id': 'sensor.test_monitored',
'friendly_name': 'Test Sensor',
'hysteresis': 0.0,
'lower': None,
'position': 'unknown',
'sensor_value': 16.0,
'type': 'upper',
'upper': None,
}),
'state': 'unknown',
})
# ---
# name: test_config_flow_preview_success[success]
dict({
'attributes': dict({
'entity_id': 'sensor.test_monitored',
'friendly_name': 'Test Sensor',
'hysteresis': 0.0,
'lower': 20.0,
'position': 'below',
'sensor_value': 16.0,
'type': 'lower',
'upper': None,
}),
'state': 'on',
})
# ---
# name: test_options_flow_preview
dict({
'attributes': dict({
'entity_id': 'sensor.test_monitored',
'friendly_name': 'Test Sensor',
'hysteresis': 0.0,
'lower': 20.0,
'position': 'below',
'sensor_value': 16.0,
'type': 'lower',
'upper': None,
}),
'state': 'on',
})
# ---
Loading
Loading