diff --git a/custom_components/versatile_thermostat/underlyings.py b/custom_components/versatile_thermostat/underlyings.py index bfe1914..6c6438b 100644 --- a/custom_components/versatile_thermostat/underlyings.py +++ b/custom_components/versatile_thermostat/underlyings.py @@ -228,8 +228,8 @@ class UnderlyingSwitch(UnderlyingEntity): self._on_time_sec = 0 self._off_time_sec = 0 self._keep_alive = IntervalCaller(hass, keep_alive_sec) - self._vswitch_on = vswitch_on - self._vswitch_off = vswitch_off + self._vswitch_on = vswitch_on.strip() if vswitch_on else None + self._vswitch_off = vswitch_off.strip() if vswitch_off else None self._domain = self._entity_id.split(".")[0] # build command command, data, state_on = self.build_command(use_on=True) @@ -312,9 +312,10 @@ class UnderlyingSwitch(UnderlyingEntity): value = None data = {ATTR_ENTITY_ID: self._entity_id} - vswitch = self._vswitch_on if use_on and not self.is_inversed else self._vswitch_off + take_on = (use_on and not self.is_inversed) or (not use_on and self.is_inversed) + vswitch = self._vswitch_on if take_on else self._vswitch_off if vswitch: - pattern = r"^(?P[^/]+)(?:/(?P[^:]+)(?::(?P.*))?)?$" + pattern = r"^(?P[^\s/]+)(?:/(?P[^\s:]+)(?::(?P[^\s]+))?)?$" match = re.match(pattern, vswitch) if match: @@ -322,13 +323,16 @@ class UnderlyingSwitch(UnderlyingEntity): command = match.group("command") argument = match.group("argument") value = match.group("value") - data.update({argument: value}) + if argument is not None and value is not None: + data.update({argument: value}) else: - raise ValueError(f"Invalid input format: {vswitch}") + raise ValueError(f"Invalid input format: {vswitch}. Must be conform to 'command[/argument[:value]]'") else: command = SERVICE_TURN_ON if use_on and not self.is_inversed else SERVICE_TURN_OFF - value = STATE_ON if use_on and not self.is_inversed else STATE_OFF + + if value is None: + value = STATE_ON if take_on else STATE_OFF return command, data, value diff --git a/tests/test_virtual_switch.py b/tests/test_virtual_switch.py index 6376dc8..4d19f49 100644 --- a/tests/test_virtual_switch.py +++ b/tests/test_virtual_switch.py @@ -13,12 +13,68 @@ from custom_components.versatile_thermostat.thermostat_switch import ThermostatO @pytest.mark.parametrize( - "is_inversed, vswitch_on_command, vswitch_off_command, expected_command_on, expected_data_on, expected_state_on, expected_command_off, expected_data_off, expected_state_off", + "is_inversed, vswitch_on_command, vswitch_off_command, expected_command_on, expected_data_on, expected_state_on, expected_command_off, expected_data_off, expected_state_off, is_ok", [ - # Select + # Select (with stripping - trim) ( False, + " select_option/option:comfort ", + " select_option/option:frost ", + "select_option", + {"entity_id": "switch.test", "option": "comfort"}, + PRESET_COMFORT, + "select_option", + {"entity_id": "switch.test", "option": "frost"}, + PRESET_FROST_PROTECTION, + True, + ), + # Inversed Select + ( + True, "select_option/option:comfort", + "select_option/option:eco", + "select_option", + {"entity_id": "switch.test", "option": "eco"}, + PRESET_ECO, + "select_option", + {"entity_id": "switch.test", "option": "comfort"}, + PRESET_COMFORT, + True, + ), + # switch + (False, "turn_on", "turn_off", "turn_on", {"entity_id": "switch.test"}, STATE_ON, "turn_off", {"entity_id": "switch.test"}, STATE_OFF, True), + # inversed switch + (True, "turn_on", "turn_off", "turn_off", {"entity_id": "switch.test"}, STATE_OFF, "turn_on", {"entity_id": "switch.test"}, STATE_ON, True), + # Climate + ( + False, + "set_hvac_mode/hvac_mode:heat", + "set_hvac_mode/hvac_mode:off", + "set_hvac_mode", + {"entity_id": "switch.test", "hvac_mode": "heat"}, + HVACMode.HEAT, + "set_hvac_mode", + {"entity_id": "switch.test", "hvac_mode": "off"}, + HVACMode.OFF, + True, + ), + # Inversed Climate + ( + True, + "set_hvac_mode/hvac_mode:heat", + "set_hvac_mode/hvac_mode:off", + "set_hvac_mode", + {"entity_id": "switch.test", "hvac_mode": "off"}, + HVACMode.OFF, + "set_hvac_mode", + {"entity_id": "switch.test", "hvac_mode": "heat"}, + HVACMode.HEAT, + True, + ), + # Error cases invalid command + ( + False, + "select_ option/option:comfort", # whitespace "select_option/option:frost", "select_option", {"entity_id": "switch.test", "option": "comfort"}, @@ -26,12 +82,35 @@ from custom_components.versatile_thermostat.thermostat_switch import ThermostatO "select_option", {"entity_id": "switch.test", "option": "frost"}, PRESET_FROST_PROTECTION, + False, + ), + ( + False, + "select_option/option comfort", # whitespace + "select_option/option:frost", + "select_option", + {"entity_id": "switch.test", "option": "comfort"}, + PRESET_COMFORT, + "select_option", + {"entity_id": "switch.test", "option": "frost"}, + PRESET_FROST_PROTECTION, + False, + ), + ( + False, + "select_option/option:com fort", # whitespace + "select_option/option:frost", + "select_option", + {"entity_id": "switch.test", "option": "comfort"}, + PRESET_COMFORT, + "select_option", + {"entity_id": "switch.test", "option": "frost"}, + PRESET_FROST_PROTECTION, + False, ), - # switch - (False, "turn_on/:on", "turn_off/:off", "turn_on", {"entity_id": "switch.test", None: None}, STATE_ON, "turn_off", {"entity_id": "switch.test", None: None}, STATE_OFF), ], ) -def test_build_command( +async def test_build_command( hass, is_inversed, vswitch_on_command, @@ -42,6 +121,7 @@ def test_build_command( expected_command_off, expected_data_off, expected_state_off, + is_ok, ): """Test the initialisation of a UnderlyingSwitch with some personnalisations commands""" @@ -49,7 +129,18 @@ def test_build_command( type(vtherm).is_inversed = PropertyMock(return_value=is_inversed) assert vtherm.is_inversed == is_inversed - under = UnderlyingSwitch(hass, vtherm, "switch.test", 0, 0, vswitch_on_command, vswitch_off_command) + + try: + under = UnderlyingSwitch(hass, vtherm, "switch.test", 0, 0, vswitch_on_command, vswitch_off_command) + except ValueError as e: + if is_ok: + pytest.fail(f"Initialization failed with ValueError: {e}") + else: + return + + if not is_ok: + pytest.fail("There should be a ValueError") + return assert under.is_inversed == is_inversed @@ -60,3 +151,18 @@ def test_build_command( assert under._off_command.get("command") == expected_command_off assert under._off_command.get("data") == expected_data_off assert under._off_command.get("state") == expected_state_off + + # Calling turn-on + # fmt: off + with patch.object(under, "check_overpowering", return_value=True), \ + patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + #fmt: on + await under.turn_on() + mock_service_call.assert_called_once_with("switch", expected_command_on, expected_data_on) + + # Calling turn-off + #fmt: off + with patch("homeassistant.core.ServiceRegistry.async_call") as mock_service_call: + #fmt: on + await under.turn_off() + mock_service_call.assert_called_once_with("switch", expected_command_off, expected_data_off)