This commit is contained in:
Franck Nijhof 2024-07-30 11:12:45 +02:00 committed by GitHub
commit 17930a6d66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1313 additions and 47 deletions

View File

@ -60,6 +60,7 @@ AUTH_EXCEPTIONS = (
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"]
}

View File

@ -73,6 +73,14 @@ SUPPORTED_SCHEMA_KEYS = {
def _format_schema(schema: dict[str, Any]) -> dict[str, Any]:
"""Format the schema to protobuf."""
if (subschemas := schema.get("anyOf")) or (subschemas := schema.get("allOf")):
for subschema in subschemas: # Gemini API does not support anyOf and allOf keys
if "type" in subschema: # Fallback to first subschema with 'type' field
return _format_schema(subschema)
return _format_schema(
subschemas[0]
) # Or, if not found, to any of the subschemas
result = {}
for key, val in schema.items():
if key not in SUPPORTED_SCHEMA_KEYS:
@ -81,12 +89,22 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]:
key = "type_"
val = val.upper()
elif key == "format":
if (schema.get("type") == "string" and val != "enum") or (
schema.get("type") not in ("number", "integer", "string")
):
continue
key = "format_"
elif key == "items":
val = _format_schema(val)
elif key == "properties":
val = {k: _format_schema(v) for k, v in val.items()}
result[key] = val
if result.get("type_") == "OBJECT" and not result.get("properties"):
# An object with undefined properties is not supported by Gemini API.
# Fallback to JSON string. This will probably fail for most tools that want it,
# but we don't have a better fallback strategy so far.
result["properties"] = {"json": {"type_": "STRING"}}
return result

View File

@ -11,6 +11,6 @@
"iot_class": "local_push",
"loggers": ["aiohue"],
"quality_scale": "platinum",
"requirements": ["aiohue==4.7.1"],
"requirements": ["aiohue==4.7.2"],
"zeroconf": ["_hue._tcp.local."]
}

View File

@ -55,7 +55,7 @@ async def async_setup_hue_events(bridge: HueBridge):
CONF_ID: slugify(f"{hue_device.metadata.name} Button"),
CONF_DEVICE_ID: device.id, # type: ignore[union-attr]
CONF_UNIQUE_ID: hue_resource.id,
CONF_TYPE: hue_resource.button.last_event.value,
CONF_TYPE: hue_resource.button.button_report.event.value,
CONF_SUBTYPE: hue_resource.metadata.control_id,
}
hass.bus.async_fire(ATTR_HUE_EVENT, data)
@ -79,7 +79,7 @@ async def async_setup_hue_events(bridge: HueBridge):
data = {
CONF_DEVICE_ID: device.id, # type: ignore[union-attr]
CONF_UNIQUE_ID: hue_resource.id,
CONF_TYPE: hue_resource.relative_rotary.last_event.action.value,
CONF_TYPE: hue_resource.relative_rotary.rotary_report.action.value,
CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value,
CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration,
CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps,

View File

@ -48,7 +48,7 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.0.5",
"aiolifx==1.0.6",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.4.15"
]

View File

