Compare commits

..

27 Commits

Author SHA1 Message Date
epenet f75e6e5257 Simplify opentherm_gw service actions 2025-06-13 14:45:07 +00:00
epenet ff17d79e73 Bump wakeonlan to 3.1.0 (#146655)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-13 08:58:44 -05:00
tronikos a8201009f3 Fix opower to work with aiohttp>=3.12.7 by disabling cookie quoting (#146697)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-13 08:58:27 -05:00
Simone Chemelli a349653282 Bump aioamazondevices to 3.1.2 (#146690) 2025-06-13 16:53:18 +03:00
epenet 355ee1178e Add callback decorator to async_setup_services (#146729) 2025-06-13 15:16:55 +02:00
Marc Mueller 30c5df3eaa Adjust core create_task tests with event_loop patch (#146699) 2025-06-13 15:16:28 +02:00
Marc Mueller 10874af19a Ignore lingering pycares shutdown thread (#146733) 2025-06-13 15:09:37 +02:00
Marc Mueller 704118b3d0 Remove unnecessary patch from toon tests (#146691) 2025-06-13 12:53:33 +02:00
Marc Mueller 7c575d0316 Fix asuswrt test patch (#146692) 2025-06-13 12:52:56 +02:00
starkillerOG ab3f11bfe7 Add Reolink IR brightness entity (#146717) 2025-06-13 12:50:12 +02:00
Allen Porter f0357539ad Add myself as a remote calendar code owner (#146703) 2025-06-13 12:48:24 +02:00
Allen Porter e70a2dd257 Partial revert of update to remote calendar to fix issue where calendar does not update (#146702)
Partial revert
2025-06-13 12:47:56 +02:00
Allen Porter 5ef99a15a5 Revert scan interval change in local calendar (#146700) 2025-06-13 12:46:01 +02:00
Marc Mueller 6421973cd6 Remove unnecessary patch from panel_custom tests (#146695) 2025-06-13 10:46:26 +02:00
Marc Mueller 7201171eb5 Replace unnecessary pydantic import in matrix tests (#146693) 2025-06-13 10:45:54 +02:00
Abílio Costa 1fb438fa6c Add missing mock value to Reolink test (#146689) 2025-06-13 07:43:21 +02:00
starkillerOG 89ae68c5af Reolink check if camera and motion supported (#146666) 2025-06-12 22:19:46 +01:00
Paul Bottein c78b66d5d5 Update frontend to 20250531.3 (#146638) 2025-06-12 16:52:09 -04:00
starkillerOG d756cf91ce Add model_id to Reolink IPC camera (#146664) 2025-06-12 20:41:13 +01:00
Simon Lamon 8d13bf93ab Bump linkplay to v0.2.12 (#146669) 2025-06-12 20:38:42 +01:00
Franck Nijhof e86e793842 Tweak non-English issue detection (#146636) 2025-06-12 13:38:20 -04:00
Tsvi Mostovicz 7e6bb021ce Bump hdate to 1.1.2 (#146659) 2025-06-12 18:29:47 +01:00
starkillerOG 680b70aa29 Reolink add diagnostics for baichuan (#146667)
* Add baichuan diagnostics

* adjust tests
2025-06-12 19:26:37 +02:00
Andre Lengwenus 8eebebc586 Bump pypck to 0.8.7 (#146657) 2025-06-12 17:36:50 +01:00
epenet 48e4624ba0 Add basic xiaomi_miio fan tests (#146593) 2025-06-12 17:33:45 +01:00
epenet b0cf974b34 Simplify swiss public transport service actions (#146611) 2025-06-12 16:27:20 +02:00
Simone Chemelli 171f7c5f81 Fix cookies with aiohttp >= 3.12.7 for Vodafone Station (#146647) 2025-06-12 16:24:10 +02:00
70 changed files with 738 additions and 411 deletions
@@ -64,16 +64,19 @@ jobs:
You are a language detection system. Your task is to determine if the provided text is written in English or another language.
Rules:
1. Analyze the text and determine the primary language
1. Analyze the text and determine the primary language of the USER'S DESCRIPTION only
2. IGNORE markdown headers (lines starting with #, ##, ###, etc.) as these are from issue templates, not user input
3. IGNORE all code blocks (text between ``` or ` markers) as they may contain system-generated error messages in other languages
4. Consider technical terms, code snippets, and URLs as neutral (they don't indicate non-English)
5. Focus on the actual sentences and descriptions written by the user
6. Return ONLY a JSON object with two fields:
- "is_english": boolean (true if the text is primarily in English, false otherwise)
4. IGNORE error messages, logs, and system output even if not in code blocks - these often appear in the user's system language
5. Consider technical terms, code snippets, URLs, and file paths as neutral (they don't indicate non-English)
6. Focus ONLY on the actual sentences and descriptions written by the user explaining their issue
7. If the user's explanation/description is in English but includes non-English error messages or logs, consider it ENGLISH
8. Return ONLY a JSON object with two fields:
- "is_english": boolean (true if the user's description is primarily in English, false otherwise)
- "detected_language": string (the name of the detected language, e.g., "English", "Spanish", "Chinese", etc.)
7. Be lenient - if the text is mostly English with minor non-English elements, consider it English
8. Common programming terms, error messages, and technical jargon should not be considered as non-English
9. Be lenient - if the user's explanation is in English with non-English system output, it's still English
10. Common programming terms, error messages, and technical jargon should not be considered as non-English
11. If you cannot reliably determine the language, set detected_language to "undefined"
Example response:
{"is_english": false, "detected_language": "Spanish"}
@@ -122,6 +125,12 @@ jobs:
return;
}
// If language is undefined or not detected, skip processing
if (!languageResult.detected_language || languageResult.detected_language === 'undefined') {
console.log('Language could not be determined, skipping processing');
return;
}
console.log(`Issue detected as non-English: ${languageResult.detected_language}`);
// Post comment explaining the language requirement
Generated
+2 -2
View File
@@ -1274,8 +1274,8 @@ build.json @home-assistant/supervisor
/tests/components/rehlko/ @bdraco @peterager
/homeassistant/components/remote/ @home-assistant/core
/tests/components/remote/ @home-assistant/core
/homeassistant/components/remote_calendar/ @Thomas55555
/tests/components/remote_calendar/ @Thomas55555
/homeassistant/components/remote_calendar/ @Thomas55555 @allenporter
/tests/components/remote_calendar/ @Thomas55555 @allenporter
/homeassistant/components/renault/ @epenet
/tests/components/renault/ @epenet
/homeassistant/components/renson/ @jimmyd-be
+20 -25
View File
@@ -613,39 +613,34 @@ async def async_enable_logging(
),
)
logger = logging.getLogger()
logger.setLevel(logging.INFO if verbose else logging.WARNING)
# Log errors to a file if we have write access to file or config dir
if log_file is None:
if "SUPERVISOR" in os.environ:
_LOGGER.info("Running in Supervisor, not logging to file")
err_log_path = None
else:
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
else:
err_log_path = os.path.abspath(log_file)
if err_log_path:
err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)
err_path_exists = os.path.isfile(err_log_path)
err_dir = os.path.dirname(err_log_path)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
err_handler = await hass.async_add_executor_job(
_create_log_file, err_log_path, log_rotate_days
)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger.addHandler(err_handler)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
logger = logging.getLogger()
logger.addHandler(err_handler)
logger.setLevel(logging.INFO if verbose else logging.WARNING)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
async_activate_log_queue_handler(hass)
+2 -1
View File
@@ -6,7 +6,7 @@ from jaraco.abode.exceptions import Exception as AbodeException
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send
@@ -70,6 +70,7 @@ def _trigger_automation(call: ServiceCall) -> None:
dispatcher_send(call.hass, signal)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Home Assistant services."""
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.0.6"]
"requirements": ["aioamazondevices==3.1.2"]
}
+2 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from homeassistant.auth.models import User
from homeassistant.auth.permissions.const import POLICY_CONTROL
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.service import async_extract_entity_ids
@@ -15,6 +15,7 @@ from .const import CAMERAS, DATA_AMCREST, DOMAIN
from .helpers import service_signal
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Amcrest IP Camera services."""
@@ -10,7 +10,7 @@ import threading
import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
@@ -141,6 +141,7 @@ def download_file(service: ServiceCall) -> None:
threading.Thread(target=do_download).start()
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the services for the downloader component."""
async_register_admin_service(
@@ -63,6 +63,7 @@ def _set_time_service(service: ServiceCall) -> None:
_async_get_elk_panel(service).set_time(dt_util.now())
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Create ElkM1 services."""
+2 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -35,6 +35,7 @@ async def _async_service_handle(service: ServiceCall) -> None:
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register FFmpeg services."""
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250531.2"]
"requirements": ["home-assistant-frontend==20250531.3"]
}
@@ -11,6 +11,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.helpers import config_validation as cv
@@ -49,6 +50,7 @@ async def _send_text_command(call: ServiceCall) -> ServiceResponse:
return None
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Assistant SDK."""
@@ -16,6 +16,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -77,6 +78,7 @@ def _read_file_contents(
return results
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register Google Photos services."""
@@ -13,7 +13,7 @@ from gspread.utils import ValueInputOption
import voluptuous as vol
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.selector import ConfigEntrySelector
@@ -76,6 +76,7 @@ async def _async_append_to_sheet(call: ServiceCall) -> None:
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Sheets."""
@@ -35,6 +35,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -249,6 +250,7 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
return entry
@callback
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
"""Set up services for Habitica integration."""
+2 -1
View File
@@ -8,7 +8,7 @@ import logging
from aiohue import HueBridgeV1, HueBridgeV2
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import verify_domain_control
@@ -25,6 +25,7 @@ from .const import (
LOGGER = logging.getLogger(__name__)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for Hue integration."""
+2 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util import slugify
@@ -115,6 +115,7 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount:
return icloud_account
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register iCloud services."""
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
"iot_class": "calculated",
"loggers": ["hdate"],
"requirements": ["hdate[astral]==1.1.1"],
"requirements": ["hdate[astral]==1.1.2"],
"single_config_entry": true
}
@@ -73,7 +73,7 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
translation_key="weekly_portion",
device_class=SensorDeviceClass.ENUM,
options_fn=lambda _: [str(p) for p in Parasha],
value_fn=lambda results: str(results.after_tzais_date.upcoming_shabbat.parasha),
value_fn=lambda results: results.after_tzais_date.upcoming_shabbat.parasha,
),
JewishCalendarSensorDescription(
key="holiday",
@@ -98,17 +98,13 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
key="omer_count",
translation_key="omer_count",
entity_registry_enabled_default=False,
value_fn=lambda results: (
results.after_shkia_date.omer.total_days
if results.after_shkia_date.omer
else 0
),
value_fn=lambda results: results.after_shkia_date.omer.total_days,
),
JewishCalendarSensorDescription(
key="daf_yomi",
translation_key="daf_yomi",
entity_registry_enabled_default=False,
value_fn=lambda results: str(results.daytime_date.daf_yomi),
value_fn=lambda results: results.daytime_date.daf_yomi,
),
)
@@ -15,6 +15,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
@@ -39,6 +40,7 @@ OMER_SCHEMA = vol.Schema(
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Jewish Calendar services."""
+1 -1
View File
@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.8.6", "lcn-frontend==0.2.5"]
"requirements": ["pypck==0.8.7", "lcn-frontend==0.2.5"]
}
+2
View File
@@ -16,6 +16,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -438,6 +439,7 @@ SERVICES = (
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for LCN."""
for service_name, service in SERVICES:
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["linkplay"],
"requirements": ["python-linkplay==0.2.11"],
"requirements": ["python-linkplay==0.2.12"],
"zeroconf": ["_linkplay._tcp.local."]
}
@@ -36,11 +36,6 @@ _LOGGER = logging.getLogger(__name__)
PRODID = "-//homeassistant.io//local_calendar 1.0//EN"
# The calendar on disk is only changed when this entity is updated, so there
# is no need to poll for changes. The calendar enttiy base class will handle
# refreshing the entity state based on the start or end time of the event.
SCAN_INTERVAL = timedelta(days=1)
async def async_setup_entry(
hass: HomeAssistant,
@@ -22,6 +22,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -66,6 +67,7 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> NordPoolConfigEntry:
return entry
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Nord Pool integration."""
+2 -1
View File
@@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -48,6 +48,7 @@ def set_speed(call: ServiceCall) -> None:
_get_coordinator(call).nzbget.rate(call.data[ATTR_SPEED])
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register integration-level services."""
@@ -11,6 +11,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import selector
@@ -70,6 +71,7 @@ def __get_client(call: ServiceCall) -> OhmeApiClient:
return entry.runtime_data.charge_session_coordinator.client
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services."""
@@ -16,6 +16,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -70,6 +71,7 @@ def _read_file_contents(
return results
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register OneDrive services."""
+2 -1
View File
@@ -8,7 +8,7 @@ import voluptuous as vol
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util.hass_dict import HassKey
@@ -40,6 +40,7 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema(
SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output"
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register Onkyo services."""
+196 -186
View File
@@ -15,7 +15,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
ATTR_TIME,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
@@ -46,6 +46,98 @@ from .const import (
if TYPE_CHECKING:
from . import OpenThermGatewayHub
_SERVICE_RESET_SCHEMA = vol.Schema({vol.Required(ATTR_GW_ID): vol.All(cv.string)})
_SERVICE_SET_CENTRAL_HEATING_OVRD_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_CH_OVRD): cv.boolean,
}
)
_SERVICE_SET_CLOCK_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Optional(ATTR_DATE, default=date.today): cv.date,
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
}
)
_SERVICE_SET_CONTROL_SETPOINT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0, max=90)
),
}
)
_SERVICE_SET_HOT_WATER_SETPOINT_SCHEMA = _SERVICE_SET_CONTROL_SETPOINT_SCHEMA
_SERVICE_SET_HOT_WATER_OVRD_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_DHW_OVRD): vol.Any(
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
),
}
)
_SERVICE_SET_GPIO_MODE_SCHEMA = vol.Schema(
vol.Any(
vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.Equal("A"),
vol.Required(ATTR_MODE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=6)
),
}
),
vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.Equal("B"),
vol.Required(ATTR_MODE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=7)
),
}
),
)
)
_SERVICE_SET_LED_MODE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.In("ABCDEF"),
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
}
)
_SERVICE_SET_MAX_MOD_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_LEVEL): vol.All(vol.Coerce(int), vol.Range(min=-1, max=100)),
}
)
_SERVICE_SET_OAT_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=-40, max=99)
),
}
)
_SERVICE_SET_SB_TEMP_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0, max=30)
),
}
)
_SERVICE_SEND_TRANSP_CMD_SCHEMA = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TRANSP_CMD): vol.All(
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
),
vol.Required(ATTR_TRANSP_ARG): vol.All(cv.string, vol.Length(min=1, max=12)),
}
)
def _get_gateway(call: ServiceCall) -> OpenThermGatewayHub:
gw_id: str = call.data[ATTR_GW_ID]
@@ -61,236 +153,154 @@ def _get_gateway(call: ServiceCall) -> OpenThermGatewayHub:
return gw_hub
async def _reset_gateway(call: ServiceCall) -> None:
"""Reset the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
mode_rst = gw_vars.OTGW_MODE_RESET
await gw_hub.gateway.set_mode(mode_rst)
async def _set_ch_ovrd(call: ServiceCall) -> None:
"""Set the central heating override on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
async def _set_control_setpoint(call: ServiceCall) -> None:
"""Set the control setpoint on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
async def _set_dhw_ovrd(call: ServiceCall) -> None:
"""Set the domestic hot water override on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
async def _set_dhw_setpoint(call: ServiceCall) -> None:
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
async def _set_device_clock(call: ServiceCall) -> None:
"""Set the clock on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
attr_date = call.data[ATTR_DATE]
attr_time = call.data[ATTR_TIME]
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
async def _set_gpio_mode(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway GPIO modes."""
gw_hub = _get_gateway(call)
gpio_id = call.data[ATTR_ID]
gpio_mode = call.data[ATTR_MODE]
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
async def _set_led_mode(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway LED modes."""
gw_hub = _get_gateway(call)
led_id = call.data[ATTR_ID]
led_mode = call.data[ATTR_MODE]
await gw_hub.gateway.set_led_mode(led_id, led_mode)
async def _set_max_mod(call: ServiceCall) -> None:
"""Set the max modulation level."""
gw_hub = _get_gateway(call)
level = call.data[ATTR_LEVEL]
if level == -1:
# Backend only clears setting on non-numeric values.
level = "-"
await gw_hub.gateway.set_max_relative_mod(level)
async def _set_outside_temp(call: ServiceCall) -> None:
"""Provide the outside temperature to the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
async def _set_setback_temp(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway SetBack temperature."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
async def _send_transparent_cmd(call: ServiceCall) -> None:
"""Send a transparent OpenTherm Gateway command."""
gw_hub = _get_gateway(call)
transp_cmd = call.data[ATTR_TRANSP_CMD]
transp_arg = call.data[ATTR_TRANSP_ARG]
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for the component."""
service_reset_schema = vol.Schema({vol.Required(ATTR_GW_ID): vol.All(cv.string)})
service_set_central_heating_ovrd_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_CH_OVRD): cv.boolean,
}
)
service_set_clock_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Optional(ATTR_DATE, default=date.today): cv.date,
vol.Optional(ATTR_TIME, default=lambda: datetime.now().time()): cv.time,
}
)
service_set_control_setpoint_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0, max=90)
),
}
)
service_set_hot_water_setpoint_schema = service_set_control_setpoint_schema
service_set_hot_water_ovrd_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_DHW_OVRD): vol.Any(
vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
),
}
)
service_set_gpio_mode_schema = vol.Schema(
vol.Any(
vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.Equal("A"),
vol.Required(ATTR_MODE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=6)
),
}
),
vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.Equal("B"),
vol.Required(ATTR_MODE): vol.All(
vol.Coerce(int), vol.Range(min=0, max=7)
),
}
),
)
)
service_set_led_mode_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_ID): vol.In("ABCDEF"),
vol.Required(ATTR_MODE): vol.In("RXTBOFHWCEMP"),
}
)
service_set_max_mod_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_LEVEL): vol.All(
vol.Coerce(int), vol.Range(min=-1, max=100)
),
}
)
service_set_oat_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=-40, max=99)
),
}
)
service_set_sb_temp_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Range(min=0, max=30)
),
}
)
service_send_transp_cmd_schema = vol.Schema(
{
vol.Required(ATTR_GW_ID): vol.All(cv.string),
vol.Required(ATTR_TRANSP_CMD): vol.All(
cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
),
vol.Required(ATTR_TRANSP_ARG): vol.All(
cv.string, vol.Length(min=1, max=12)
),
}
)
async def reset_gateway(call: ServiceCall) -> None:
"""Reset the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
mode_rst = gw_vars.OTGW_MODE_RESET
await gw_hub.gateway.set_mode(mode_rst)
hass.services.async_register(
DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema
DOMAIN, SERVICE_RESET_GATEWAY, _reset_gateway, _SERVICE_RESET_SCHEMA
)
async def set_ch_ovrd(call: ServiceCall) -> None:
"""Set the central heating override on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_ch_enable_bit(1 if call.data[ATTR_CH_OVRD] else 0)
hass.services.async_register(
DOMAIN,
SERVICE_SET_CH_OVRD,
set_ch_ovrd,
service_set_central_heating_ovrd_schema,
_set_ch_ovrd,
_SERVICE_SET_CENTRAL_HEATING_OVRD_SCHEMA,
)
async def set_control_setpoint(call: ServiceCall) -> None:
"""Set the control setpoint on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
hass.services.async_register(
DOMAIN,
SERVICE_SET_CONTROL_SETPOINT,
set_control_setpoint,
service_set_control_setpoint_schema,
_set_control_setpoint,
_SERVICE_SET_CONTROL_SETPOINT_SCHEMA,
)
async def set_dhw_ovrd(call: ServiceCall) -> None:
"""Set the domestic hot water override on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
hass.services.async_register(
DOMAIN,
SERVICE_SET_HOT_WATER_OVRD,
set_dhw_ovrd,
service_set_hot_water_ovrd_schema,
_set_dhw_ovrd,
_SERVICE_SET_HOT_WATER_OVRD_SCHEMA,
)
async def set_dhw_setpoint(call: ServiceCall) -> None:
"""Set the domestic hot water setpoint on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
hass.services.async_register(
DOMAIN,
SERVICE_SET_HOT_WATER_SETPOINT,
set_dhw_setpoint,
service_set_hot_water_setpoint_schema,
_set_dhw_setpoint,
_SERVICE_SET_HOT_WATER_SETPOINT_SCHEMA,
)
async def set_device_clock(call: ServiceCall) -> None:
"""Set the clock on the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
attr_date = call.data[ATTR_DATE]
attr_time = call.data[ATTR_TIME]
await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
hass.services.async_register(
DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema
DOMAIN, SERVICE_SET_CLOCK, _set_device_clock, _SERVICE_SET_CLOCK_SCHEMA
)
async def set_gpio_mode(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway GPIO modes."""
gw_hub = _get_gateway(call)
gpio_id = call.data[ATTR_ID]
gpio_mode = call.data[ATTR_MODE]
await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
hass.services.async_register(
DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
DOMAIN, SERVICE_SET_GPIO_MODE, _set_gpio_mode, _SERVICE_SET_GPIO_MODE_SCHEMA
)
async def set_led_mode(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway LED modes."""
gw_hub = _get_gateway(call)
led_id = call.data[ATTR_ID]
led_mode = call.data[ATTR_MODE]
await gw_hub.gateway.set_led_mode(led_id, led_mode)
hass.services.async_register(
DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
DOMAIN, SERVICE_SET_LED_MODE, _set_led_mode, _SERVICE_SET_LED_MODE_SCHEMA
)
async def set_max_mod(call: ServiceCall) -> None:
"""Set the max modulation level."""
gw_hub = _get_gateway(call)
level = call.data[ATTR_LEVEL]
if level == -1:
# Backend only clears setting on non-numeric values.
level = "-"
await gw_hub.gateway.set_max_relative_mod(level)
hass.services.async_register(
DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema
DOMAIN, SERVICE_SET_MAX_MOD, _set_max_mod, _SERVICE_SET_MAX_MOD_SCHEMA
)
async def set_outside_temp(call: ServiceCall) -> None:
"""Provide the outside temperature to the OpenTherm Gateway."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
hass.services.async_register(
DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema
DOMAIN, SERVICE_SET_OAT, _set_outside_temp, _SERVICE_SET_OAT_SCHEMA
)
async def set_setback_temp(call: ServiceCall) -> None:
"""Set the OpenTherm Gateway SetBack temperature."""
gw_hub = _get_gateway(call)
await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
hass.services.async_register(
DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema
DOMAIN, SERVICE_SET_SB_TEMP, _set_setback_temp, _SERVICE_SET_SB_TEMP_SCHEMA
)
async def send_transparent_cmd(call: ServiceCall) -> None:
"""Send a transparent OpenTherm Gateway command."""
gw_hub = _get_gateway(call)
transp_cmd = call.data[ATTR_TRANSP_CMD]
transp_arg = call.data[ATTR_TRANSP_ARG]
await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TRANSP_CMD,
send_transparent_cmd,
service_send_transp_cmd_schema,
_send_transparent_cmd,
_SERVICE_SEND_TRANSP_CMD_SCHEMA,
)
@@ -10,6 +10,7 @@ from opower import (
CannotConnect,
InvalidAuth,
Opower,
create_cookie_jar,
get_supported_utility_names,
select_utility,
)
@@ -39,7 +40,7 @@ async def _validate_login(
) -> dict[str, str]:
"""Validate login data and return any errors."""
api = Opower(
async_create_clientsession(hass),
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
login_data[CONF_UTILITY],
login_data[CONF_USERNAME],
login_data[CONF_PASSWORD],
@@ -12,6 +12,7 @@ from opower import (
MeterType,
Opower,
ReadResolution,
create_cookie_jar,
)
from opower.exceptions import ApiException, CannotConnect, InvalidAuth
@@ -30,7 +31,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client, issue_registry as ir
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -62,7 +64,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
update_interval=timedelta(hours=12),
)
self.api = Opower(
aiohttp_client.async_get_clientsession(hass),
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
config_entry.data[CONF_UTILITY],
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.12.3"]
"requirements": ["opower==0.12.4"]
}
+2 -1
View File
@@ -7,7 +7,7 @@ from typing import cast
from python_picnic_api2 import PicnicAPI
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from .const import (
@@ -26,6 +26,7 @@ class PicnicServiceException(Exception):
"""Exception for Picnic services."""
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for the Picnic integration, if not registered yet."""
+2 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.const import ATTR_COMMAND, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from .const import COMMANDS, DOMAIN, PS4_DATA
@@ -29,6 +29,7 @@ async def async_service_command(call: ServiceCall) -> None:
await device.async_send_command(command)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Handle for services."""
@@ -4,6 +4,7 @@ from datetime import datetime
import logging
from ical.event import Event
from ical.timeline import Timeline
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
@@ -48,12 +49,18 @@ class RemoteCalendarEntity(
super().__init__(coordinator)
self._attr_name = entry.data[CONF_CALENDAR_NAME]
self._attr_unique_id = entry.entry_id
self._event: CalendarEvent | None = None
self._timeline: Timeline | None = None
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return self._event
if self._timeline is None:
return None
now = dt_util.now()
events = self._timeline.active_after(now)
if event := next(events, None):
return _get_calendar_event(event)
return None
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
@@ -79,15 +86,12 @@ class RemoteCalendarEntity(
"""
await super().async_update()
def next_timeline_event() -> CalendarEvent | None:
def _get_timeline() -> Timeline | None:
"""Return the next active event."""
now = dt_util.now()
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
if event := next(events, None):
return _get_calendar_event(event)
return None
return self.coordinator.data.timeline_tz(now.tzinfo)
self._event = await self.hass.async_add_executor_job(next_timeline_event)
self._timeline = await self.hass.async_add_executor_job(_get_timeline)
def _get_calendar_event(event: Event) -> CalendarEvent:
@@ -1,7 +1,7 @@
{
"domain": "remote_calendar",
"name": "Remote Calendar",
"codeowners": ["@Thomas55555"],
"codeowners": ["@Thomas55555", "@allenporter"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/remote_calendar",
"integration_type": "service",
@@ -63,6 +63,7 @@ BINARY_PUSH_SENSORS = (
cmd_id=33,
device_class=BinarySensorDeviceClass.MOTION,
value=lambda api, ch: api.motion_detected(ch),
supported=lambda api, ch: api.supported(ch, "motion_detection"),
),
ReolinkBinarySensorEntityDescription(
key=FACE_DETECTION_TYPE,
@@ -37,23 +37,27 @@ CAMERA_ENTITIES = (
key="sub",
stream="sub",
translation_key="sub",
supported=lambda api, ch: api.supported(ch, "stream"),
),
ReolinkCameraEntityDescription(
key="main",
stream="main",
translation_key="main",
supported=lambda api, ch: api.supported(ch, "stream"),
entity_registry_enabled_default=False,
),
ReolinkCameraEntityDescription(
key="snapshots_sub",
stream="snapshots_sub",
translation_key="snapshots_sub",
supported=lambda api, ch: api.supported(ch, "snapshot"),
entity_registry_enabled_default=False,
),
ReolinkCameraEntityDescription(
key="snapshots",
stream="snapshots_main",
translation_key="snapshots_main",
supported=lambda api, ch: api.supported(ch, "snapshot"),
entity_registry_enabled_default=False,
),
ReolinkCameraEntityDescription(
@@ -39,6 +39,8 @@ async def async_get_config_entry_diagnostics(
"firmware version": api.sw_version,
"HTTPS": api.use_https,
"HTTP(S) port": api.port,
"Baichuan port": api.baichuan.port,
"Baichuan only": api.baichuan_only,
"WiFi connection": api.wifi_connection,
"WiFi signal": api.wifi_signal,
"RTMP enabled": api.rtmp_enabled,
@@ -190,6 +190,7 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
via_device=(DOMAIN, self._host.unique_id),
name=self._host.api.camera_name(dev_ch),
model=self._host.api.camera_model(dev_ch),
model_id=self._host.api.item_number(dev_ch),
manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.camera_hardware_version(dev_ch),
sw_version=self._host.api.camera_sw_version(dev_ch),
@@ -172,6 +172,9 @@
"floodlight_brightness": {
"default": "mdi:spotlight-beam"
},
"ir_brightness": {
"default": "mdi:led-off"
},
"volume": {
"default": "mdi:volume-high",
"state": {
@@ -122,6 +122,20 @@ NUMBER_ENTITIES = (
value=lambda api, ch: api.whiteled_brightness(ch),
method=lambda api, ch, value: api.set_whiteled(ch, brightness=int(value)),
),
ReolinkNumberEntityDescription(
key="ir_brightness",
cmd_key="208",
translation_key="ir_brightness",
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ir_brightness"),
value=lambda api, ch: api.baichuan.ir_brightness(ch),
method=lambda api, ch, value: (
api.baichuan.set_status_led(ch, ir_brightness=int(value))
),
),
ReolinkNumberEntityDescription(
key="volume",
cmd_key="GetAudioCfg",
@@ -532,6 +532,9 @@
"floodlight_brightness": {
"name": "Floodlight turn on brightness"
},
"ir_brightness": {
"name": "Infrared light brightness"
},
"volume": {
"name": "Volume"
},
+2 -2
View File
@@ -76,10 +76,10 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity)
def _wake_on_lan(self) -> None:
"""Wake the device via wake on lan."""
send_magic_packet(self._mac, ip_address=self._host)
send_magic_packet(self._mac, ip_address=self._host) # type: ignore[arg-type]
# If the ip address changed since we last saw the device
# broadcast a packet as well
send_magic_packet(self._mac)
send_magic_packet(self._mac) # type: ignore[arg-type]
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
@@ -38,7 +38,7 @@
"getmac==0.9.5",
"samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==2.1.0",
"wakeonlan==3.1.0",
"async-upnp-client==0.44.0"
],
"ssdp": [
@@ -35,7 +35,7 @@ from .coordinator import (
SwissPublicTransportDataUpdateCoordinator,
)
from .helper import offset_opendata, unique_id_from_config
from .services import setup_services
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -47,7 +47,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Swiss public transport component."""
setup_services(hass)
async_setup_services(hass)
return True
@@ -8,6 +8,7 @@ from homeassistant.core import (
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.selector import (
@@ -39,7 +40,7 @@ SERVICE_FETCH_CONNECTIONS_SCHEMA = vol.Schema(
)
def async_get_entry(
def _async_get_entry(
hass: HomeAssistant, config_entry_id: str
) -> SwissPublicTransportConfigEntry:
"""Get the Swiss public transport config entry."""
@@ -58,34 +59,36 @@ def async_get_entry(
return entry
def setup_services(hass: HomeAssistant) -> None:
async def _async_fetch_connections(
call: ServiceCall,
) -> ServiceResponse:
"""Fetch a set of connections."""
config_entry = _async_get_entry(call.hass, call.data[ATTR_CONFIG_ENTRY_ID])
limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT
try:
connections = await config_entry.runtime_data.fetch_connections_as_json(
limit=int(limit)
)
except UpdateFailed as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={
"error": str(e),
},
) from e
return {"connections": connections}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Swiss public transport integration."""
async def async_fetch_connections(
call: ServiceCall,
) -> ServiceResponse:
"""Fetch a set of connections."""
config_entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
limit = call.data.get(ATTR_LIMIT) or CONNECTIONS_COUNT
try:
connections = await config_entry.runtime_data.fetch_connections_as_json(
limit=int(limit)
)
except UpdateFailed as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={
"error": str(e),
},
) from e
return {"connections": connections}
hass.services.async_register(
DOMAIN,
SERVICE_FETCH_CONNECTIONS,
async_fetch_connections,
_async_fetch_connections,
schema=SERVICE_FETCH_CONNECTIONS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
@@ -7,7 +7,7 @@ from voluptuous import All, Range
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -98,6 +98,7 @@ def async_get_energy_site_for_entry(
return energy_data
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the Teslemetry services."""
@@ -303,6 +303,7 @@ SERVICES = [
]
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the global UniFi Protect services."""
@@ -9,5 +9,5 @@ from homeassistant.helpers import aiohttp_client
async def async_client_session(hass: HomeAssistant) -> ClientSession:
"""Return a new aiohttp session."""
return aiohttp_client.async_create_clientsession(
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
)
@@ -52,7 +52,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
await hass.async_add_executor_job(
partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs)
partial(wakeonlan.send_magic_packet, mac_address, **service_kwargs) # type: ignore[arg-type]
)
hass.services.async_register(
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/wake_on_lan",
"iot_class": "local_push",
"requirements": ["wakeonlan==2.1.0"]
"requirements": ["wakeonlan==3.1.0"]
}
+2 -1
View File
@@ -4,7 +4,7 @@ import voluptuous as vol
from yolink.client_request import ClientRequest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
@@ -25,6 +25,7 @@ _SPEAKER_HUB_PLAY_CALL_OPTIONAL_ATTRS = (
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register services for YoLink integration."""
@@ -58,6 +58,7 @@ TARGET_VALIDATORS = {
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register integration services."""
services = ZWaveServices(hass, er.async_get(hass), dr.async_get(hass))
+1 -1
View File
@@ -39,7 +39,7 @@ habluetooth==3.49.0
hass-nabucasa==0.101.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250531.2
home-assistant-frontend==20250531.3
home-assistant-intents==2025.6.10
httpx==0.28.1
ifaddr==0.2.0
+7 -7
View File
@@ -182,7 +182,7 @@ aioairzone-cloud==0.6.12
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==3.0.6
aioamazondevices==3.1.2
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -1133,7 +1133,7 @@ hass-splunk==0.1.1
hassil==2.2.3
# homeassistant.components.jewish_calendar
hdate[astral]==1.1.1
hdate[astral]==1.1.2
# homeassistant.components.heatmiser
heatmiserV3==2.0.3
@@ -1164,7 +1164,7 @@ hole==0.8.0
holidays==0.74
# homeassistant.components.frontend
home-assistant-frontend==20250531.2
home-assistant-frontend==20250531.3
# homeassistant.components.conversation
home-assistant-intents==2025.6.10
@@ -1617,7 +1617,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.12.3
opower==0.12.4
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -2236,7 +2236,7 @@ pypaperless==4.1.0
pypca==0.0.7
# homeassistant.components.lcn
pypck==0.8.6
pypck==0.8.7
# homeassistant.components.pglab
pypglab==0.0.5
@@ -2452,7 +2452,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.11
python-linkplay==0.2.12
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -3059,7 +3059,7 @@ vultr==0.1.2
# homeassistant.components.samsungtv
# homeassistant.components.wake_on_lan
wakeonlan==2.1.0
wakeonlan==3.1.0
# homeassistant.components.wallbox
wallbox==0.9.0
+7 -7
View File
@@ -170,7 +170,7 @@ aioairzone-cloud==0.6.12
aioairzone==1.0.0
# homeassistant.components.alexa_devices
aioamazondevices==3.0.6
aioamazondevices==3.1.2
# homeassistant.components.ambient_network
# homeassistant.components.ambient_station
@@ -988,7 +988,7 @@ hass-nabucasa==0.101.0
hassil==2.2.3
# homeassistant.components.jewish_calendar
hdate[astral]==1.1.1
hdate[astral]==1.1.2
# homeassistant.components.here_travel_time
here-routing==1.0.1
@@ -1010,7 +1010,7 @@ hole==0.8.0
holidays==0.74
# homeassistant.components.frontend
home-assistant-frontend==20250531.2
home-assistant-frontend==20250531.3
# homeassistant.components.conversation
home-assistant-intents==2025.6.10
@@ -1370,7 +1370,7 @@ openhomedevice==2.2.0
openwebifpy==4.3.1
# homeassistant.components.opower
opower==0.12.3
opower==0.12.4
# homeassistant.components.oralb
oralb-ble==0.17.6
@@ -1857,7 +1857,7 @@ pypalazzetti==0.1.19
pypaperless==4.1.0
# homeassistant.components.lcn
pypck==0.8.6
pypck==0.8.7
# homeassistant.components.pglab
pypglab==0.0.5
@@ -2022,7 +2022,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.11
python-linkplay==0.2.12
# homeassistant.components.lirc
# python-lirc==1.2.3
@@ -2518,7 +2518,7 @@ vultr==0.1.2
# homeassistant.components.samsungtv
# homeassistant.components.wake_on_lan
wakeonlan==2.1.0
wakeonlan==3.1.0
# homeassistant.components.wallbox
wallbox==0.9.0
@@ -56,6 +56,9 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
do_not_disturb=False,
response_style=None,
bluetooth_state=True,
entity_id="11111111-2222-3333-4444-555555555555",
appliance_id="G1234567890123456789012345678A",
sensors={},
)
}
client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get(
+6 -1
View File
@@ -175,7 +175,12 @@ async def test_error_invalid_ssh(hass: HomeAssistant, patch_is_file) -> None:
config_data = {k: v for k, v in CONFIG_DATA_SSH.items() if k != CONF_PASSWORD}
config_data[CONF_SSH_KEY] = SSH_KEY
patch_is_file.return_value = False
def mock_is_file(file) -> bool:
if str(file).endswith(SSH_KEY):
return False
return True
patch_is_file.side_effect = mock_is_file
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER, "show_advanced_options": True},
+3 -2
View File
@@ -1,6 +1,7 @@
"""Test MatrixBot._login."""
from pydantic.dataclasses import dataclass
from dataclasses import dataclass
import pytest
from homeassistant.components.matrix import MatrixBot
@@ -17,7 +18,7 @@ class LoginTestParameters:
access_token: dict[str, str]
expected_login_state: bool
expected_caplog_messages: set[str]
expected_expection: type(Exception) | None = None
expected_expection: type[Exception] | None = None
good_password_missing_token = LoginTestParameters(
+5 -8
View File
@@ -1,7 +1,5 @@
"""The tests for the panel_custom component."""
from unittest.mock import Mock, patch
from homeassistant import setup
from homeassistant.components import frontend, panel_custom
from homeassistant.core import HomeAssistant
@@ -22,14 +20,13 @@ async def test_webcomponent_custom_path_not_found(hass: HomeAssistant) -> None:
}
}
with patch("os.path.isfile", Mock(return_value=False)):
result = await setup.async_setup_component(hass, "panel_custom", config)
assert not result
result = await setup.async_setup_component(hass, "panel_custom", config)
assert not result
panels = hass.data.get(frontend.DATA_PANELS, [])
panels = hass.data.get(frontend.DATA_PANELS, [])
assert panels
assert "nice_url" not in panels
assert panels
assert "nice_url" not in panels
async def test_js_webcomponent(hass: HomeAssistant) -> None:
+1
View File
@@ -143,6 +143,7 @@ def reolink_connect_class() -> Generator[MagicMock]:
# Baichuan
host_mock.baichuan = create_autospec(Baichuan)
host_mock.baichuan_only = False
# Disable tcp push by default for tests
host_mock.baichuan.port = TEST_BC_PORT
host_mock.baichuan.events_active = False
@@ -10,6 +10,8 @@
'pushAlarm': 7,
}),
}),
'Baichuan only': False,
'Baichuan port': 5678,
'Chimes': dict({
'12345678': dict({
'channel': 0,
@@ -62,6 +64,10 @@
0,
]),
'cmd list': dict({
'208': dict({
'0': 1,
'null': 1,
}),
'296': dict({
'0': 1,
'null': 1,
+11 -5
View File
@@ -141,6 +141,7 @@ async def test_browsing(
entry_id = config_entry.entry_id
reolink_connect.supported.return_value = 1
reolink_connect.model = "Reolink TrackMix PoE"
reolink_connect.is_nvr = False
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(entry_id) is True
@@ -333,7 +334,14 @@ async def test_browsing_rec_playback_unsupported(
config_entry: MockConfigEntry,
) -> None:
"""Test browsing a Reolink camera which does not support playback of recordings."""
reolink_connect.supported.return_value = 0
def test_supported(ch, key):
"""Test supported function."""
if key == "replay":
return False
return True
reolink_connect.supported = test_supported
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -347,6 +355,8 @@ async def test_browsing_rec_playback_unsupported(
assert browse.identifier is None
assert browse.children == []
reolink_connect.supported = lambda ch, key: True # Reset supported function
async def test_browsing_errors(
hass: HomeAssistant,
@@ -354,8 +364,6 @@ async def test_browsing_errors(
config_entry: MockConfigEntry,
) -> None:
"""Test browsing a Reolink camera errors."""
reolink_connect.supported.return_value = 1
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@@ -373,8 +381,6 @@ async def test_browsing_not_loaded(
config_entry: MockConfigEntry,
) -> None:
"""Test browsing a Reolink camera integration which is not loaded."""
reolink_connect.supported.return_value = 1
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
+6 -7
View File
@@ -27,13 +27,12 @@ async def setup_component(hass: HomeAssistant) -> None:
{"external_url": "https://example.com"},
)
with patch("os.path.isfile", return_value=False):
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}},
)
await hass.async_block_till_done()
assert await async_setup_component(
hass,
DOMAIN,
{DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}},
)
await hass.async_block_till_done()
async def test_abort_if_no_configuration(hass: HomeAssistant) -> None:
@@ -0,0 +1,127 @@
# serializer version: 1
# name: test_fan_status[dmaker.fan.p18][fan.test_fan-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'preset_modes': list([
'Normal',
'Nature',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.test_fan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xiaomi_miio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <FanEntityFeature: 63>,
'translation_key': 'generic_fan',
'unique_id': '123456',
'unit_of_measurement': None,
})
# ---
# name: test_fan_status[dmaker.fan.p18][fan.test_fan-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'direction': None,
'friendly_name': 'test_fan',
'oscillating': None,
'percentage': None,
'percentage_step': 1.0,
'preset_mode': None,
'preset_modes': list([
'Normal',
'Nature',
]),
'supported_features': <FanEntityFeature: 63>,
}),
'context': <ANY>,
'entity_id': 'fan.test_fan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_fan_status[dmaker.fan.p5][fan.test_fan-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'preset_modes': list([
'Normal',
'Nature',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'fan',
'entity_category': None,
'entity_id': 'fan.test_fan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'xiaomi_miio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <FanEntityFeature: 63>,
'translation_key': 'generic_fan',
'unique_id': '123456',
'unit_of_measurement': None,
})
# ---
# name: test_fan_status[dmaker.fan.p5][fan.test_fan-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'direction': None,
'friendly_name': 'test_fan',
'oscillating': False,
'percentage': None,
'percentage_step': 1.0,
'preset_mode': 'Nature',
'preset_modes': list([
'Normal',
'Nature',
]),
'supported_features': <FanEntityFeature: 63>,
}),
'context': <ANY>,
'entity_id': 'fan.test_fan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
+130
View File
@@ -0,0 +1,130 @@
"""The tests for the xiaomi_miio fan component."""
from collections.abc import Generator
from unittest.mock import MagicMock, Mock, patch
from miio.integrations.fan.dmaker.fan import FanStatusP5
from miio.integrations.fan.dmaker.fan_miot import FanStatusMiot
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.xiaomi_miio import MODEL_TO_CLASS_MAP
from homeassistant.components.xiaomi_miio.const import CONF_FLOW_TYPE, DOMAIN
from homeassistant.const import (
CONF_DEVICE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_TOKEN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import TEST_MAC
from tests.common import MockConfigEntry, snapshot_platform
_MODEL_INFORMATION = {
"dmaker.fan.p5": {
"patch_class": "homeassistant.components.xiaomi_miio.FanP5",
"mock_status": FanStatusP5(
{
"roll_angle": 60,
"beep_sound": False,
"child_lock": False,
"time_off": 0,
"power": False,
"light": True,
"mode": "nature",
"roll_enable": False,
"speed": 64,
}
),
},
"dmaker.fan.p18": {
"patch_class": "homeassistant.components.xiaomi_miio.FanMiot",
"mock_status": FanStatusMiot(
{
"swing_mode_angle": 90,
"buzzer": False,
"child_lock": False,
"power_off_time": 0,
"power": False,
"light": True,
"mode": 0,
"swing_mode": False,
"fan_speed": 100,
}
),
},
}
@pytest.fixture(
name="model_code",
params=_MODEL_INFORMATION.keys(),
)
def get_model_code(request: pytest.FixtureRequest) -> str:
"""Parametrize model code."""
return request.param
@pytest.fixture(autouse=True)
def setup_device(model_code: str) -> Generator[MagicMock]:
"""Initialize test xiaomi_miio for fan entity."""
model_information = _MODEL_INFORMATION[model_code]
mock_fan = MagicMock()
mock_fan.status = Mock(return_value=model_information["mock_status"])
with (
patch(
"homeassistant.components.xiaomi_miio.get_platforms",
return_value=[Platform.FAN],
),
patch(model_information["patch_class"]) as mock_fan_cls,
patch.dict(
MODEL_TO_CLASS_MAP,
{model_code: mock_fan_cls} if model_code in MODEL_TO_CLASS_MAP else {},
),
):
mock_fan_cls.return_value = mock_fan
yield mock_fan
async def setup_component(
hass: HomeAssistant, model_code: str, entry_title: str
) -> MockConfigEntry:
"""Set up fan component."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="123456",
title=entry_title,
data={
CONF_FLOW_TYPE: CONF_DEVICE,
CONF_HOST: "192.168.1.100",
CONF_TOKEN: "12345678901234567890123456789012",
CONF_MODEL: model_code,
CONF_MAC: TEST_MAC,
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
async def test_fan_status(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
model_code: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test fan status."""
config_entry = await setup_component(hass, model_code, "test_fan")
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
+4 -2
View File
@@ -382,8 +382,10 @@ def verify_cleanup(
# Verify no threads where left behind.
threads = frozenset(threading.enumerate()) - threads_before
for thread in threads:
assert isinstance(thread, threading._DummyThread) or thread.name.startswith(
"waitpid-"
assert (
isinstance(thread, threading._DummyThread)
or thread.name.startswith("waitpid-")
or "_run_safe_shutdown_loop" in thread.name
)
try:
+4 -44
View File
@@ -85,21 +85,6 @@ async def test_async_enable_logging(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test to ensure logging is migrated to the queue handlers."""
config_log_file_pattern = get_test_config_dir("home-assistant.log*")
arg_log_file_pattern = "test.log*"
def cleanup_log_files() -> None:
"""Remove all log files."""
for f in glob.glob(config_log_file_pattern):
os.remove(f)
for f in glob.glob(arg_log_file_pattern):
os.remove(f)
# Ensure we start with a clean slate
cleanup_log_files()
assert len(glob.glob(config_log_file_pattern)) == 0
assert len(glob.glob(arg_log_file_pattern)) == 0
with (
patch("logging.getLogger"),
patch(
@@ -112,8 +97,6 @@ async def test_async_enable_logging(
):
await bootstrap.async_enable_logging(hass)
mock_async_activate_log_queue_handler.assert_called_once()
assert len(glob.glob(config_log_file_pattern)) > 0
mock_async_activate_log_queue_handler.reset_mock()
await bootstrap.async_enable_logging(
hass,
@@ -121,36 +104,13 @@ async def test_async_enable_logging(
log_file="test.log",
)
mock_async_activate_log_queue_handler.assert_called_once()
assert len(glob.glob(arg_log_file_pattern)) > 0
for f in glob.glob("test.log*"):
os.remove(f)
for f in glob.glob("testing_config/home-assistant.log*"):
os.remove(f)
assert "Error rolling over log file" in caplog.text
cleanup_log_files()
with (
patch.dict(os.environ, {"SUPERVISOR": "1"}),
patch("logging.getLogger"),
patch(
"homeassistant.bootstrap.async_activate_log_queue_handler"
) as mock_async_activate_log_queue_handler,
):
await bootstrap.async_enable_logging(hass)
mock_async_activate_log_queue_handler.assert_called_once()
# On Supervisor, the default log file should not be created
assert len(glob.glob(config_log_file_pattern)) == 0
mock_async_activate_log_queue_handler.reset_mock()
await bootstrap.async_enable_logging(
hass,
log_rotate_days=5,
log_file="test.log",
)
mock_async_activate_log_queue_handler.assert_called_once()
# Even on Supervisor, the log file should be created if explicitly set
assert len(glob.glob(arg_log_file_pattern)) > 0
cleanup_log_files()
async def test_load_hassio(hass: HomeAssistant) -> None:
"""Test that we load the hassio integration when using Supervisor."""
+47 -32
View File
@@ -255,45 +255,51 @@ async def test_async_add_hass_job_schedule_partial_callback() -> None:
partial = functools.partial(ha.callback(job))
ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(partial))
assert len(hass.loop.call_soon.mock_calls) == 1
assert len(hass.loop.create_task.mock_calls) == 0
assert len(hass.add_job.mock_calls) == 0
assert hass.loop.call_soon.call_count == 1
assert hass.loop.create_task.call_count == 0
assert hass.add_job.call_count == 0
async def test_async_add_hass_job_schedule_corofunction_eager_start() -> None:
"""Test that we schedule coroutines and add jobs to the job pool."""
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
async def job():
pass
with patch(
"homeassistant.core.create_eager_task", wraps=create_eager_task
) as mock_create_eager_task:
with (
patch(
"homeassistant.core.create_eager_task", wraps=create_eager_task
) as mock_create_eager_task,
patch.object(loop, "call_soon") as mock_loop_call_soon,
):
hass_job = ha.HassJob(job)
task = ha.HomeAssistant._async_add_hass_job(hass, hass_job)
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.add_job.mock_calls) == 0
assert mock_loop_call_soon.call_count == 0
assert hass.add_job.call_count == 0
assert mock_create_eager_task.mock_calls
await task
async def test_async_add_hass_job_schedule_partial_corofunction_eager_start() -> None:
"""Test that we schedule coroutines and add jobs to the job pool."""
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
async def job():
pass
partial = functools.partial(job)
with patch(
"homeassistant.core.create_eager_task", wraps=create_eager_task
) as mock_create_eager_task:
with (
patch(
"homeassistant.core.create_eager_task", wraps=create_eager_task
) as mock_create_eager_task,
patch.object(loop, "call_soon") as mock_loop_call_soon,
):
hass_job = ha.HassJob(partial)
task = ha.HomeAssistant._async_add_hass_job(hass, hass_job)
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.add_job.mock_calls) == 0
assert mock_loop_call_soon.call_count == 0
assert hass.add_job.call_count == 0
assert mock_create_eager_task.mock_calls
await task
@@ -306,35 +312,42 @@ async def test_async_add_job_add_hass_threaded_job_to_pool() -> None:
pass
ha.HomeAssistant._async_add_hass_job(hass, ha.HassJob(job))
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.loop.create_task.mock_calls) == 0
assert len(hass.loop.run_in_executor.mock_calls) == 2
assert hass.loop.call_soon.call_count == 0
assert hass.loop.create_task.call_count == 0
assert hass.loop.run_in_executor.call_count == 1
async def test_async_create_task_schedule_coroutine() -> None:
"""Test that we schedule coroutines and add jobs to the job pool."""
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
async def job():
pass
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=False)
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.loop.create_task.mock_calls) == 1
assert len(hass.add_job.mock_calls) == 0
with (
patch.object(loop, "call_soon") as mock_loop_call_soon,
patch.object(loop, "create_task") as mock_loop_create_task,
):
coro = job()
ha.HomeAssistant.async_create_task_internal(hass, coro, eager_start=False)
assert mock_loop_call_soon.call_count == 0
assert mock_loop_create_task.call_count == 1
assert hass.add_job.call_count == 0
await coro
async def test_async_create_task_eager_start_schedule_coroutine() -> None:
"""Test that we schedule coroutines and add jobs to the job pool."""
hass = MagicMock(loop=MagicMock(wraps=asyncio.get_running_loop()))
hass = MagicMock(loop=(loop := asyncio.get_running_loop()))
async def job():
pass
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True)
# Should create the task directly since 3.12 supports eager_start
assert len(hass.loop.create_task.mock_calls) == 0
assert len(hass.add_job.mock_calls) == 0
with patch.object(loop, "create_task") as mock_loop_create_task:
ha.HomeAssistant.async_create_task_internal(hass, job(), eager_start=True)
# Should create the task directly since 3.12 supports eager_start
assert mock_loop_create_task.call_count == 0
assert hass.add_job.call_count == 0
async def test_async_create_task_schedule_coroutine_with_name() -> None:
@@ -344,13 +357,15 @@ async def test_async_create_task_schedule_coroutine_with_name() -> None:
async def job():
pass
coro = job()
task = ha.HomeAssistant.async_create_task_internal(
hass, job(), "named task", eager_start=False
hass, coro, "named task", eager_start=False
)
assert len(hass.loop.call_soon.mock_calls) == 0
assert len(hass.loop.create_task.mock_calls) == 1
assert len(hass.add_job.mock_calls) == 0
assert hass.loop.call_soon.call_count == 0
assert hass.loop.create_task.call_count == 1
assert hass.add_job.call_count == 0
assert "named task" in str(task)
await coro
async def test_async_run_eager_hass_job_calls_callback() -> None: