Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
fec008c
Improve abode light type hints (#161756)
epenet Jan 28, 2026
a94d39e
Bump j178/prek-action from 1.0.12 to 1.1.0 (#161736)
dependabot[bot] Jan 28, 2026
6409574
Improve flux_led light type hints (#161760)
epenet Jan 28, 2026
1cb5621
Improve crownstone light type hints (#161758)
epenet Jan 28, 2026
33ae951
Improve shelly light type hints (#161761)
epenet Jan 28, 2026
5cb5b0e
Handle wait_for_trigger service actions when extracting references (#…
abmantis Jan 28, 2026
2f7a895
Cleanup deprecated dt util function (#161752)
epenet Jan 28, 2026
d6a830d
Improve deconz light type hints (#161769)
epenet Jan 28, 2026
b6772c4
Bump lunatone-rest-api-client to 0.6.3 (#161764)
MoonDevLT Jan 28, 2026
8e9e406
Fix labs description url check in hassfest (#161730)
arturpragacz Jan 28, 2026
6d215c2
Improve zwave_js light type hints (#161775)
epenet Jan 28, 2026
66612f9
Improve govee_light_local light type hints (#161772)
epenet Jan 28, 2026
0203f6e
Improve hue light type hints (#161766)
epenet Jan 28, 2026
1c1a99e
Improve elgato light type hints (#161771)
epenet Jan 28, 2026
75b7f80
Cleanup deprecated zeroconf aliases (#161746)
epenet Jan 28, 2026
570146c
Cleanup deprecated vacuum state constants (#161750)
epenet Jan 28, 2026
c6c5970
Cleanup deprecated water_heater alias (#161751)
epenet Jan 28, 2026
3ec96f2
Cleanup deprecated get access in Lovelace data (#161749)
epenet Jan 28, 2026
699b4b1
Improve demo light type hints (#161770)
epenet Jan 28, 2026
020d122
Enable snapshot analytics as labs feature (#160068)
arturpragacz Jan 28, 2026
f460bf3
Improve homekit_controller light type hints (#161773)
epenet Jan 28, 2026
dee07b2
Improve decora_wifi light type hints (#161759)
epenet Jan 28, 2026
b3e42a1
Improve tasmota light type hints (#161762)
epenet Jan 28, 2026
d3658a5
Improve upb light type hints (#161763)
epenet Jan 28, 2026
316d804
Improve cync light type hints (#161768)
epenet Jan 28, 2026
c08912f
Improve sunricher_dali light shorthand attributes (#161765)
epenet Jan 28, 2026
760a75d
Rename group attribute in LimitlessLED (#161701)
arturpragacz Jan 28, 2026
4bae0d1
Rename group attribute in Insteon (#161703)
arturpragacz Jan 28, 2026
e6399d2
Cleanup deprecated ssdp aliases (#161747)
epenet Jan 28, 2026
630a9b4
Cleanup deprecated usb alias (#161748)
epenet Jan 28, 2026
18bda2d
Remove Mastodon extra field attributes (#161659)
andrew-codechimp Jan 28, 2026
825da95
Remove bluesound sleep timer service (#161120)
joostlek Jan 28, 2026
9c27e12
Pass aiohttp websession to librehardwaremonitor-api (#161741)
Sab44 Jan 28, 2026
9f3b13d
Add Cloudflare R2 integration (#152825)
corrreia Jan 28, 2026
2df6238
Remove str from light color mode (#161755)
epenet Jan 28, 2026
8e3befc
Rename add-on to app in OTBR issue description (#161781)
tr4nt0r Jan 28, 2026
1221c5b
Rename add-on to app in SABnzbd config flow (#161783)
tr4nt0r Jan 28, 2026
91e2a31
Improve mqtt light tests (#161780)
epenet Jan 28, 2026
6fd27ec
Update template cover to new framework (#161481)
Petro31 Jan 28, 2026
84a09be
Make template weather consistent with itself and other platforms (#15…
Petro31 Jan 28, 2026
0e98e8c
Cleanup deprecated vacuum battery support from mqtt (#161745)
epenet Jan 28, 2026
d45ddd3
Add the ability to set Cleaning mode and mop mode for Q7 Vacs (#161725)
Lash-L Jan 28, 2026
8a08016
Rename add-on to app in Reolink issue description (#161787)
tr4nt0r Jan 28, 2026
d5e58c8
Add API server endpoint to options for Telegram bot (#161580)
hanwg Jan 28, 2026
7ff5f14
Bump pysiaalarm to 3.2.2 (#161788)
amitfin Jan 28, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ jobs:
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run prek
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12
uses: j178/prek-action@564dda4cfa5e96aafdc4a5696c4bf7b46baae5ac # v1.1.0
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
RUFF_OUTPUT_FORMAT: github
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions homeassistant/brands/cloudflare.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"domain": "cloudflare",
"name": "Cloudflare",
"integrations": ["cloudflare", "cloudflare_r2"]
}
4 changes: 2 additions & 2 deletions homeassistant/components/abode/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def hs_color(self) -> tuple[float, float] | None:
return _hs

@property
def color_mode(self) -> str | None:
def color_mode(self) -> ColorMode | None:
"""Return the color mode of the light."""
if self._device.is_dimmable and self._device.is_color_capable:
if self.hs_color is not None:
Expand All @@ -110,7 +110,7 @@ def color_mode(self) -> str | None:
return ColorMode.ONOFF

@property
def supported_color_modes(self) -> set[str] | None:
def supported_color_modes(self) -> set[ColorMode] | None:
"""Flag supported color modes."""
if self._device.is_dimmable and self._device.is_color_capable:
return {ColorMode.COLOR_TEMP, ColorMode.HS}
Expand Down
46 changes: 39 additions & 7 deletions homeassistant/components/analytics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.components import labs, websocket_api
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType
Expand All @@ -18,7 +18,13 @@
EntityAnalyticsModifications,
async_devices_payload,
)
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, PREFERENCE_SCHEMA
from .const import (
ATTR_ONBOARDED,
ATTR_PREFERENCES,
ATTR_SNAPSHOTS,
DOMAIN,
PREFERENCE_SCHEMA,
)
from .http import AnalyticsDevicesView

__all__ = [
Expand All @@ -44,29 +50,55 @@

DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)

LABS_SNAPSHOT_FEATURE = "snapshots"


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the analytics integration."""
analytics_config = config.get(DOMAIN, {})

# For now we want to enable device analytics only if the url option
# is explicitly listed in YAML.
if CONF_SNAPSHOTS_URL in analytics_config:
disable_snapshots = False
await labs.async_update_preview_feature(
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
)
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
else:
disable_snapshots = True
snapshots_url = None

analytics = Analytics(hass, snapshots_url, disable_snapshots)
analytics = Analytics(hass, snapshots_url)

# Load stored data
await analytics.load()

started = False

async def _async_handle_labs_update(
event: Event[labs.EventLabsUpdatedData],
) -> None:
"""Handle labs feature toggle."""
await analytics.save_preferences({ATTR_SNAPSHOTS: event.data["enabled"]})
if started:
await analytics.async_schedule()

@callback
def _async_labs_event_filter(event_data: labs.EventLabsUpdatedData) -> bool:
"""Filter labs events for this integration's snapshot feature."""
return (
event_data["domain"] == DOMAIN
and event_data["preview_feature"] == LABS_SNAPSHOT_FEATURE
)

async def start_schedule(_event: Event) -> None:
"""Start the send schedule after the started event."""
nonlocal started
started = True
await analytics.async_schedule()

hass.bus.async_listen(
labs.EVENT_LABS_UPDATED,
_async_handle_labs_update,
event_filter=_async_labs_event_filter,
)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)

websocket_api.async_register_command(hass, websocket_analytics)
Expand Down
19 changes: 12 additions & 7 deletions homeassistant/components/analytics/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
DOMAIN as ENERGY_DOMAIN,
is_configured as energy_is_configured,
)
from homeassistant.components.labs import async_is_preview_feature_enabled
from homeassistant.components.recorder import (
DOMAIN as RECORDER_DOMAIN,
get_instance as get_recorder_instance,
Expand Down Expand Up @@ -241,12 +242,10 @@ def __init__(
self,
hass: HomeAssistant,
snapshots_url: str | None = None,
disable_snapshots: bool = False,
) -> None:
"""Initialize the Analytics class."""
self._hass: HomeAssistant = hass
self._snapshots_url = snapshots_url
self._disable_snapshots = disable_snapshots

self._session = async_get_clientsession(hass)
self._data = AnalyticsData(False, {})
Expand All @@ -258,15 +257,13 @@ def __init__(
def preferences(self) -> dict:
"""Return the current active preferences."""
preferences = self._data.preferences
result = {
return {
ATTR_BASE: preferences.get(ATTR_BASE, False),
ATTR_DIAGNOSTICS: preferences.get(ATTR_DIAGNOSTICS, False),
ATTR_USAGE: preferences.get(ATTR_USAGE, False),
ATTR_STATISTICS: preferences.get(ATTR_STATISTICS, False),
ATTR_SNAPSHOTS: preferences.get(ATTR_SNAPSHOTS, False),
}
if not self._disable_snapshots:
result[ATTR_SNAPSHOTS] = preferences.get(ATTR_SNAPSHOTS, False)
return result

@property
def onboarded(self) -> bool:
Expand All @@ -291,6 +288,11 @@ def supervisor(self) -> bool:
"""Return bool if a supervisor is present."""
return is_hassio(self._hass)

@property
def _snapshots_enabled(self) -> bool:
"""Check if snapshots feature is enabled via labs."""
return async_is_preview_feature_enabled(self._hass, DOMAIN, "snapshots")

async def load(self) -> None:
"""Load preferences."""
stored = await self._store.async_load()
Expand Down Expand Up @@ -645,7 +647,10 @@ async def async_schedule(self) -> None:
),
)

if not self.preferences.get(ATTR_SNAPSHOTS, False) or self._disable_snapshots:
if (
not self.preferences.get(ATTR_SNAPSHOTS, False)
or not self._snapshots_enabled
):
LOGGER.debug("Snapshot analytics not scheduled")
if self._snapshot_scheduled:
self._snapshot_scheduled()
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/analytics/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@
"documentation": "https://www.home-assistant.io/integrations/analytics",
"integration_type": "system",
"iot_class": "cloud_push",
"preview_features": {
"snapshots": {
"feedback_url": "https://forms.gle/GqvRmgmghSDco8M46",
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
}
},
"quality_scale": "internal"
}
10 changes: 10 additions & 0 deletions homeassistant/components/analytics/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"preview_features": {
"snapshots": {
"description": "This free, open source device database of the Open Home Foundation helps users find useful information about smart home devices used in real installations.\n\nYou can help build it by anonymously sharing data about your devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).\n\nLearn more about the device database and how we process your data in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement), which you accept by opting in.",
"disable_confirmation": "Your data will no longer be shared with the Open Home Foundation's device database.",
"enable_confirmation": "This feature is still in development and may change. The device database is being refined based on user feedback and is not yet complete.",
"name": "Device database"
}
}
}
101 changes: 12 additions & 89 deletions homeassistant/components/automation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import Callable, Mapping
from dataclasses import dataclass
import logging
from typing import Any, Literal, Protocol, cast
from typing import Any, Protocol, cast

from propcache.api import cached_property
import voluptuous as vol
Expand All @@ -25,18 +25,11 @@
CONF_ACTIONS,
CONF_ALIAS,
CONF_CONDITIONS,
CONF_DEVICE_ID,
CONF_ENTITY_ID,
CONF_EVENT_DATA,
CONF_ID,
CONF_MODE,
CONF_OPTIONS,
CONF_PATH,
CONF_PLATFORM,
CONF_TARGET,
CONF_TRIGGERS,
CONF_VARIABLES,
CONF_ZONE,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
SERVICE_TOGGLE,
Expand All @@ -53,10 +46,13 @@
ServiceCall,
callback,
split_entity_id,
valid_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
from homeassistant.helpers import condition as condition_helper, config_validation as cv
from homeassistant.helpers import (
condition as condition_helper,
config_validation as cv,
trigger as trigger_helper,
)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.issue_registry import (
Expand Down Expand Up @@ -86,7 +82,6 @@
trace_get,
trace_path,
)
from homeassistant.helpers.trigger import async_initialize_triggers
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime
Expand Down Expand Up @@ -618,7 +613,7 @@ def referenced_labels(self) -> set[str]:
)

for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_LABEL_ID))
return referenced

@cached_property
Expand All @@ -633,7 +628,7 @@ def referenced_floors(self) -> set[str]:
)

for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_FLOOR_ID))
return referenced

@cached_property
Expand All @@ -646,7 +641,7 @@ def referenced_areas(self) -> set[str]:
referenced |= condition_helper.async_extract_targets(conf, ATTR_AREA_ID)

for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
referenced |= set(trigger_helper.async_extract_targets(conf, ATTR_AREA_ID))
return referenced

@property
Expand All @@ -666,7 +661,7 @@ def referenced_devices(self) -> set[str]:
referenced |= condition_helper.async_extract_devices(conf)

for conf in self._trigger_config:
referenced |= set(_trigger_extract_devices(conf))
referenced |= set(trigger_helper.async_extract_devices(conf))

return referenced

Expand All @@ -680,7 +675,7 @@ def referenced_entities(self) -> set[str]:
referenced |= condition_helper.async_extract_entities(conf)

for conf in self._trigger_config:
for entity_id in _trigger_extract_entities(conf):
for entity_id in trigger_helper.async_extract_entities(conf):
referenced.add(entity_id)

return referenced
Expand Down Expand Up @@ -954,7 +949,7 @@ async def _async_attach_triggers(
self._logger.error("Error rendering trigger variables: %s", err)
return None

return await async_initialize_triggers(
return await trigger_helper.async_initialize_triggers(
self.hass,
self._trigger_config,
self._async_trigger_if_enabled,
Expand Down Expand Up @@ -1238,78 +1233,6 @@ async def _async_process_if(
return result


@callback
def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
"""Extract devices from a trigger config."""
if trigger_conf[CONF_PLATFORM] == "device":
return [trigger_conf[CONF_DEVICE_ID]]

if (
trigger_conf[CONF_PLATFORM] == "event"
and CONF_EVENT_DATA in trigger_conf
and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA]
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str)
):
return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]]

if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]

if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
return target_devices

return []


@callback
def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
"""Extract entities from a trigger config."""
if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"):
return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return]

if trigger_conf[CONF_PLATFORM] == "calendar":
return [trigger_conf[CONF_OPTIONS][CONF_ENTITY_ID]]

if trigger_conf[CONF_PLATFORM] == "zone":
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return]

if trigger_conf[CONF_PLATFORM] == "geo_location":
return [trigger_conf[CONF_ZONE]]

if trigger_conf[CONF_PLATFORM] == "sun":
return ["sun.sun"]

if (
trigger_conf[CONF_PLATFORM] == "event"
and CONF_EVENT_DATA in trigger_conf
and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA]
and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str)
and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID])
):
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]

if target_entities := _get_targets_from_trigger_config(
trigger_conf, CONF_ENTITY_ID
):
return target_entities

return []


@callback
def _get_targets_from_trigger_config(
config: dict,
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
) -> list[str]:
"""Extract targets from a target config."""
if not (target_conf := config.get(CONF_TARGET)):
return []
if not (targets := target_conf.get(target)):
return []

return [targets] if isinstance(targets, str) else targets


@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
def websocket_config(
hass: HomeAssistant,
Expand Down
Loading
Loading