@ -3,7 +3,7 @@
from __future__ import annotations
import mimetypes
from typing import Any
from typing import Any, cast
from mastodon import Mastodon
from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError
@ -71,11 +71,15 @@ class MastodonNotificationService(BaseNotificationService):
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Toot a message, with media perhaps."""
target = None
if (target_list := kwargs.get(ATTR_TARGET)) is not None:
target = cast(list[str], target_list)[0]
data = kwargs.get(ATTR_DATA)
media = None
mediadata = None
target = None
sensitive = False
content_warning = None
@ -87,7 +91,6 @@ class MastodonNotificationService(BaseNotificationService):
return
mediadata = self._upload_media(media)
target = data.get(ATTR_TARGET)
sensitive = data.get(ATTR_MEDIA_WARNING)
content_warning = data.get(ATTR_CONTENT_WARNING)

View File

@ -168,10 +168,10 @@ class MatterLock(MatterEntity, LockEntity):
LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)
if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched:
if lock_state == clusters.DoorLock.Enums.DlLockState.kUnlatched:
self._attr_is_locked = False
self._attr_is_open = True
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
elif lock_state == clusters.DoorLock.Enums.DlLockState.kLocked:
self._attr_is_locked = True
self._attr_is_open = False
elif lock_state in (

View File

@ -251,7 +251,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
mqtt_data.client.async_restore_tracked_subscriptions(
mqtt_data.subscriptions_to_restore
)
mqtt_data.subscriptions_to_restore = []
mqtt_data.subscriptions_to_restore = set()
mqtt_data.reload_dispatchers.append(
entry.add_update_listener(_async_config_entry_updated)
)

View File

@ -428,12 +428,12 @@ class MQTT:
await self.async_init_client()
@property
def subscriptions(self) -> list[Subscription]:
def subscriptions(self) -> set[Subscription]:
"""Return the tracked subscriptions."""
return [
return {
*chain.from_iterable(self._simple_subscriptions.values()),
*self._wildcard_subscriptions,
]
}
def cleanup(self) -> None:
"""Clean up listeners."""
@ -736,7 +736,7 @@ class MQTT:
@callback
def async_restore_tracked_subscriptions(
self, subscriptions: list[Subscription]
self, subscriptions: set[Subscription]
) -> None:
"""Restore tracked subscriptions after reload."""
for subscription in subscriptions:

View File

@ -423,7 +423,7 @@ class MqttData:
reload_handlers: dict[str, CALLBACK_TYPE] = field(default_factory=dict)
reload_schema: dict[str, VolSchemaType] = field(default_factory=dict)
state_write_requests: EntityTopicState = field(default_factory=EntityTopicState)
subscriptions_to_restore: list[Subscription] = field(default_factory=list)
subscriptions_to_restore: set[Subscription] = field(default_factory=set)
tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict)

View File

@ -18,5 +18,5 @@
"documentation": "https://www.home-assistant.io/integrations/reolink",
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"requirements": ["reolink-aio==0.9.4"]
"requirements": ["reolink-aio==0.9.5"]
}

View File

@ -132,7 +132,7 @@ async def _generate_trackables(
trackable = await trackable.details()
# Check that the pet has tracker linked.
if not trackable["device_id"]:
if not trackable.get("device_id"):
return None
if "details" not in trackable:

View File

@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["aiotractive"],
"requirements": ["aiotractive==0.5.6"]
"requirements": ["aiotractive==0.6.0"]
}

View File

@ -61,7 +61,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
TrafikverketSensorEntityDescription(
key="air_temp",
translation_key="air_temperature",
value_fn=lambda data: data.air_temp or 0,
value_fn=lambda data: data.air_temp,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@ -69,7 +69,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
TrafikverketSensorEntityDescription(
key="road_temp",
translation_key="road_temperature",
value_fn=lambda data: data.road_temp or 0,
value_fn=lambda data: data.road_temp,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
),
TrafikverketSensorEntityDescription(
key="wind_speed",
value_fn=lambda data: data.windforce or 0,
value_fn=lambda data: data.windforce,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
device_class=SensorDeviceClass.WIND_SPEED,
state_class=SensorStateClass.MEASUREMENT,
@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
TrafikverketSensorEntityDescription(
key="wind_speed_max",
translation_key="wind_speed_max",
value_fn=lambda data: data.windforcemax or 0,
value_fn=lambda data: data.windforcemax,
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
device_class=SensorDeviceClass.WIND_SPEED,
entity_registry_enabled_default=False,
@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
),
TrafikverketSensorEntityDescription(
key="humidity",
value_fn=lambda data: data.humidity or 0,
value_fn=lambda data: data.humidity,
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
entity_registry_enabled_default=False,
@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
),
TrafikverketSensorEntityDescription(
key="precipitation_amount",
value_fn=lambda data: data.precipitation_amount or 0,
value_fn=lambda data: data.precipitation_amount,
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
state_class=SensorStateClass.MEASUREMENT,
@ -130,7 +130,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = (
TrafikverketSensorEntityDescription(
key="dew_point",
translation_key="dew_point",
value_fn=lambda data: data.dew_point or 0,
value_fn=lambda data: data.dew_point,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,

View File

@ -950,6 +950,8 @@ class ViCareSensor(ViCareEntity, SensorEntity):
"""Initialize the sensor."""
super().__init__(device_config, api, description.key)
self.entity_description = description
# run update to have device_class set depending on unit_of_measurement
self.update()
@property
def available(self) -> bool:

View File

@ -579,6 +579,15 @@ DISCOVERY_SCHEMAS = [
),
entity_registry_enabled_default=False,
),
# ZVIDAR Z-CM-V01 (SmartWings/Deyi WM25L/V Z-Wave Motor for Roller Shade)
ZWaveDiscoverySchema(
platform=Platform.COVER,
hint="shade",
manufacturer_id={0x045A},
product_id={0x0507},
product_type={0x0904},
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
),
# Vision Security ZL7432 In Wall Dual Relay Switch
ZWaveDiscoverySchema(
platform=Platform.SWITCH,

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "3"
PATCH_VERSION: Final = "4"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -615,6 +615,9 @@ class ScriptTool(Tool):
entity_registry = er.async_get(hass)
self.name = split_entity_id(script_entity_id)[1]
if self.name[0].isdigit():
self.name = "_" + self.name
self._entity_id = script_entity_id
self.parameters = vol.Schema({})
entity_entry = entity_registry.async_get(script_entity_id)
if entity_entry and entity_entry.unique_id:
@ -715,7 +718,7 @@ class ScriptTool(Tool):
SCRIPT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: SCRIPT_DOMAIN + "." + self.name,
ATTR_ENTITY_ID: self._entity_id,
ATTR_VARIABLES: tool_input.tool_args,
},
context=llm_context.context,

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.7.3"
version = "2024.7.4"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -264,7 +264,7 @@ aioharmony==0.2.10
aiohomekit==3.1.5
# homeassistant.components.hue
aiohue==4.7.1
aiohue==4.7.2
# homeassistant.components.imap
aioimaplib==1.1.0
@ -282,7 +282,7 @@ aiolifx-effects==0.3.2
aiolifx-themes==0.4.15
# homeassistant.components.lifx
aiolifx==1.0.5
aiolifx==1.0.6
# homeassistant.components.livisi
aiolivisi==0.0.19
@ -386,7 +386,7 @@ aiosyncthing==0.5.1
aiotankerkoenig==0.4.1
# homeassistant.components.tractive
aiotractive==0.5.6
aiotractive==0.6.0
# homeassistant.components.unifi
aiounifi==79
@ -709,7 +709,7 @@ debugpy==1.8.1
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==8.1.1
deebot-client==8.2.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -2460,7 +2460,7 @@ renault-api==0.2.4
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.9.4
reolink-aio==0.9.5
# homeassistant.components.idteck_prox
rfk101py==0.0.1

View File

@ -240,7 +240,7 @@ aioharmony==0.2.10
aiohomekit==3.1.5
# homeassistant.components.hue
aiohue==4.7.1
aiohue==4.7.2
# homeassistant.components.imap
aioimaplib==1.1.0
@ -255,7 +255,7 @@ aiolifx-effects==0.3.2
aiolifx-themes==0.4.15
# homeassistant.components.lifx
aiolifx==1.0.5
aiolifx==1.0.6
# homeassistant.components.livisi
aiolivisi==0.0.19
@ -359,7 +359,7 @@ aiosyncthing==0.5.1
aiotankerkoenig==0.4.1
# homeassistant.components.tractive
aiotractive==0.5.6
aiotractive==0.6.0
# homeassistant.components.unifi
aiounifi==79
@ -590,7 +590,7 @@ dbus-fast==2.22.1
debugpy==1.8.1
# homeassistant.components.ecovacs
deebot-client==8.1.1
deebot-client==8.2.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -1924,7 +1924,7 @@ renault-api==0.2.4
renson-endura-delta==1.7.1
# homeassistant.components.reolink
reolink-aio==0.9.4
reolink-aio==0.9.5
# homeassistant.components.rflink
rflink==0.0.66

View File

@ -442,6 +442,24 @@
description: "Test function"
parameters {
type_: OBJECT
properties {
key: "param3"
value {
type_: OBJECT
properties {
key: "json"
value {
type_: STRING
}
}
}
}
properties {
key: "param2"
value {
type_: NUMBER
}
}
properties {
key: "param1"
value {
@ -449,7 +467,6 @@
description: "Test parameters"
items {
type_: STRING
format_: "lower"
}
}
}

View File

@ -185,7 +185,9 @@ async def test_function_call(
{
vol.Optional("param1", description="Test parameters"): [
vol.All(str, vol.Lower)
]
],
vol.Optional("param2"): vol.Any(float, int),
vol.Optional("param3"): dict,
}
)

View File

@ -1487,6 +1487,10 @@
"on": {
"on": true
},
"owner": {
"rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6",
"rtype": "zone"
},
"type": "grouped_light"
},
{
@ -1498,6 +1502,10 @@
"on": {
"on": true
},
"owner": {
"rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6",
"rtype": "zone"
},
"type": "grouped_light"
},
{
@ -1509,6 +1517,10 @@
"on": {
"on": false
},
"owner": {
"rid": "7cee478d-6455-483a-9e32-9f9fdcbcc4f6",
"rtype": "zone"
},
"type": "grouped_light"
},
{

View File

@ -28,7 +28,12 @@ async def test_hue_event(
# Emit button update event
btn_event = {
"button": {"last_event": "initial_press"},
"button": {
"button_report": {
"event": "initial_press",
"updated": "2021-10-01T12:00:00Z",
}
},
"id": "c658d3d8-a013-4b81-8ac6-78b248537e70",
"metadata": {"control_id": 1},
"type": "button",
@ -41,7 +46,7 @@ async def test_hue_event(
assert len(events) == 1
assert events[0].data["id"] == "wall_switch_with_2_controls_button"
assert events[0].data["unique_id"] == btn_event["id"]
assert events[0].data["type"] == btn_event["button"]["last_event"]
assert events[0].data["type"] == btn_event["button"]["button_report"]["event"]
assert events[0].data["subtype"] == btn_event["metadata"]["control_id"]

View File

@ -8,6 +8,7 @@ import pytest
from homeassistant.components.lock import (
STATE_LOCKED,
STATE_OPEN,
STATE_UNLOCKED,
LockEntityFeature,
)
@ -82,12 +83,12 @@ async def test_lock(
assert state
assert state.state == STATE_UNLOCKED
set_node_attribute(door_lock, 1, 257, 0, 0)
set_node_attribute(door_lock, 1, 257, 0, 1)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_UNLOCKED
assert state.state == STATE_LOCKED
set_node_attribute(door_lock, 1, 257, 0, None)
await trigger_subscription_callback(hass, matter_client)
@ -213,9 +214,16 @@ async def test_lock_with_unbolt(
assert state
assert state.state == STATE_OPENING
set_node_attribute(door_lock_with_unbolt, 1, 257, 3, 0)
set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 0)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_LOCKED
assert state.state == STATE_UNLOCKED
set_node_attribute(door_lock_with_unbolt, 1, 257, 0, 3)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get("lock.mock_door_lock_lock")
assert state
assert state.state == STATE_OPEN

View File

@ -472,6 +472,12 @@ def iblinds_v3_state_fixture():
return json.loads(load_fixture("zwave_js/cover_iblinds_v3_state.json"))
@pytest.fixture(name="zvidar_state", scope="package")
def zvidar_state_fixture():
"""Load the ZVIDAR node state fixture data."""
return json.loads(load_fixture("zwave_js/cover_zvidar_state.json"))
@pytest.fixture(name="qubino_shutter_state", scope="package")
def qubino_shutter_state_fixture():
"""Load the Qubino Shutter node state fixture data."""
@ -1081,6 +1087,14 @@ def iblinds_v3_cover_fixture(client, iblinds_v3_state):
return node
@pytest.fixture(name="zvidar")
def zvidar_cover_fixture(client, zvidar_state):
"""Mock a ZVIDAR window cover node."""
node = Node(client, copy.deepcopy(zvidar_state))
client.driver.controller.nodes[node.node_id] = node
return node
@pytest.fixture(name="qubino_shutter")
def qubino_shutter_cover_fixture(client, qubino_shutter_state):
"""Mock a Qubino flush shutter node."""

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,18 @@ async def test_iblinds_v2(hass: HomeAssistant, client, iblinds_v2, integration)
assert state
async def test_zvidar_state(hass: HomeAssistant, client, zvidar, integration) -> None:
"""Test that an ZVIDAR Z-CM-V01 multilevel switch value is discovered as a cover."""
node = zvidar
assert node.device_class.specific.label == "Unused"
state = hass.states.get("light.window_blind_controller")
assert not state
state = hass.states.get("cover.window_blind_controller")
assert state
async def test_ge_12730(hass: HomeAssistant, client, ge_12730, integration) -> None:
"""Test GE 12730 Fan Controller v2.0 multilevel switch is discovered as a fan."""
node = ge_12730

View File

@ -780,6 +780,46 @@ async def test_script_tool(
}
async def test_script_tool_name(hass: HomeAssistant) -> None:
"""Test that script tool name is not started with a digit."""
assert await async_setup_component(hass, "homeassistant", {})
context = Context()
llm_context = llm.LLMContext(
platform="test_platform",
context=context,
user_prompt="test_text",
language="*",
assistant="conversation",
device_id=None,
)
# Create a script with a unique ID
assert await async_setup_component(
hass,
"script",
{
"script": {
"123456": {
"description": "This is a test script",
"sequence": [],
"fields": {
"beer": {"description": "Number of beers", "required": True},
},
},
}
},
)
async_expose_entity(hass, "conversation", "script.123456", True)
api = await llm.async_get_api(hass, "assist", llm_context)
tools = [tool for tool in api.tools if isinstance(tool, llm.ScriptTool)]
assert len(tools) == 1
tool = tools[0]
assert tool.name == "_123456"
async def test_selector_serializer(
hass: HomeAssistant, llm_context: llm.LLMContext
) -> None: