Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.indevolt.*
homeassistant.components.indi_allsky.*
homeassistant.components.inels.*
homeassistant.components.infrared.*
homeassistant.components.input_button.*
Expand Down
6 changes: 4 additions & 2 deletions CODEOWNERS

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

3 changes: 2 additions & 1 deletion homeassistant/components/airvisual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
entity_registry as er,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.redact import partial_redact

from .const import (
CONF_CITY,
Expand Down Expand Up @@ -63,7 +64,7 @@ def async_get_cloud_api_update_interval(

LOGGER.debug(
"Leveling API key usage (%s): %s consumers, %s minutes between updates",
api_key,
partial_redact(api_key),
num_consumers,
minutes_between_api_calls,
)
Expand Down
87 changes: 51 additions & 36 deletions homeassistant/components/command_line/notify.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Support for command line notification services."""

import logging
import subprocess
import asyncio
from contextlib import suppress
from typing import Any, override

from homeassistant.components.notify import (
Expand All @@ -12,13 +12,10 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.process import kill_subprocess

from .const import CONF_COMMAND_TIMEOUT, DOMAIN, LOGGER
from .utils import create_platform_yaml_not_supported_issue, render_template_args

_LOGGER = logging.getLogger(__name__)


async def async_get_service(
hass: HomeAssistant,
Expand Down Expand Up @@ -46,40 +43,58 @@ def __init__(self, command: str, timeout: int) -> None:
self._timeout = timeout

@override
def send_message(self, message: str = "", **kwargs: Any) -> None:
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a command line."""
if not (command := render_template_args(self.hass, self.command)):
return

LOGGER.debug("Running with message: %s", message)

with subprocess.Popen( # noqa: S602 # shell by design
command,
universal_newlines=True,
stdin=subprocess.PIPE,
close_fds=False, # required for posix_spawn
shell=True,
) as proc:
try:
proc.communicate(input=message, timeout=self._timeout)
if proc.returncode != 0:
_LOGGER.error(
"Command failed (with return code %s): %s",
proc.returncode,
command,
)
except subprocess.TimeoutExpired as err:
_LOGGER.debug("Timeout for command: %s", command)
kill_subprocess(proc)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_error",
translation_placeholders={"command": command},
) from err
except subprocess.SubprocessError as err:
_LOGGER.debug("Error trying to exec command: %s", command)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={"command": command, "error": str(err)},
) from err
try:
proc = await asyncio.create_subprocess_shell( # shell by design
command,
stdin=asyncio.subprocess.PIPE,
close_fds=False, # required for posix_spawn
)
except OSError as err:
LOGGER.debug("Error trying to exec command: %s", command)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={"command": command, "error": str(err)},
) from err

try:
async with asyncio.timeout(self._timeout):
await proc.communicate(input=message.encode())
except TimeoutError as err:
LOGGER.debug("Timeout for command: %s", command)
with suppress(ProcessLookupError):
# The command may have exited between the timeout and the kill.
proc.kill()
if (stdin := proc.stdin) is not None and (
not stdin.is_closing() or stdin.transport.get_write_buffer_size()
):
# A still connected stdin pipe keeps proc.wait() pending forever,
# see https://bugs.python.org/issue43884.
stdin.transport.abort()
await proc.wait()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_error",
translation_placeholders={"command": command},
) from err
except asyncio.CancelledError:
# Kill synchronously so the child isn't orphaned; the event loop
# reaps it without awaiting wait(), which cancellation would
# interrupt anyway.
with suppress(ProcessLookupError):
proc.kill()
raise

if proc.returncode != 0:
LOGGER.error(
"Command failed (with return code %s): %s",
proc.returncode,
command,
)
6 changes: 6 additions & 0 deletions homeassistant/components/hue/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from aiohttp import client_exceptions
from aiohue import HueBridgeV1, HueBridgeV2, LinkButtonNotPressed, Unauthorized
from aiohue.errors import AiohueException, BridgeBusy
from aiohue.v2.scene_activity import SceneActivityTracker

