Skip to content

Commit d3b6bd6

Browse files
Merge pull request #8 from andrew-codechimp/not-state
Add not state helper
2 parents 229c4c8 + 0e90aba commit d3b6bd6

File tree

11 files changed

+273
-27
lines changed

11 files changed

+273
-27
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99
Label State Helpers for Home Assistant
1010

11-
You can create both state and numeric state helpers which provide a binary sensor that turns on if any entity with an assigned label matches the criteria you specify.
11+
You can create state, not state and numeric state helpers which provide a binary sensor that turns on if any entity with an assigned label matches the criteria you specify.
1212

1313
An `entities` attribute is available which lists all entities that match the criteria.
1414

1515
## Example use cases
1616

17-
- Create a critical sensors label and create a Label State helper which turns on when any of those entities goes unavailable so you can get a notification.
18-
- If you have appliances which should always draw a certain wattage create a numeric Label State helper to turn on when any of those devices starts drawing 0 watts, triggering a notification.
17+
- Create a `critical sensors` label and create a State type Label State helper which turns on when any of those entities goes unavailable so you can get a notification.
18+
- Create a `must be on` label and create a Not State type Label State helper which turns on when any of those entities goes to any state but on so you can get a notification.
19+
- If you have appliances which should always draw a certain wattage create a Numeric State type Label State helper to turn on when any of those devices starts drawing 0 watts, triggering a notification.
1920

