Skip to content

Commit d78e10a

Browse files
committed
Updates
1 parent ba6d614 commit d78e10a

File tree

211 files changed

+4802
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

211 files changed

+4802
-141
lines changed

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Build Status](https://github.com/aneisch/home-assistant-config/actions/workflows/check-ha-release-compatibility.yml/badge.svg)](https://github.com/aneisch/home-assistant-config/actions)
44
[![GitHub last commit](https://img.shields.io/github/last-commit/aneisch/home-assistant-config)](https://github.com/aneisch/home-assistant-config/commits/master)
55
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/aneisch/home-assistant-config)](https://github.com/aneisch/home-assistant-config/graphs/commit-activity)
6-
[![HA Version](https://img.shields.io/badge/Running%20Home%20Assistant-2026.2.1%20(Latest)-brightgreen)](https://github.com/home-assistant/home-assistant/releases/latest)
6+
[![HA Version](https://img.shields.io/badge/Running%20Home%20Assistant-2026.2.2%20(Latest)-brightgreen)](https://github.com/home-assistant/home-assistant/releases/latest)
77
<br><a href="https://www.buymeacoffee.com/aneisch" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-black.png" width="150px" height="35px" alt="Buy Me A Coffee" style="height: 35px !important;width: 150px !important;" ></a>
88

99
I do my best to keep [Home Assistant](https://github.com/home-assistant/home-assistant) on the [latest release](https://github.com/home-assistant/home-assistant/releases/latest). I'm heavily utilizing [AppDaemon](http://appdaemon.readthedocs.io/en/latest/) and [NodeRed](https://flows.nodered.org/node/node-red-contrib-home-assistant-websocket) for advanced/templated automations. See [Appdaemon config](https://github.com/aneisch/home-assistant-config/tree/master/extras/appdaemon) and my NodeRed screenshots below for details. Most of my setup is run as Docker containers (see [docker-compose](https://github.com/aneisch/home-assistant-config/tree/master/extras/docker-compose) for container list).
@@ -58,21 +58,21 @@ Home Assistant and other containers have ingress handled automatically by [Traef
5858
Description | value
5959
-- | --
6060
Lines of ESPHome YAML | 6017
61-
Lines of Home Assistant YAML | 12716
62-
[Integrations](https://www.home-assistant.io/integrations/) in use | 77
61+
Lines of Home Assistant YAML | 12733
62+
[Integrations](https://www.home-assistant.io/integrations/) in use | 76
6363
Zigbee devices in [`zha`](https://www.home-assistant.io/integrations/zha/) |
6464
Z-Wave devices in [`zwave_js`](https://www.home-assistant.io/integrations/zwave_js/) | 37
6565

6666
Description | value
6767
-- | --
68-
Entities in the [`ai_task`](https://www.home-assistant.io/components/ai_task) domain | 2
69-
Entities in the [`automation`](https://www.home-assistant.io/components/automation) domain | 144
68+
Entities in the [`ai_task`](https://www.home-assistant.io/components/ai_task) domain | 1
69+
Entities in the [`automation`](https://www.home-assistant.io/components/automation) domain | 145
7070
Entities in the [`binary_sensor`](https://www.home-assistant.io/components/binary_sensor) domain | 183
71-
Entities in the [`button`](https://www.home-assistant.io/components/button) domain | 72
71+
Entities in the [`button`](https://www.home-assistant.io/components/button) domain | 74
7272
Entities in the [`calendar`](https://www.home-assistant.io/components/calendar) domain | 1
7373
Entities in the [`camera`](https://www.home-assistant.io/components/camera) domain | 13
7474
Entities in the [`climate`](https://www.home-assistant.io/components/climate) domain | 1
75-
Entities in the [`conversation`](https://www.home-assistant.io/components/conversation) domain | 3
75+
Entities in the [`conversation`](https://www.home-assistant.io/components/conversation) domain | 2
7676
Entities in the [`counter`](https://www.home-assistant.io/components/counter) domain | 1
7777
Entities in the [`cover`](https://www.home-assistant.io/components/cover) domain | 19
7878
Entities in the [`datetime`](https://www.home-assistant.io/components/datetime) domain | 6
@@ -97,20 +97,20 @@ Entities in the [`plant`](https://www.home-assistant.io/components/plant) domain
9797
Entities in the [`remote`](https://www.home-assistant.io/components/remote) domain | 5
9898
Entities in the [`script`](https://www.home-assistant.io/components/script) domain | 58
9999
Entities in the [`select`](https://www.home-assistant.io/components/select) domain | 14
100-
Entities in the [`sensor`](https://www.home-assistant.io/components/sensor) domain | 731
100+
Entities in the [`sensor`](https://www.home-assistant.io/components/sensor) domain | 738
101101
Entities in the [`setter`](https://www.home-assistant.io/components/setter) domain | 1
102102
Entities in the [`siren`](https://www.home-assistant.io/components/siren) domain | 2
103103
Entities in the [`stt`](https://www.home-assistant.io/components/stt) domain | 1
104104
Entities in the [`sun`](https://www.home-assistant.io/components/sun) domain | 1
105-
Entities in the [`switch`](https://www.home-assistant.io/components/switch) domain | 208
105+
Entities in the [`switch`](https://www.home-assistant.io/components/switch) domain | 209
106106
Entities in the [`text`](https://www.home-assistant.io/components/text) domain | 2
107107
Entities in the [`timer`](https://www.home-assistant.io/components/timer) domain | 6
108108
Entities in the [`tts`](https://www.home-assistant.io/components/tts) domain | 2
109109
Entities in the [`update`](https://www.home-assistant.io/components/update) domain | 90
110110
Entities in the [`vacuum`](https://www.home-assistant.io/components/vacuum) domain | 1
111111
Entities in the [`weather`](https://www.home-assistant.io/components/weather) domain | 1
112112
Entities in the [`zone`](https://www.home-assistant.io/components/zone) domain | 8
113-
**Total state objects** | **1835**
113+
**Total state objects** | **1844**
114114
## The HACS integrations/plugins that I use:
115115

116116
**Appdaemon**:<br>

automations.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,8 @@
14981498
- sensor.latest_utility_bill_id
14991499
conditions:
15001500
- condition: template
1501-
value_template: '{{ trigger.to_state not in ["unknown", "unavailable"] }}'
1501+
value_template: '{{ trigger.to_state.state not in ["unknown", "unavailable"] and
1502+
trigger.from_state.state not in ["unknown", "unavailable"] }}'
15021503
actions:
15031504
- action: google_sheets.append_sheet
15041505
metadata: {}

configuration.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ logger:
255255
# asyncio: debug
256256
homeassistant.components.tts: debug
257257
homeassistant.components.pico_tts: debug
258-
homeassistant.components.google_generative_ai_conversation: debug
258+
#homeassistant.components.google_generative_ai_conversation: debug
259259
#zigpy: debug
260260
# bellows: debug
261261
# zigpy_xbee: debug

custom_components/alexa_media/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -951,7 +951,7 @@ async def update_last_called(login_obj, last_called=None, force=False):
951951

952952
if not isinstance(last_called, dict) or not last_called.get("summary"):
953953
try:
954-
async with async_timeout.timeout(10):
954+
async with async_timeout.timeout(20):
955955
last_called = await AlexaAPI.get_last_device_serial(login_obj)
956956
except TypeError:
957957
_LOGGER.debug(

custom_components/alexa_media/alexa_entity.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -757,9 +757,7 @@ def is_cap_state_still_acceptable(
757757
return False
758758

759759
try:
760-
time_of_sample = datetime.strptime(
761-
formatted_time_of_sample, "%Y-%m-%dT%H:%M:%S%z"
762-
)
760+
time_of_sample = datetime.fromisoformat(formatted_time_of_sample)
763761
except ValueError:
764762
return False
765763

custom_components/alexa_media/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@
277277
"A2J0R2SD7G9LPA": "Lenovo SmartTab M10",
278278
"A2JKHJ0PX4J3L3": "Fire TV Cube (Gen2)",
279279
"A2LH725P8DQR2A": "Fabriq Riff",
280+
"A2LLN0UXRW4N50": "Echo Show 11 (Gen1)",
280281
"A2LWARUGJLBYEW": "Fire TV Stick (Gen2)",
281282
"A2M35JJZWCQOMZ": "Echo Plus (Gen1)",
282283
"A2M4YX06LWP8WI": "Fire Tablet",

custom_components/alexa_media/helpers.py

Lines changed: 139 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,109 @@
2929
ArgType = TypeVar("ArgType")
3030

3131

32+
def _norm_filter_token(value: Any) -> str | None:
33+
"""Normalize a single filter token for reliable matching."""
34+
if value is None:
35+
return None
36+
s = str(value).strip()
37+
if not s:
38+
return None
39+
return s.casefold()
40+
41+
42+
def _coerce_filter(value: Any) -> set[str]:
43+
"""Coerce include/exclude filter input into a normalized set[str].
44+
45+
Accepts:
46+
- None / empty -> empty set
47+
- comma-separated str -> split on commas
48+
- list/set/tuple -> per-item normalization
49+
- anything else -> single token (best effort)
50+
"""
51+
if not value:
52+
return set()
53+
54+
# Legacy/back-compat: allow comma-separated string
55+
if isinstance(value, str):
56+
out = set()
57+
for part in value.split(","):
58+
token = _norm_filter_token(part)
59+
if token:
60+
out.add(token)
61+
return out
62+
63+
if isinstance(value, (list, set, tuple)):
64+
out = set()
65+
for v in value:
66+
token = _norm_filter_token(v)
67+
if token:
68+
out.add(token)
69+
return out
70+
71+
token = _norm_filter_token(value)
72+
return {token} if token else set()
73+
74+
3275
async def add_devices(
3376
account: str,
3477
devices: list[Entity],
3578
add_devices_callback: Callable[[list[Entity], bool], None],
36-
include_filter: Optional[list[str]] = None,
37-
exclude_filter: Optional[list[str]] = None,
79+
include_filter: str | list[str] | set[str] | tuple[str, ...] | None = None,
80+
exclude_filter: str | list[str] | set[str] | tuple[str, ...] | None = None,
3881
) -> bool:
3982
"""Add devices using add_devices_callback."""
40-
include_filter = include_filter or []
41-
exclude_filter = exclude_filter or []
83+
include_filter_set = _coerce_filter(include_filter)
84+
exclude_filter_set = _coerce_filter(exclude_filter)
85+
if include_filter_set:
86+
_LOGGER.debug(
87+
"%s: include_filter_set: %s",
88+
account,
89+
include_filter_set,
90+
)
91+
if exclude_filter_set:
92+
_LOGGER.debug(
93+
"%s: exclude_filter_set: %s",
94+
account,
95+
exclude_filter_set,
96+
)
4297

4398
def _device_name(dev: Entity) -> str | None:
44-
"""Best-effort name before entity_id is assigned."""
45-
return (
99+
"""Best-effort name before entity_id is assigned.
100+
101+
For AMP switches, reconstruct the legacy "<device> <suffix> switch"
102+
name only if those attributes were explicitly set.
103+
"""
104+
105+
# First prefer explicitly set name attributes (works for tests + most entities)
106+
name = (
46107
getattr(dev, "name", None)
47108
or getattr(dev, "_attr_name", None)
48109
or getattr(dev, "_name", None)
49110
or getattr(dev, "_device_name", None)
50111
or getattr(dev, "_friendly_name", None)
51112
)
113+
if name:
114+
return name
115+
116+
# Only attempt switch reconstruction if attributes were explicitly defined
117+
# (avoids MagicMock auto-attribute trap in tests)
118+
dev_dict = getattr(dev, "__dict__", {})
119+
120+
client = dev_dict.get("_client")
121+
suffix = dev_dict.get("_unique_id_suffix")
122+
123+
if client and suffix:
124+
client_dict = getattr(client, "__dict__", {})
125+
base = (
126+
client_dict.get("name")
127+
or client_dict.get("_attr_name")
128+
or client_dict.get("_name")
129+
or client_dict.get("_device_name")
130+
)
131+
if base:
132+
return f"{base} {suffix} switch"
133+
134+
return None
52135

53136
def _device_label(dev: Entity) -> str:
54137
"""Return a compact, stable identifier for logging."""
@@ -68,19 +151,58 @@ def _devices_preview(devs: list[Entity]) -> str:
68151
suffix = f" …(+{len(devs) - max_items} more)" if len(devs) > max_items else ""
69152
return ", ".join(labels) + suffix
70153

71-
new_devices: list[Entity] = []
72-
for device in devices:
73-
dev_name = _device_name(device)
154+
def _filter_devices(
155+
devs: list[Entity],
156+
include_set: set[str],
157+
exclude_set: set[str],
158+
) -> list[Entity]:
159+
selected: list[Entity] = []
74160

75-
if (include_filter and dev_name not in include_filter) or (
76-
exclude_filter and dev_name in exclude_filter
77-
):
78-
_LOGGER.debug("%s: Excluding device: %s", account, _device_label(device))
79-
continue
161+
include_mode = bool(include_set)
162+
if include_mode and exclude_set:
163+
_LOGGER.debug(
164+
"%s: include_devices set; ignoring exclude_devices per documented precedence",
165+
account,
166+
)
167+
168+
for dev in devs:
169+
dev_name = _norm_filter_token(_device_name(dev))
170+
171+
# INCLUDE MODE: only include explicitly listed names
172+
if include_mode:
173+
if dev_name and dev_name in include_set:
174+
selected.append(dev)
175+
else:
176+
if not dev_name:
177+
_LOGGER.debug(
178+
"%s: Not including device (no name yet): %s",
179+
account,
180+
_device_label(dev),
181+
)
182+
else:
183+
_LOGGER.debug(
184+
"%s: Not including device: %s (match key=%r)",
185+
account,
186+
_device_label(dev),
187+
dev_name,
188+
)
189+
continue
190+
191+
# EXCLUDE MODE: exclude listed names
192+
if exclude_set and dev_name and dev_name in exclude_set:
193+
_LOGGER.debug(
194+
"%s: Excluding device: %s (match key=%r)",
195+
account,
196+
_device_label(dev),
197+
dev_name,
198+
)
199+
continue
200+
201+
selected.append(dev)
80202

81-
new_devices.append(device)
203+
return selected
82204

83-
devices = new_devices
205+
devices = _filter_devices(devices, include_filter_set, exclude_filter_set)
84206
if not devices:
85207
return True
86208

@@ -234,7 +356,7 @@ async def _catch_login_errors(func, instance, args, kwargs) -> Any:
234356
email = login.email
235357
if await login.test_loggedin():
236358
_LOGGER.info(
237-
"%s.%s: Successfully re-login after a login error for %s",
359+
"%s.%s: Successful re-login after a login error for %s",
238360
func.__module__[func.__module__.find(".") + 1 :],
239361
func.__name__,
240362
hide_email(email),

custom_components/alexa_media/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
"issue_tracker": "https://github.com/alandtse/alexa_media_player/issues",
1010
"loggers": ["alexapy", "authcaptureproxy"],
1111
"requirements": [
12-
"alexapy==1.29.15",
12+
"alexapy==1.29.16",
1313
"packaging>=20.3",
1414
"wrapt>=1.14.0",
1515
"dictor>=0.1.12,<0.2"
1616
],
17-
"version": "5.12.2"
17+
"version": "5.13.0"
1818
}

custom_components/alexa_media/strings.json

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,25 @@
1818
},
1919
"step": {
2020
"user": {
21-
"title": "Alexa Media Player - Configuration",
22-
"description": "* Required entry",
2321
"data": {
24-
"url": "Amazon region domain (e.g., amazon.co.uk)",
22+
"debug": "Advanced debug",
2523
"email": "Email Address",
26-
"password": "Password",
27-
"securitycode": "One-time password (OTP)",
28-
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
24+
"exclude_devices": "or Exclude these devices from all (comma separated)",
25+
"extended_entity_discovery": "Include additional sensors, switches and lights",
2926
"hass_url": "Local network URL to access Home Assistant",
30-
"public_url": "Public URL shared with external hosted services",
3127
"include_devices": "Only include these devices (comma separated)",
32-
"exclude_devices": "or Exclude these devices from all (comma separated)",
33-
"scan_interval": "Scheduled polling interval (seconds)",
28+
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
29+
"password": "Password",
30+
"public_url": "Public URL shared with external hosted services",
3431
"queue_delay": "Delay to queue multiple commands together (seconds)",
32+
"scan_interval": "Scheduled polling interval (seconds)",
33+
"securitycode": "One-time password (OTP)",
3534
"should_get_network": "Discover Alexa network",
36-
"extended_entity_discovery": "Include additional sensors, switches and lights",
37-
"debug": "Advanced debugging"
35+
"url": "Amazon region domain (e.g., amazon.co.uk)"
36+
},
37+
"data_description": {
38+
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
39+
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
3840
}
3941
},
4042
"proxy_warning": {
@@ -59,14 +61,19 @@
5961
"title": "Alexa Media Player - Reconfiguration",
6062
"description": "* Required entry",
6163
"data": {
62-
"public_url": "Public URL shared with external hosted services",
63-
"include_devices": "Only include these devices (comma separated)",
64+
"debug": "Advanced debug",
6465
"exclude_devices": "or Exclude these devices from all (comma separated)",
65-
"scan_interval": "Scheduled polling frequency (seconds)",
66-
"queue_delay": "Delay to queue multiple commands together (seconds)",
67-
"should_get_network": "Discover Alexa network",
6866
"extended_entity_discovery": "Include additional sensors, switches and lights",
69-
"debug": "Advanced debugging"
67+
"include_devices": "Only include these devices (comma separated)",
68+
"otp_secret": "52-character Authenticator App Key for Amazon 2SV",
69+
"public_url": "Public URL shared with external hosted services",
70+
"queue_delay": "Delay to queue multiple commands together (seconds)",
71+
"scan_interval": "Scheduled polling interval (seconds)",
72+
"should_get_network": "Discover Alexa network"
73+
},
74+
"data_description": {
75+
"debug": "Enables very verbose, trace-level logging for advanced troubleshooting.\nNot recommended for normal operation due to increased log volume.\nEnsure logger levels are set to DEBUG for full output.",
76+
"otp_secret": "Example: 35T5 LQSY I5IO 3EFQ LGAJ I6YB JWBY JJPR PYT7 XPPW IDAK SQBJ CVXA"
7077
}
7178
}
7279
}

0 commit comments

Comments
 (0)