diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 7dac8015b5481d..f5e4cc400c8129 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -101,6 +101,9 @@ "child_lock": { "name": "Child lock" }, + "pet_immunity_enabled": { + "name": "Pet immunity" + }, "routing": { "name": "Range extension" } diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index 34cc500cd60505..6ffd56be522091 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -108,6 +108,15 @@ class SHCSwitchEntityDescription(SwitchEntityDescription): on_value=BypassService.State.BYPASS_ACTIVE, should_poll=False, ), + "pet_immunity_enabled": SHCSwitchEntityDescription( + key="pet_immunity_enabled", + translation_key="pet_immunity_enabled", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + on_key="pet_immunity_enabled", + on_value=True, + should_poll=False, + ), } @@ -257,6 +266,18 @@ async def async_setup_entry( for switch in session.device_helper.shutter_contacts2 ) + entities.extend( + SHCSwitch( + hass=hass, + device=switch, + parent_id=shc_info.unique_id, + entry_id=config_entry.entry_id, + description=SWITCH_TYPES["pet_immunity_enabled"], + unique_id_suffix="pet_immunity", + ) + for switch in session.device_helper.motion_detectors2 + ) + async_add_entities(entities) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index ab99c8b94703b7..5ea1191b509bdb 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -171,7 +171,7 @@ def _try_connect(host: str, port: int) -> None: _LOGGER.debug("Caught HomeworksNoCredentialsProvided") raise SchemaFlowError("credentials_needed") from err except Exception as err: - _LOGGER.exception("Caught unexpected exception %s") + _LOGGER.exception("Caught unexpected exception") raise SchemaFlowError("unknown_error") from err diff --git a/homeassistant/components/indi_allsky/sensor.py b/homeassistant/components/indi_allsky/sensor.py index c90868471ea269..11098097c6ff2a 100644 --- a/homeassistant/components/indi_allsky/sensor.py +++ b/homeassistant/components/indi_allsky/sensor.py @@ -75,7 +75,8 @@ class IndiAllSkySensorEntityDescription(SensorEntityDescription): value_fn=lambda data: data.exposure.stars if data.exposure else None, ), IndiAllSkySensorEntityDescription( - key="temperature", + key="camera_sensor_temp", + translation_key="camera_sensor_temp", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, diff --git a/homeassistant/components/indi_allsky/strings.json b/homeassistant/components/indi_allsky/strings.json index 20318e771506cf..4405368225d3b8 100644 --- a/homeassistant/components/indi_allsky/strings.json +++ b/homeassistant/components/indi_allsky/strings.json @@ -31,6 +31,9 @@ "binmode": { "name": "Binning mode" }, + "camera_sensor_temp": { + "name": "Camera sensor temperature" + }, "exposure": { "name": "Exposure time" }, diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 6ed70942346a0e..08cac519f374f3 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -7,13 +7,15 @@ import probatio from homeassistant.const import CONF_HOST, Platform -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.typing import ConfigType +from .const import DOMAIN +from .services import async_setup_services + _LOGGER = logging.getLogger(__name__) -DOMAIN = "keba" PLATFORMS = (Platform.BINARY_SENSOR, Platform.SENSOR, Platform.LOCK, Platform.NOTIFY) CONF_RFID = "rfid" @@ -43,17 +45,6 @@ extra=probatio.ALLOW_EXTRA, ) -_SERVICE_MAP = { - "request_data": "async_request_data", - "set_energy": "async_set_energy", - "set_current": "async_set_current", - "authorize": "async_start", - "deauthorize": "async_stop", - "enable": "async_enable_ev", - "disable": "async_disable_ev", - "set_failsafe": "async_set_failsafe", -} - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Check connectivity and version of KEBA charging station.""" @@ -78,19 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: except ValueError as ex: _LOGGER.warning("Could not set failsafe mode %s", ex) - # Register services to hass - async def execute_service(call: ServiceCall) -> None: - """Execute a service to KEBA charging station. - - This must be a member function as we need access to the keba - object here. - """ - function_name = _SERVICE_MAP[call.service] - function_call = getattr(keba, function_name) - await function_call(call.data) - - for service in _SERVICE_MAP: - hass.services.async_register(DOMAIN, service, execute_service) + async_setup_services(hass) # Load components for platform in PLATFORMS: diff --git a/homeassistant/components/keba/const.py b/homeassistant/components/keba/const.py new file mode 100644 index 00000000000000..3e29c4a3c894cd --- /dev/null +++ b/homeassistant/components/keba/const.py @@ -0,0 +1,3 @@ +"""Constants for the KEBA charging station integration.""" + +DOMAIN = "keba" diff --git a/homeassistant/components/keba/services.py b/homeassistant/components/keba/services.py new file mode 100644 index 00000000000000..12ceb27e94bc8c --- /dev/null +++ b/homeassistant/components/keba/services.py @@ -0,0 +1,30 @@ +"""Support for KEBA charging station services.""" + +from homeassistant.core import HomeAssistant, ServiceCall, callback + +from .const import DOMAIN + +_SERVICE_MAP = { + "request_data": "async_request_data", + "set_energy": "async_set_energy", + "set_current": "async_set_current", + "authorize": "async_start", + "deauthorize": "async_stop", + "enable": "async_enable_ev", + "disable": "async_disable_ev", + "set_failsafe": "async_set_failsafe", +} + + +async def _async_execute_service(call: ServiceCall) -> None: + """Execute a service to KEBA charging station.""" + keba = call.hass.data[DOMAIN] + function_call = getattr(keba, _SERVICE_MAP[call.service]) + await function_call(call.data) + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register the KEBA services.""" + for service in _SERVICE_MAP: + hass.services.async_register(DOMAIN, service, _async_execute_service) diff --git a/homeassistant/components/litellm/entity.py b/homeassistant/components/litellm/entity.py index aa7a542c805ed1..a1c84484e422b3 100644 --- a/homeassistant/components/litellm/entity.py +++ b/homeassistant/components/litellm/entity.py @@ -38,8 +38,10 @@ def _format_tool( custom_serializer: Callable[[Any], Any] | None, ) -> ChatCompletionFunctionToolParam: """Format tool specification.""" - unsupported_keys = {"oneOf", "anyOf", "allOf"} - schema = to_openapi(tool.parameters, custom_serializer=custom_serializer) + unsupported_keys = {"oneOf", "anyOf", "allOf", "enum", "not"} + schema = to_openapi( + tool.parameters, custom_serializer=custom_serializer, openapi_version="3.1.0" + ) schema = {k: v for k, v in schema.items() if k not in unsupported_keys} tool_spec = FunctionDefinition( diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py index 8eecdd091b6bdd..c4ae699624c923 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -506,7 +506,8 @@ def attributes_prepare_discovery_update(self, config: DiscoveryInfoType) -> None _LOGGER.info( "Group member update received for entity %s, " "but this entity was not initialized with the `group` option. " - "Reload the MQTT integration or restart Home Assistant to activate" + "Reload the MQTT integration or restart Home Assistant to activate", + self.entity_id, ) self._attributes_config = config diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index edbcd5f85a13bf..8354619c885410 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -90,14 +90,14 @@ def brightness(self) -> int: async def async_turn_off(self, **kwargs: Any) -> None: """Put the table to sleep.""" await self._table.sleep() - _LOGGER.debug("Sisyphus table %s: sleep") + _LOGGER.debug("Sisyphus table %s: sleep", self._name) @override async def async_turn_on(self, **kwargs: Any) -> None: """Wake up the table if necessary, optionally changes brightness.""" if not self.is_on: await self._table.wakeup() - _LOGGER.debug("Sisyphus table %s: wakeup") + _LOGGER.debug("Sisyphus table %s: wakeup", self._name) if "brightness" in kwargs: await self._table.set_brightness(kwargs["brightness"] / 255.0) diff --git a/homeassistant/components/snoo/config_flow.py b/homeassistant/components/snoo/config_flow.py index d5713e29730781..02e098bf5675b1 100644 --- a/homeassistant/components/snoo/config_flow.py +++ b/homeassistant/components/snoo/config_flow.py @@ -49,7 +49,7 @@ async def async_step_user( except InvalidSnooAuth: errors["base"] = "invalid_auth" except Exception: - _LOGGER.exception("Unexpected exception %s") + _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: user_uuid = jwt.decode( diff --git a/tests/components/bosch_shc/conftest.py b/tests/components/bosch_shc/conftest.py index c12836b1ebbd8c..1fc83307660861 100644 --- a/tests/components/bosch_shc/conftest.py +++ b/tests/components/bosch_shc/conftest.py @@ -14,6 +14,7 @@ SHCLightSwitchBSM, SHCMicromoduleBlinds, SHCMicromoduleRelay, + SHCMotionDetector2, SHCPresenceSimulationSystem, SHCShutterContact2, SHCShutterControl, @@ -73,6 +74,7 @@ def mock_config_entry() -> MockConfigEntry: "micromodule_relays", "micromodule_shutter_controls", "motion_detectors", + "motion_detectors2", "roomthermostats", "shutter_contacts", "shutter_contacts2", @@ -332,3 +334,23 @@ def shutter_contact2_device( device.bypass = bypass device.bypass_infinite = bypass_infinite return device + + +def motion_detector2_device( + device_id: str = "hdm:ZigBee:motiondetector1", + name: str = "Motion Detector", + pet_immunity_enabled: bool = False, +) -> SHCMotionDetector2: + """Build a minimal device double for the motion_detectors2 bucket.""" + device = create_autospec(SHCMotionDetector2, instance=True, spec_set=True) + device.name = name + device.id = device_id + device.root_device_id = "test-mac" + device.serial = f"serial-{device_id}" + device.manufacturer = "Bosch" + device.device_model = "MD2" + device.device_services = [] + device.deleted = False + device.status = "AVAILABLE" + device.pet_immunity_enabled = pet_immunity_enabled + return device diff --git a/tests/components/bosch_shc/test_switch.py b/tests/components/bosch_shc/test_switch.py index 53ced8857858cf..d21f63c378e4fe 100644 --- a/tests/components/bosch_shc/test_switch.py +++ b/tests/components/bosch_shc/test_switch.py @@ -19,6 +19,7 @@ from .conftest import ( light_switch_bsm_device, micromodule_relay_device, + motion_detector2_device, presence_simulation_system_device, setup_integration, shutter_contact2_device, @@ -317,3 +318,39 @@ async def test_shutter_contact2_bypass_unique_id( assert bypass_entry is not None assert bypass_infinite_entry is not None assert bypass_entry.unique_id != bypass_infinite_entry.unique_id + + +@pytest.mark.parametrize( + "device_buckets", + [{"motion_detectors2": [motion_detector2_device(pet_immunity_enabled=False)]}], + indirect=True, +) +@pytest.mark.usefixtures("mock_session") +async def test_motion_detector2_pet_immunity( + hass: HomeAssistant, + mock_session: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """A Motion Detector 2's pet immunity setting is exposed and controllable.""" + await setup_integration(hass, mock_config_entry) + device = mock_session.device_helper.motion_detectors2[0] + + state = hass.states.get("switch.motion_detector_pet_immunity") + assert state is not None + assert state.state == "off" + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.motion_detector_pet_immunity"}, + blocking=True, + ) + assert device.pet_immunity_enabled is True + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.motion_detector_pet_immunity"}, + blocking=True, + ) + assert device.pet_immunity_enabled is False diff --git a/tests/components/indi_allsky/snapshots/test_sensor.ambr b/tests/components/indi_allsky/snapshots/test_sensor.ambr index f2b8a8a5031691..47365b1c0f68c4 100644 --- a/tests/components/indi_allsky/snapshots/test_sensor.ambr +++ b/tests/components/indi_allsky/snapshots/test_sensor.ambr @@ -49,6 +49,64 @@ 'state': 'unknown', }) # --- +# name: test_sensor_setup_and_states[sensor.indi_allsky_camera_sensor_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.indi_allsky_camera_sensor_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Camera sensor temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Camera sensor temperature', + 'platform': 'indi_allsky', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'camera_sensor_temp', + 'unique_id': '1234567890abcdef1234567890abcdef_camera_sensor_temp', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor_setup_and_states[sensor.indi_allsky_camera_sensor_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'INDI Allsky Camera sensor temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.indi_allsky_camera_sensor_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensor_setup_and_states[sensor.indi_allsky_exposure_time-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ @@ -313,61 +371,3 @@ 'state': 'unknown', }) # --- -# name: test_sensor_setup_and_states[sensor.indi_allsky_temperature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': list([ - None, - ]), - 'area_id': None, - 'capabilities': dict({ - : , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.indi_allsky_temperature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'object_id_base': 'Temperature', - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 1, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Temperature', - 'platform': 'indi_allsky', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '1234567890abcdef1234567890abcdef_temperature', - 'unit_of_measurement': , - }) -# --- -# name: test_sensor_setup_and_states[sensor.indi_allsky_temperature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - : 'temperature', - : 'INDI Allsky Temperature', - : , - : , - }), - 'context': , - 'entity_id': 'sensor.indi_allsky_temperature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'unknown', - }) -# --- diff --git a/tests/components/indi_allsky/test_sensor.py b/tests/components/indi_allsky/test_sensor.py index 212b43f6e3c4f7..9eda4d3c893b65 100644 --- a/tests/components/indi_allsky/test_sensor.py +++ b/tests/components/indi_allsky/test_sensor.py @@ -54,9 +54,9 @@ async def test_disabled_sensors( for entity_id in ( "sensor.indi_allsky_exposure_time", + "sensor.indi_allsky_camera_sensor_temperature", "sensor.indi_allsky_sky_quality", "sensor.indi_allsky_stars", - "sensor.indi_allsky_temperature", ): entry = entity_registry.async_get(entity_id) assert entry is not None @@ -105,7 +105,7 @@ async def test_sensor_updates( assert state is not None assert state.state == "0.185" - state = hass.states.get("sensor.indi_allsky_temperature") + state = hass.states.get("sensor.indi_allsky_camera_sensor_temperature") assert state is not None assert state.state == STATE_UNKNOWN @@ -133,6 +133,6 @@ async def test_sensor_updates( callback(replace(mock_exposure_data, temp=12.5)) await hass.async_block_till_done() - state = hass.states.get("sensor.indi_allsky_temperature") + state = hass.states.get("sensor.indi_allsky_camera_sensor_temperature") assert state is not None assert state.state == "12.5"