2021
![Helper State](https://raw.githubusercontent.com/andrew-codechimp/ha-label-state/main/images/label_state.png "Helper Label State")
2122

custom_components/label_state/binary_sensor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
ATTR_ENTITIES,
2525
CONF_LABEL,
2626
CONF_STATE_LOWER_LIMIT,
27+
CONF_STATE_NOT,
2728
CONF_STATE_TO,
2829
CONF_STATE_TYPE,
2930
CONF_STATE_UPPER_LIMIT,
@@ -48,6 +49,7 @@ async def async_setup_entry(
4849
label: str = config_entry.options[CONF_LABEL]
4950
state_type: str = config_entry.options[CONF_STATE_TYPE]
5051
state_to: str | None = config_entry.options.get(CONF_STATE_TO)
52+
state_not: str | None = config_entry.options.get(CONF_STATE_NOT)
5153
state_lower_limit: float | None = config_entry.options.get(CONF_STATE_LOWER_LIMIT)
5254
state_upper_limit: float | None = config_entry.options.get(CONF_STATE_UPPER_LIMIT)
5355
unique_id = config_entry.entry_id
@@ -64,6 +66,7 @@ async def async_setup_entry(
6466
name,
6567
state_type,
6668
state_to,
69+
state_not,
6770
state_lower_limit,
6871
state_upper_limit,
6972
unique_id,
@@ -83,6 +86,7 @@ async def async_setup_platform(
8386
name: str | None = config.get(CONF_NAME)
8487
state_type: str = config[CONF_STATE_TYPE]
8588
state_to: str | None = config.get(CONF_STATE_TO)
89+
state_not: str | None = config.get(CONF_STATE_NOT)
8690
state_lower_limit: float | None = config.get(CONF_STATE_LOWER_LIMIT)
8791
state_upper_limit: float | None = config.get(CONF_STATE_UPPER_LIMIT)
8892
unique_id = config.get(CONF_UNIQUE_ID)
@@ -95,6 +99,7 @@ async def async_setup_platform(
9599
name,
96100
state_type,
97101
state_to,
102+
state_not,
98103
state_lower_limit,
99104
state_upper_limit,
100105
unique_id,
@@ -118,6 +123,7 @@ def __init__(
118123
name: str | None,
119124
state_type: str,
120125
state_to: str | None,
126+
state_not: str | None,
121127
state_lower_limit: float | None,
122128
state_upper_limit: float | None,
123129
unique_id: str | None,
@@ -127,6 +133,7 @@ def __init__(
127133
self._label = label
128134
self._state_type = state_type
129135
self._state_to = state_to
136+
self._state_not = state_not
130137
self._state_lower_limit = state_lower_limit
131138
self._state_upper_limit = state_upper_limit
132139
self._attr_name = name
@@ -275,6 +282,21 @@ def _calc_state(self) -> None:
275282
state_is_on = True
276283
entities_on.append(entity_id)
277284

285+
if self._state_type == StateTypes.NOT_STATE:
286+
for entity_id in self._state_dict.keys():
287+
# Check if the state still has the label
288+
entity_entry = entity_registry.async_get(entity_id)
289+
if entity_entry and self._label in entity_entry.labels:
290+
entity_state = self._state_dict[entity_id]
291+
292+
if (
293+
entity_state
294+
and self._state_not
295+
and entity_state.casefold() != self._state_not.casefold()
296+
):
297+
state_is_on = True
298+
entities_on.append(entity_id)
299+
278300
if self._state_type == StateTypes.NUMERIC_STATE:
279301
for entity_id in self._state_dict.keys():
280302
# Check if the state still has the label

custom_components/label_state/config_flow.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@
1818
from .const import (
1919
CONF_LABEL,
2020
CONF_STATE_LOWER_LIMIT,
21+
CONF_STATE_NOT,
2122
CONF_STATE_TO,
2223
CONF_STATE_TYPE,
2324
CONF_STATE_UPPER_LIMIT,
2425
DOMAIN,
2526
StateTypes,
2627
)
2728

28-
STATE_TYPES = [
29-
"numeric_state",
30-
"state",
31-
]
29+
STATE_TYPES = ["numeric_state", "state", "state_not"]
3230

3331
STATE_TO_UNAVAILABLE = "unavailable"
3432
STATE_TO_UNKNOWN = "unknown"
35-
STATE_TO_ON = "on"
36-
STATE_TO_OFF = "off"
37-
STATE_TO_OPTIONS = [STATE_TO_UNAVAILABLE, STATE_TO_UNKNOWN, STATE_TO_ON, STATE_TO_OFF]
33+
STATE_ON = "on"
34+
STATE_OFF = "off"
35+
STATE_TO_OPTIONS = [STATE_TO_UNAVAILABLE, STATE_TO_UNKNOWN, STATE_ON, STATE_OFF]
36+
STATE_NOT_OPTIONS = [STATE_ON, STATE_OFF]
3837

3938

4039
OPTIONS_SCHEMA_NUMERIC_STATE = vol.Schema(
@@ -66,6 +65,19 @@
6665
}
6766
)
6867

68+
OPTIONS_SCHEMA_NOT_STATE = vol.Schema(
69+
{
70+
vol.Required(CONF_LABEL): selector.LabelSelector(),
71+
vol.Required(CONF_STATE_NOT): selector.SelectSelector(
72+
selector.SelectSelectorConfig(
73+
options=STATE_NOT_OPTIONS,
74+
translation_key="state_not",
75+
custom_value=True,
76+
)
77+
),
78+
}
79+
)
80+
6981
CONFIG_SCHEMA_NUMERIC_STATE = vol.Schema(
7082
{
7183
vol.Required("name"): selector.TextSelector(),
@@ -78,6 +90,12 @@
7890
}
7991
).extend(OPTIONS_SCHEMA_STATE.schema)
8092

93+
CONFIG_SCHEMA_NOT_STATE = vol.Schema(
94+
{
95+
vol.Required("name"): selector.TextSelector(),
96+
}
97+
).extend(OPTIONS_SCHEMA_NOT_STATE.schema)
98+
8199

82100
async def choose_options_step(options: dict[str, Any]) -> str:
83101
"""Return next step_id for options flow according to label_type."""
@@ -128,6 +146,10 @@ async def _validate_user_input(
128146
CONFIG_SCHEMA_STATE,
129147
validate_user_input=validate_user_input(StateTypes.STATE),
130148
),
149+
StateTypes.NOT_STATE: SchemaFlowFormStep(
150+
CONFIG_SCHEMA_NOT_STATE,
151+
validate_user_input=validate_user_input(StateTypes.NOT_STATE),
152+
),
131153
}
132154

133155

@@ -140,6 +162,10 @@ async def _validate_user_input(
140162
StateTypes.STATE: SchemaFlowFormStep(
141163
OPTIONS_SCHEMA_STATE, validate_user_input=validate_user_input(StateTypes.STATE)
142164
),
165+
StateTypes.NOT_STATE: SchemaFlowFormStep(
166+
OPTIONS_SCHEMA_NOT_STATE,
167+
validate_user_input=validate_user_input(StateTypes.NOT_STATE),
168+
),
143169
}
144170

145171

custom_components/label_state/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
CONF_LABEL = "label"
2727
CONF_STATE_TYPE = "state_type"
2828
CONF_STATE_TO = "state_to"
29+
CONF_STATE_NOT = "state_not"
2930
CONF_STATE_LOWER_LIMIT = "state_lower_limit"
3031
CONF_STATE_UPPER_LIMIT = "state_upper_limit"
3132

@@ -37,3 +38,4 @@ class StateTypes(StrEnum):
3738

3839
NUMERIC_STATE = "numeric_state"
3940
STATE = "state"
41+
NOT_STATE = "state_not"

custom_components/label_state/translations/de.json

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
"description": "Create a binary sensor that is on if any sensor with the label meets the criteria.",
1010
"menu_options": {
1111
"numeric_state": "Numeric state",
12-
"state": "State"
12+
"state": "State",
13+
"state_not": "Not state"
1314
}
1415
},
1516
"numeric_state": {
16-
"title": "Label State",
17+
"title": "Label Numeric State",
1718
"description": "Create a binary sensor that is on if any sensor's value with the label is outside of its normal bounds.",
1819
"data": {
1920
"label": "Label",
@@ -24,11 +25,20 @@
2425
},
2526
"state": {
2627
"title": "Label State",
27-
"description": "Create a binary sensor that is on if any sensor with a label meets the criteria.",
28+
"description": "Create a binary sensor that is on if any sensor with the label has the specified state.",
29+
"data": {
30+
"label": "Label",
31+
"name": "Name",
32+
"state_to": "Is"
33+
}
34+
},
35+
"state_not": {
36+
"title": "Label Not State",
37+
"description": "Create a binary sensor that is on if any sensor with the label does not have the specified state.",
2838
"data": {
2939
"label": "Label",
3040
"name": "Name",
31-
"state_to": "To"
41+
"state_not": "Not"
3242
}
3343
}
3444
}
@@ -39,7 +49,7 @@
3949
},
4050
"step": {
4151
"numeric_state": {
42-
"title": "Label State",
52+
"title": "Label Numeric State",
4353
"description": "Create a binary sensor that is on if any sensor's value with the label is outside of its normal bounds.",
4454
"data": {
4555
"label": "Label",
@@ -49,10 +59,18 @@
4959
},
5060
"state": {
5161
"title": "Label State",
52-
"description": "Create a binary sensor that is on if any sensor with a label meets the criteria.",
62+
"description": "Create a binary sensor that is on if any sensor with the label has the specified state.",
63+
"data": {
64+
"label": "Label",
65+
"state_to": "Is"
66+
}
67+
},
68+
"state_not": {
69+
"title": "Label Not State",
70+
"description": "Create a binary sensor that is on if any sensor with the label does not have the specified state.",
5371
"data": {
5472
"label": "Label",
55-
"state_to": "To"
73+
"state_not": "Not"
5674
}
5775
}
5876
}
@@ -65,6 +83,13 @@
6583
"on": "On",
6684
"off": "Off"
6785
}
86+
},
87+
"state_not": {
88+
"options": {
89+
"on": "On",
90+
"off": "Off"
91+
}
6892
}
93+
6994
}
7095
}

