Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
92adcd8
add the velux KLF 200 gateway as device (#155434)
wollew Nov 11, 2025
d5da64d
Bump pyhive to 1.0.7 (#156309)
KJonline Nov 11, 2025
0a480a2
Remove import of config_entry_oauth2_flow in scaffold in favor of dir…
wmoss Nov 11, 2025
25d44e8
Enhance compressor phase with state translations in ViCare integratio…
CFenner Nov 11, 2025
2921e7e
Use pytest.mark.freeze_time in plaato tests (#156362)
emontnemery Nov 11, 2025
0f2ff29
Use pytest.mark.freeze_time in sleep_as_android tests (#156351)
emontnemery Nov 11, 2025
f502739
Use pytest.mark.freeze_time in zha tests (#156358)
emontnemery Nov 11, 2025
4e9da52
Use pytest.mark.freeze_time in utility_meter tests (#156361)
emontnemery Nov 11, 2025
aa9003a
Use pytest.mark.freeze_time in wake_word tests (#156360)
emontnemery Nov 11, 2025
bb7dc69
Use pytest.mark.freeze_time in yale_smart_alarm tests (#156359)
emontnemery Nov 11, 2025
55feb1e
Use pytest.mark.freeze_time in tomorrowio tests (#156355)
emontnemery Nov 11, 2025
3068e19
Use pytest.mark.freeze_time in telegram_bot tests (#156354)
emontnemery Nov 11, 2025
a6a1519
Use pytest.mark.freeze_time in snoo tests (#156353)
emontnemery Nov 11, 2025
8749b0d
Use pytest.mark.freeze_time in smhi tests (#156352)
emontnemery Nov 11, 2025
df34864
Use pytest.mark.freeze_time in openai_conversation tests (#156345)
emontnemery Nov 11, 2025
93025c9
Use pytest.mark.freeze_time in pglab tests (#156346)
emontnemery Nov 11, 2025
b375010
Use pytest.mark.freeze_time in playstation_network tests (#156347)
emontnemery Nov 11, 2025
f0e4296
Use pytest.mark.freeze_time in sensor tests (#156349)
emontnemery Nov 11, 2025
c18dc0a
Add support for Switchbot Smart thermostat radiator (#155123)
zerzhang Nov 11, 2025
3f22dba
Update pytest to 9.0.0 (#156365)
cdce8p Nov 11, 2025
ccdd54b
Fix support for Hyperion 2.1.1 (#156343)
antoniocifu Nov 11, 2025
cd379aa
Use pytest.mark.freeze_time in sensibo tests (#156348)
emontnemery Nov 11, 2025
c0e59c4
Add support for switchbot s20 (#156368)
zerzhang Nov 11, 2025
dcec6c3
Forbid to choose state in Ukraine Alarm integration (#156183)
PaulAnnekov Nov 11, 2025
8e8becc
tplink: handle repeated, unknown thermostat modes gracefully (#156310)
rytilahti Nov 11, 2025
ca2e7b9
Add Matter Eve Shutter device with corresponding fixtures and snapsho…
lboue Nov 11, 2025
12fc79e
Fix google_generative_ai_conversation tests opening sockets (#156371)
emontnemery Nov 11, 2025
1b0b6e6
Fix squeezebox tests opening sockets (#156373)
emontnemery Nov 11, 2025
85cd3c6
Remove redundant Z-Wave binary sensor `entity_description` arg (#156323)
TheJulianJES Nov 11, 2025
ee2e9dc
Fix homewizard tests opening sockets (#156370)
emontnemery Nov 11, 2025
cb086bb
Refactor media source platform in Xbox integration (#155925)
tr4nt0r Nov 11, 2025
df60de3
Add `In party` sensor to Xbox integration (#155967)
tr4nt0r Nov 11, 2025
c0f61f6
Improve code quality of music assistant config flow (#156263)
arturpragacz Nov 11, 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
2 changes: 1 addition & 1 deletion homeassistant/components/hive/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"],
"requirements": ["pyhive-integration==1.0.6"]
"requirements": ["pyhive-integration==1.0.7"]
}
4 changes: 3 additions & 1 deletion homeassistant/components/hyperion/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from aiohttp import web
from hyperion import client
from hyperion.const import (
KEY_DATA,
KEY_IMAGE,
KEY_IMAGE_STREAM,
KEY_LEDCOLORS,
Expand Down Expand Up @@ -155,7 +156,8 @@ async def _update_imagestream(self, img: dict[str, Any] | None = None) -> None:
"""Update Hyperion components."""
if not img:
return
img_data = img.get(KEY_RESULT, {}).get(KEY_IMAGE)
# Prefer KEY_DATA (Hyperion server >= 2.1.1); fall back to KEY_RESULT for older server versions
img_data = img.get(KEY_DATA, img.get(KEY_RESULT, {})).get(KEY_IMAGE)
if not img_data or not img_data.startswith(IMAGE_STREAM_JPG_SENTINEL):
return
async with self._image_cond:
Expand Down
114 changes: 40 additions & 74 deletions homeassistant/components/music_assistant/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,22 @@
from music_assistant_models.api import ServerInfoMessage
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo

from .const import DOMAIN, LOGGER

DEFAULT_URL = "http://mass.local:8095"
DEFAULT_TITLE = "Music Assistant"
DEFAULT_URL = "http://mass.local:8095"


def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema:
"""Return a schema for the manual step."""
default_url = user_input.get(CONF_URL, DEFAULT_URL)
return vol.Schema(
{
vol.Required(CONF_URL, default=default_url): str,
}
)
STEP_USER_SCHEMA = vol.Schema({vol.Required(CONF_URL): str})


async def get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage:
async def _get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage:
"""Validate the user input allows us to connect."""
async with MusicAssistantClient(
url, aiohttp_client.async_get_clientsession(hass)
Expand All @@ -52,25 +45,17 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):

def __init__(self) -> None:
"""Set up flow instance."""
self.server_info: ServerInfoMessage | None = None
self.url: str | None = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a manual configuration."""
errors: dict[str, str] = {}

if user_input is not None:
try:
self.server_info = await get_server_info(
self.hass, user_input[CONF_URL]
)
await self.async_set_unique_id(
self.server_info.server_id, raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_URL: user_input[CONF_URL]},
reload_on_update=True,
)
server_info = await _get_server_info(self.hass, user_input[CONF_URL])
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidServerVersion:
Expand All @@ -79,85 +64,66 @@ async def async_step_user(
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
server_info.server_id, raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_URL: user_input[CONF_URL]}
)

return self.async_create_entry(
title=DEFAULT_TITLE,
data={
CONF_URL: user_input[CONF_URL],
},
data={CONF_URL: user_input[CONF_URL]},
)

return self.async_show_form(
step_id="user", data_schema=get_manual_schema(user_input), errors=errors
)
suggested_values = user_input
if suggested_values is None:
suggested_values = {CONF_URL: DEFAULT_URL}

return self.async_show_form(step_id="user", data_schema=get_manual_schema({}))
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_SCHEMA, suggested_values
),
errors=errors,
)

async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle a discovered Mass server.

This flow is triggered by the Zeroconf component. It will check if the
host is already configured and delegate to the import step if not.
"""
# abort if discovery info is not what we expect
if "server_id" not in discovery_info.properties:
return self.async_abort(reason="missing_server_id")

self.server_info = ServerInfoMessage.from_dict(discovery_info.properties)
await self.async_set_unique_id(self.server_info.server_id)
"""Handle a zeroconf discovery for a Music Assistant server."""
try:
server_info = ServerInfoMessage.from_dict(discovery_info.properties)
except LookupError:
return self.async_abort(reason="invalid_discovery_info")

# Check if we already have a config entry for this server_id
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, self.server_info.server_id
)
self.url = server_info.base_url

if existing_entry:
# If the entry was ignored or disabled, don't make any changes
if existing_entry.source == SOURCE_IGNORE or existing_entry.disabled_by:
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(server_info.server_id)
self._abort_if_unique_id_configured(updates={CONF_URL: self.url})

# Test connectivity to the current URL first
current_url = existing_entry.data[CONF_URL]
try:
await get_server_info(self.hass, current_url)
# Current URL is working, no need to update
return self.async_abort(reason="already_configured")
except CannotConnect:
# Current URL is not working, update to the discovered URL
# and continue to discovery confirm
self.hass.config_entries.async_update_entry(
existing_entry,
data={**existing_entry.data, CONF_URL: self.server_info.base_url},
)
# Schedule reload since URL changed
self.hass.config_entries.async_schedule_reload(existing_entry.entry_id)
else:
# No existing entry, proceed with normal flow
self._abort_if_unique_id_configured()

# Test connectivity to the discovered URL
try:
await get_server_info(self.hass, self.server_info.base_url)
await _get_server_info(self.hass, self.url)
except CannotConnect:
return self.async_abort(reason="cannot_connect")

return await self.async_step_discovery_confirm()

async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle user-confirmation of discovered server."""
if TYPE_CHECKING:
assert self.server_info is not None
assert self.url is not None

if user_input is not None:
return self.async_create_entry(
title=DEFAULT_TITLE,
data={
CONF_URL: self.server_info.base_url,
},
data={CONF_URL: self.url},
)

self._set_confirm_only()
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={"url": self.server_info.base_url},
description_placeholders={"url": self.url},
)
7 changes: 7 additions & 0 deletions homeassistant/components/switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
Expand Down Expand Up @@ -102,6 +103,10 @@
SupportedModels.RELAY_SWITCH_2PM.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.GARAGE_DOOR_OPENER.value: [Platform.COVER, Platform.SENSOR],
SupportedModels.CLIMATE_PANEL.value: [Platform.SENSOR, Platform.BINARY_SENSOR],
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: [
Platform.CLIMATE,
Platform.SENSOR,
],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
Expand All @@ -119,6 +124,7 @@
SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade,
SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan,
SupportedModels.S10_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.S20_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_PRO_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum,
Expand All @@ -136,6 +142,7 @@
SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch,
SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM,
SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener,
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: switchbot.SwitchbotSmartThermostatRadiator,
}


Expand Down
140 changes: 140 additions & 0 deletions homeassistant/components/switchbot/climate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Support for Switchbot Climate devices."""

from __future__ import annotations

import logging
from typing import Any

import switchbot
from switchbot import (
ClimateAction as SwitchBotClimateAction,
ClimateMode as SwitchBotClimateMode,
)

from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .coordinator import SwitchbotConfigEntry
from .entity import SwitchbotEntity, exception_handler

SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE = {
SwitchBotClimateMode.HEAT: HVACMode.HEAT,
SwitchBotClimateMode.OFF: HVACMode.OFF,
}

HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE = {
HVACMode.HEAT: SwitchBotClimateMode.HEAT,
HVACMode.OFF: SwitchBotClimateMode.OFF,
}

SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION = {
SwitchBotClimateAction.HEATING: HVACAction.HEATING,
SwitchBotClimateAction.IDLE: HVACAction.IDLE,
SwitchBotClimateAction.OFF: HVACAction.OFF,
}

_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0


async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchbotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbot climate based on a config entry."""
coordinator = entry.runtime_data
async_add_entities([SwitchBotClimateEntity(coordinator)])


class SwitchBotClimateEntity(SwitchbotEntity, ClimateEntity):
"""Representation of a Switchbot Climate device."""

_device: switchbot.SwitchbotDevice
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_target_temperature_step = 0.5
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "climate"
_attr_name = None

@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
return self._device.min_temperature

@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
return self._device.max_temperature

@property
def preset_modes(self) -> list[str] | None:
"""Return the list of available preset modes."""
return self._device.preset_modes

@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
return self._device.preset_mode

@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
return SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE.get(
self._device.hvac_mode, HVACMode.OFF
)

@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available HVAC modes."""
return [
SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE[mode]
for mode in self._device.hvac_modes
]

@property
def hvac_action(self) -> HVACAction | None:
"""Return the current HVAC action."""
return SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION.get(
self._device.hvac_action, HVACAction.OFF
)

@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.current_temperature

@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._device.target_temperature

@exception_handler
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new HVAC mode."""
return await self._device.set_hvac_mode(
HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE[hvac_mode]
)

@exception_handler
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
return await self._device.set_preset_mode(preset_mode)

@exception_handler
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
return await self._device.set_target_temperature(temperature)
Loading
Loading