Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
354cacd
Fix pterodactyl server config link (#154758)
electricsteve Oct 19, 2025
2d96e8a
Bump OpenRGB to Silver (#154690)
felipecrs Oct 19, 2025
769a770
Code quality followup to Homee stale devices (#154741)
Taraman17 Oct 19, 2025
b2699d8
Bump aioautomower to v2.3.1 (#151795)
Thomas55555 Oct 19, 2025
0f3de62
Refactor sensors and binary sensors in Xbox integration (#154719)
tr4nt0r Oct 19, 2025
591eb94
Moved non-translatable URL out of strings.json for plex (#154826)
AJ-SM Oct 19, 2025
204ff5d
Add valve group support (#154749)
thecode Oct 19, 2025
6edafd8
Fix incorrect forward header handling (#154793)
0xFaul Oct 19, 2025
e79c76c
Add reconfigure flow in SolarEdge (#154189)
tronikos Oct 19, 2025
2f5fbc1
Add instance ID (mDNS) conflict detection and repair flow for zerocon…
jpbede Oct 19, 2025
da6986e
Allow overriding recipients per message in XMPP (#149375)
gaaf Oct 19, 2025
0c342c4
vesync show fan speed for smart tower fans (#154842)
cdnninja Oct 19, 2025
fd08c55
declaraing typing fixes handling for agents (#154833)
johnmschoonover Oct 19, 2025
acead56
Enhance `check_config` script with JSON output and fail on warnings (…
BenjaminMichaelis Oct 19, 2025
bc77daf
OpenUV: Add protection window tests (#154498)
wbyoung Oct 19, 2025
ea22680
Tuya Alarm-Control: Ignore low-battery warnings (#152888)
asafhas Oct 19, 2025
4be428f
Set Pyright level as `basic` by default for VS Code (#154495)
felipecrs Oct 19, 2025
711526f
Remove brackets from decorator in Husqvarna Automower (#154042)
Thomas55555 Oct 19, 2025
d4e72ad
Refactor Xbox integration setup and exception handling (#154823)
tr4nt0r Oct 19, 2025
4912280
Portainer add endoint sensors (#154676)
erwindouna Oct 19, 2025
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 .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"python.analysis.typeCheckingMode": "basic",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
Expand Down
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ rules:
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
- **Testing**: pytest with plain functions and fixtures
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)

Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment",
// Pyright is too pedantic for Home Assistant
"python.analysis.typeCheckingMode": "basic",
"json.schemas": [
{
"fileMatch": [
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/group/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
Platform.VALVE,
]

_LOGGER = logging.getLogger(__name__)
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/group/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from .notify import async_create_preview_notify
from .sensor import async_create_preview_sensor
from .switch import async_create_preview_switch
from .valve import async_create_preview_valve

_STATISTIC_MEASURES = [
"last",
Expand Down Expand Up @@ -172,6 +173,7 @@ async def light_switch_options_schema(
"notify",
"sensor",
"switch",
"valve",
]


Expand Down Expand Up @@ -253,6 +255,11 @@ async def _set_group_type(
preview="group",
validate_user_input=set_group_type("switch"),
),
"valve": SchemaFlowFormStep(
basic_group_config_schema("valve"),
preview="group",
validate_user_input=set_group_type("valve"),
),
}


Expand Down Expand Up @@ -302,6 +309,10 @@ async def _set_group_type(
partial(light_switch_options_schema, "switch"),
preview="group",
),
"valve": SchemaFlowFormStep(
partial(basic_group_options_schema, "valve"),
preview="group",
),
}

PREVIEW_OPTIONS_SCHEMA: dict[str, vol.Schema] = {}
Expand All @@ -321,6 +332,7 @@ async def _set_group_type(
"notify": async_create_preview_notify,
"sensor": async_create_preview_sensor,
"switch": async_create_preview_switch,
"valve": async_create_preview_valve,
}


Expand Down
25 changes: 24 additions & 1 deletion homeassistant/components/group/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"media_player": "Media player group",
"notify": "Notify group",
"sensor": "Sensor group",
"switch": "Switch group"
"switch": "Switch group",
"valve": "Valve group"
}
},
"binary_sensor": {
Expand Down Expand Up @@ -127,6 +128,18 @@
"data_description": {
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
}
},
"valve": {
"title": "[%key:component::group::config::step::user::title%]",
"data": {
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
}
}
}
},
Expand Down Expand Up @@ -212,6 +225,16 @@
"data_description": {
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
}
},
"valve": {
"data": {
"all": "[%key:component::group::config::step::binary_sensor::data::all%]",
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
},
"data_description": {
"all": "[%key:component::group::config::step::binary_sensor::data_description::all%]"
}
}
}
},
Expand Down
262 changes: 262 additions & 0 deletions homeassistant/components/group/valve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
"""Platform allowing several valves to be grouped into one valve."""

from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant.components.valve import (
ATTR_CURRENT_POSITION,
ATTR_POSITION,
DOMAIN as VALVE_DOMAIN,
PLATFORM_SCHEMA as VALVE_PLATFORM_SCHEMA,
ValveEntity,
ValveEntityFeature,
ValveState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_ENTITIES,
CONF_NAME,
CONF_UNIQUE_ID,
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
SERVICE_SET_VALVE_POSITION,
SERVICE_STOP_VALVE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType

from .entity import GroupEntity
from .util import reduce_attribute

KEY_OPEN_CLOSE = "open_close"
KEY_STOP = "stop"
KEY_SET_POSITION = "set_position"

DEFAULT_NAME = "Valve Group"

# No limit on parallel updates to enable a group calling another group
PARALLEL_UPDATES = 0

PLATFORM_SCHEMA = VALVE_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ENTITIES): cv.entities_domain(VALVE_DOMAIN),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Valve Group platform."""
async_add_entities(
[
ValveGroup(
config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
)
]
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize Valve Group config entry."""
registry = er.async_get(hass)
entities = er.async_validate_entity_ids(
registry, config_entry.options[CONF_ENTITIES]
)

async_add_entities(
[ValveGroup(config_entry.entry_id, config_entry.title, entities)]
)


@callback
def async_create_preview_valve(
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
) -> ValveGroup:
"""Create a preview valve."""
return ValveGroup(
None,
name,
validated_config[CONF_ENTITIES],
)


class ValveGroup(GroupEntity, ValveEntity):
"""Representation of a ValveGroup."""

_attr_available: bool = False
_attr_current_valve_position: int | None = None
_attr_is_closed: bool | None = None
_attr_is_closing: bool | None = False
_attr_is_opening: bool | None = False
_attr_reports_position: bool = False

def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
"""Initialize a ValveGroup entity."""
self._entity_ids = entities
self._valves: dict[str, set[str]] = {
KEY_OPEN_CLOSE: set(),
KEY_STOP: set(),
KEY_SET_POSITION: set(),
}

self._attr_name = name
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities}
self._attr_unique_id = unique_id

@callback
def async_update_supported_features(
self,
entity_id: str,
new_state: State | None,
) -> None:
"""Update dictionaries with supported features."""
if not new_state:
for values in self._valves.values():
values.discard(entity_id)
return

features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)

if features & (ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE):
self._valves[KEY_OPEN_CLOSE].add(entity_id)
else:
self._valves[KEY_OPEN_CLOSE].discard(entity_id)
if features & (ValveEntityFeature.STOP):
self._valves[KEY_STOP].add(entity_id)
else:
self._valves[KEY_STOP].discard(entity_id)
if features & (ValveEntityFeature.SET_POSITION):
self._valves[KEY_SET_POSITION].add(entity_id)
else:
self._valves[KEY_SET_POSITION].discard(entity_id)

async def async_open_valve(self) -> None:
"""Open the valves."""
data = {ATTR_ENTITY_ID: self._valves[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
VALVE_DOMAIN, SERVICE_OPEN_VALVE, data, blocking=True, context=self._context
)

async def async_handle_open_valve(self) -> None: # type: ignore[misc]
"""Open the valves.

Override the base class to avoid calling the set position service
for all valves. Transfer the service call to the base class and let
it decide if the valve uses set position or open service.
"""
await self.async_open_valve()

async def async_close_valve(self) -> None:
"""Close valves."""
data = {ATTR_ENTITY_ID: self._valves[KEY_OPEN_CLOSE]}
await self.hass.services.async_call(
VALVE_DOMAIN,
SERVICE_CLOSE_VALVE,
data,
blocking=True,
context=self._context,
)

async def async_handle_close_valve(self) -> None: # type: ignore[misc]
"""Close the valves.

Override the base class to avoid calling the set position service
for all valves. Transfer the service call to the base class and let
it decide if the valve uses set position or close service.
"""
await self.async_close_valve()

async def async_set_valve_position(self, position: int) -> None:
"""Move the valves to a specific position."""
data = {
ATTR_ENTITY_ID: self._valves[KEY_SET_POSITION],
ATTR_POSITION: position,
}
await self.hass.services.async_call(
VALVE_DOMAIN,
SERVICE_SET_VALVE_POSITION,
data,
blocking=True,
context=self._context,
)

async def async_stop_valve(self) -> None:
"""Stop the valves."""
data = {ATTR_ENTITY_ID: self._valves[KEY_STOP]}
await self.hass.services.async_call(
VALVE_DOMAIN, SERVICE_STOP_VALVE, data, blocking=True, context=self._context
)

@callback
def async_update_group_state(self) -> None:
"""Update state and attributes."""
states = [
state
for entity_id in self._entity_ids
if (state := self.hass.states.get(entity_id)) is not None
]

# Set group as unavailable if all members are unavailable or missing
self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states)

self._attr_is_closed = True
self._attr_is_closing = False
self._attr_is_opening = False
self._attr_reports_position = False
self._update_assumed_state_from_members()
for state in states:
if state.attributes.get(ATTR_CURRENT_POSITION) is not None:
self._attr_reports_position = True
if state.state == ValveState.OPEN:
self._attr_is_closed = False
continue
if state.state == ValveState.CLOSED:
continue
if state.state == ValveState.CLOSING:
self._attr_is_closing = True
continue
if state.state == ValveState.OPENING:
self._attr_is_opening = True
continue

valid_state = any(
state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
)
if not valid_state:
# Set as unknown if all members are unknown or unavailable
self._attr_is_closed = None

self._attr_current_valve_position = reduce_attribute(
states, ATTR_CURRENT_POSITION
)

supported_features = ValveEntityFeature(0)
if self._valves[KEY_OPEN_CLOSE]:
supported_features |= ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
if self._valves[KEY_STOP]:
supported_features |= ValveEntityFeature.STOP
if self._valves[KEY_SET_POSITION]:
supported_features |= ValveEntityFeature.SET_POSITION
self._attr_supported_features = supported_features
Loading
Loading