custom_components/label_state/translations/en.json

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
"description": "Create a binary sensor that is on if any sensor with the label meets the criteria.",
1010
"menu_options": {
1111
"numeric_state": "Numeric state",
12-
"state": "State"
12+
"state": "State",
13+
"state_not": "Not state"
1314
}
1415
},
1516
"numeric_state": {
16-
"title": "Label State",
17+
"title": "Label Numeric State",
1718
"description": "Create a binary sensor that is on if any sensor's value with the label is outside of its normal bounds.",
1819
"data": {
1920
"label": "Label",
@@ -24,11 +25,20 @@
2425
},
2526
"state": {
2627
"title": "Label State",
27-
"description": "Create a binary sensor that is on if any sensor with a label meets the criteria.",
28+
"description": "Create a binary sensor that is on if any sensor with the label has the specified state.",
29+
"data": {
30+
"label": "Label",
31+
"name": "Name",
32+
"state_to": "Is"
33+
}
34+
},
35+
"state_not": {
36+
"title": "Label Not State",
37+
"description": "Create a binary sensor that is on if any sensor with the label does not have the specified state.",
2838
"data": {
2939
"label": "Label",
3040
"name": "Name",
31-
"state_to": "To"
41+
"state_not": "Not"
3242
}
3343
}
3444
}
@@ -39,7 +49,7 @@
3949
},
4050
"step": {
4151
"numeric_state": {
42-
"title": "Label State",
52+
"title": "Label Numeric State",
4353
"description": "Create a binary sensor that is on if any sensor's value with the label is outside of its normal bounds.",
4454
"data": {
4555
"label": "Label",
@@ -49,10 +59,18 @@
4959
},
5060
"state": {
5161
"title": "Label State",
52-
"description": "Create a binary sensor that is on if any sensor with a label meets the criteria.",
62+
"description": "Create a binary sensor that is on if any sensor with the label has the specified state.",
63+
"data": {
64+
"label": "Label",
65+
"state_to": "Is"
66+
}
67+
},
68+
"state_not": {
69+
"title": "Label Not State",
70+
"description": "Create a binary sensor that is on if any sensor with the label does not have the specified state.",
5371
"data": {
5472
"label": "Label",
55-
"state_to": "To"
73+
"state_not": "Not"
5674
}
5775
}
5876
}
@@ -65,6 +83,13 @@
6583
"on": "On",
6684
"off": "Off"
6785
}
86+
},
87+
"state_not": {
88+
"options": {
89+
"on": "On",
90+
"off": "Off"
91+
}
6892
}
93+
6994
}
7095
}

images/label_state.png

-144 KB
Loading

images/label_state_numeric.png

-114 KB
Loading

tests/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ source venv/bin/activate
1212
```
1313

1414
You can then install the dependencies that will allow you to run tests:
15-
`pip3 install -r requirements_test.txt.`
15+
`pip3 install -r requirements_test.txt`
1616

1717
This will install `homeassistant`, `pytest`, and `pytest-homeassistant-custom-component`, a plugin which allows you to leverage helpers that are available in Home Assistant for core integration tests.
1818

0 commit comments

Comments
 (0)