from homeassistant import core
from homeassistant.components import persistent_notification
Expand All @@ -31,6 +32,7 @@
Platform.EVENT,
Platform.LIGHT,
Platform.SCENE,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
Expand All @@ -49,6 +51,7 @@ def __init__(self, hass: core.HomeAssistant, config_entry: HueConfigEntry) -> No
# Jobs to be executed when API is reset.
self.reset_jobs: list[core.CALLBACK_TYPE] = []
self.sensor_manager: SensorManager | None = None
self.scene_activity_tracker: SceneActivityTracker | None = None
self.logger = logging.getLogger(__name__)
# store actual api connection to bridge as api
app_key: str = self.config_entry.data[CONF_API_KEY]
Expand Down Expand Up @@ -121,6 +124,9 @@ async def async_initialize_bridge(self) -> bool:
else:
await async_setup_devices(self)
await async_setup_hue_events(self)
self.scene_activity_tracker = SceneActivityTracker(self.api.scenes)
self.scene_activity_tracker.start()
self.reset_jobs.append(self.scene_activity_tracker.stop)
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS_v2
)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/hue/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
}
}
}
},
"select": {
"active_scene": {
"default": "mdi:palette"
}
}
},
"services": {
Expand Down
19 changes: 14 additions & 5 deletions homeassistant/components/hue/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.scenes import ScenesController
from aiohue.v2.models.room import Room
from aiohue.v2.models.scene import Scene as HueScene, ScenePut as HueScenePut
from aiohue.v2.models.smart_scene import SmartScene as HueSmartScene, SmartSceneState
from aiohue.v2.models.zone import Zone
import probatio

from homeassistant.components.scene import ATTR_TRANSITION, Scene as SceneEntity
Expand Down Expand Up @@ -50,13 +52,21 @@ def async_add_entity(
event_type: EventType, resource: HueScene | HueSmartScene
) -> None:
"""Add entity from Hue resource."""
if (group := api.scenes.get_group(resource.id)) is None:
LOGGER.warning(
"Skipping Hue scene %s: group %s could not be resolved",
resource.id,
resource.group.rid,
)
return

# Catch creation errors to continue adding other scenes even if one fails
try:
entity: HueSceneEntityBase
if isinstance(resource, HueSmartScene):
entity = HueSmartSceneEntity(bridge, api.scenes, resource)
entity = HueSmartSceneEntity(bridge, api.scenes, resource, group)
else:
entity = HueSceneEntity(bridge, api.scenes, resource)
entity = HueSceneEntity(bridge, api.scenes, resource, group)
except KeyError, StopIteration:
LOGGER.exception("Unable to create Hue scene entity for %s", resource.id)
return
Expand Down Expand Up @@ -102,14 +112,13 @@ def __init__(
bridge: HueBridge,
controller: ScenesController,
resource: HueScene | HueSmartScene,
group: Room | Zone,
) -> None:
"""Initialize the entity."""
super().__init__(bridge, controller, resource)
self.resource = resource
self.controller = controller
if (hue_group := self.controller.get_group(self.resource.id)) is None:
raise KeyError(self.resource.group.rid)
self.hue_group = hue_group
self.hue_group = group
# we create a virtual service/device for Hue zones/rooms
# so we have a parent for grouped lights and scenes
self._attr_device_info = DeviceInfo(
Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/hue/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Support for select platform for Hue scenes (V2 only)."""

from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .bridge import HueConfigEntry
from .v2.select import async_setup_entry as setup_entry_v2

PARALLEL_UPDATES = 0


async def async_setup_entry(
hass: HomeAssistant,
config_entry: HueConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Hue select entities."""
bridge = config_entry.runtime_data
if bridge.api_version == 1:
# should not happen, but just in case
raise NotImplementedError("Select support is only available for V2 bridges")
await setup_entry_v2(hass, config_entry, async_add_entities)
5 changes: 5 additions & 0 deletions homeassistant/components/hue/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
}
}
},
"select": {
"active_scene": {
"name": "Scene"
}
},
"sensor": {
"zigbee_connectivity": {
"name": "Zigbee connectivity",
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/hue/v2/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
from aiohue.v2.models.light import Light
from aiohue.v2.models.light_level import LightLevel
from aiohue.v2.models.motion import Motion
from aiohue.v2.models.room import Room
from aiohue.v2.models.zone import Zone

type HueResource = Light | DevicePower | GroupedLight | LightLevel | Motion
type HueResource = (
Light | DevicePower | GroupedLight | LightLevel | Motion | Room | Zone
)


RESOURCE_TYPE_NAMES = {
Expand Down
Loading
Loading