|
1 | 1 | """Test the Z-Wave JS climate platform.""" |
| 2 | +import copy |
| 3 | + |
2 | 4 | import pytest |
3 | 5 | from zwave_js_server.const import CommandClass |
4 | 6 | from zwave_js_server.const.command_class.thermostat import ( |
|
37 | 39 | ATTR_ENTITY_ID, |
38 | 40 | ATTR_SUPPORTED_FEATURES, |
39 | 41 | ATTR_TEMPERATURE, |
| 42 | + SERVICE_TURN_OFF, |
| 43 | + SERVICE_TURN_ON, |
40 | 44 | ) |
41 | 45 | from homeassistant.core import HomeAssistant |
42 | 46 | from homeassistant.exceptions import ServiceValidationError |
@@ -89,6 +93,18 @@ async def test_thermostat_v2( |
89 | 93 |
|
90 | 94 | client.async_send_command.reset_mock() |
91 | 95 |
|
| 96 | + # Check that turning the device on is a no-op because it is already on |
| 97 | + await hass.services.async_call( |
| 98 | + CLIMATE_DOMAIN, |
| 99 | + SERVICE_TURN_ON, |
| 100 | + {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, |
| 101 | + blocking=True, |
| 102 | + ) |
| 103 | + |
| 104 | + assert len(client.async_send_command.call_args_list) == 0 |
| 105 | + |
| 106 | + client.async_send_command.reset_mock() |
| 107 | + |
92 | 108 | # Test setting hvac mode |
93 | 109 | await hass.services.async_call( |
94 | 110 | CLIMATE_DOMAIN, |
@@ -277,6 +293,68 @@ async def test_thermostat_v2( |
277 | 293 |
|
278 | 294 | client.async_send_command.reset_mock() |
279 | 295 |
|
| 296 | + # Test turning device off then on to see if the previous state is retained |
| 297 | + await hass.services.async_call( |
| 298 | + CLIMATE_DOMAIN, |
| 299 | + SERVICE_TURN_OFF, |
| 300 | + {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, |
| 301 | + blocking=True, |
| 302 | + ) |
| 303 | + |
| 304 | + assert len(client.async_send_command.call_args_list) == 1 |
| 305 | + args = client.async_send_command.call_args[0][0] |
| 306 | + assert args["command"] == "node.set_value" |
| 307 | + assert args["nodeId"] == 13 |
| 308 | + assert args["valueId"] == { |
| 309 | + "endpoint": 1, |
| 310 | + "commandClass": 64, |
| 311 | + "property": "mode", |
| 312 | + } |
| 313 | + assert args["value"] == 0 |
| 314 | + |
| 315 | + # Update state to off |
| 316 | + event = Event( |
| 317 | + type="value updated", |
| 318 | + data={ |
| 319 | + "source": "node", |
| 320 | + "event": "value updated", |
| 321 | + "nodeId": 13, |
| 322 | + "args": { |
| 323 | + "commandClassName": "Thermostat Mode", |
| 324 | + "commandClass": 64, |
| 325 | + "endpoint": 1, |
| 326 | + "property": "mode", |
| 327 | + "propertyName": "mode", |
| 328 | + "newValue": 0, |
| 329 | + "prevValue": 3, |
| 330 | + }, |
| 331 | + }, |
| 332 | + ) |
| 333 | + node.receive_event(event) |
| 334 | + |
| 335 | + client.async_send_command.reset_mock() |
| 336 | + |
| 337 | + # Test turning device on |
| 338 | + await hass.services.async_call( |
| 339 | + CLIMATE_DOMAIN, |
| 340 | + SERVICE_TURN_ON, |
| 341 | + {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, |
| 342 | + blocking=True, |
| 343 | + ) |
| 344 | + |
| 345 | + assert len(client.async_send_command.call_args_list) == 1 |
| 346 | + args = client.async_send_command.call_args[0][0] |
| 347 | + assert args["command"] == "node.set_value" |
| 348 | + assert args["nodeId"] == 13 |
| 349 | + assert args["valueId"] == { |
| 350 | + "endpoint": 1, |
| 351 | + "commandClass": 64, |
| 352 | + "property": "mode", |
| 353 | + } |
| 354 | + assert args["value"] == 3 |
| 355 | + |
| 356 | + client.async_send_command.reset_mock() |
| 357 | + |
280 | 358 | # Test setting invalid fan mode |
281 | 359 | with pytest.raises(ServiceValidationError): |
282 | 360 | await hass.services.async_call( |
@@ -304,6 +382,145 @@ async def test_thermostat_v2( |
304 | 382 | assert "Error while refreshing value" in caplog.text |
305 | 383 |
|
306 | 384 |
|
| 385 | +async def test_thermostat_v2_turn_on_after_off( |
| 386 | + hass: HomeAssistant, client, climate_radio_thermostat_ct100_plus, integration |
| 387 | +) -> None: |
| 388 | + """Test thermostat v2 command class entity that is turned on after starting off.""" |
| 389 | + node = climate_radio_thermostat_ct100_plus |
| 390 | + |
| 391 | + # Turn device off so we can test turning it back on to see if the turn on service |
| 392 | + # attempts to find a value to set |
| 393 | + event = Event( |
| 394 | + type="value updated", |
| 395 | + data={ |
| 396 | + "source": "node", |
| 397 | + "event": "value updated", |
| 398 | + "nodeId": 13, |
| 399 | + "args": { |
| 400 | + "commandClassName": "Thermostat Mode", |
| 401 | + "commandClass": 64, |
| 402 | + "endpoint": 1, |
| 403 | + "property": "mode", |
| 404 | + "propertyName": "mode", |
| 405 | + "newValue": 0, |
| 406 | + "prevValue": 1, |
| 407 | + }, |
| 408 | + }, |
| 409 | + ) |
| 410 | + node.receive_event(event) |
| 411 | + |
| 412 | + client.async_send_command.reset_mock() |
| 413 | + |
| 414 | + # Test turning device on |
| 415 | + await hass.services.async_call( |
| 416 | + CLIMATE_DOMAIN, |
| 417 | + SERVICE_TURN_ON, |
| 418 | + {ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY}, |
| 419 | + blocking=True, |
| 420 | + ) |
| 421 | + |
| 422 | + assert len(client.async_send_command.call_args_list) == 1 |
| 423 | + args = client.async_send_command.call_args[0][0] |
| 424 | + assert args["command"] == "node.set_value" |
| 425 | + assert args["nodeId"] == 13 |
| 426 | + assert args["valueId"] == { |
| 427 | + "endpoint": 1, |
| 428 | + "commandClass": 64, |
| 429 | + "property": "mode", |
| 430 | + } |
| 431 | + assert args["value"] == 3 |
| 432 | + |
| 433 | + client.async_send_command.reset_mock() |
| 434 | + |
| 435 | + |
| 436 | +async def test_thermostat_turn_on_after_off_no_heat_cool_auto( |
| 437 | + hass: HomeAssistant, client, aeotec_radiator_thermostat_state, integration |
| 438 | +) -> None: |
| 439 | + """Test thermostat that is turned on after starting off w/o heat, cool, or auto.""" |
| 440 | + node_state = copy.deepcopy(aeotec_radiator_thermostat_state) |
| 441 | + # Only allow off and dry modes so we can test fallback logic when turning HVAC on |
| 442 | + # without a last mode stored. |
| 443 | + value = next( |
| 444 | + value |
| 445 | + for value in node_state["values"] |
| 446 | + if value["commandClass"] == 64 and value["property"] == "mode" |
| 447 | + ) |
| 448 | + value["metadata"]["states"] = {"0": "Off", "6": "Fan", "8": "Dry"} |
| 449 | + value["value"] = 0 |
| 450 | + node = Node(client, node_state) |
| 451 | + client.driver.controller.emit("node added", {"node": node}) |
| 452 | + await hass.async_block_till_done() |
| 453 | + entity_id = "climate.thermostat_hvac" |
| 454 | + assert hass.states.get(entity_id).state == HVACMode.OFF |
| 455 | + |
| 456 | + client.async_send_command.reset_mock() |
| 457 | + |
| 458 | + # Test turning device on sets it to first available mode (Energy heat) |
| 459 | + await hass.services.async_call( |
| 460 | + CLIMATE_DOMAIN, |
| 461 | + SERVICE_TURN_ON, |
| 462 | + {ATTR_ENTITY_ID: entity_id}, |
| 463 | + blocking=True, |
| 464 | + ) |
| 465 | + |
| 466 | + assert len(client.async_send_command.call_args_list) == 1 |
| 467 | + args = client.async_send_command.call_args[0][0] |
| 468 | + assert args["command"] == "node.set_value" |
| 469 | + assert args["nodeId"] == 4 |
| 470 | + assert args["valueId"] == { |
| 471 | + "endpoint": 0, |
| 472 | + "commandClass": 64, |
| 473 | + "property": "mode", |
| 474 | + } |
| 475 | + assert args["value"] == 6 |
| 476 | + |
| 477 | + |
| 478 | +async def test_thermostat_turn_on_after_off_with_resume( |
| 479 | + hass: HomeAssistant, client, aeotec_radiator_thermostat_state, integration |
| 480 | +) -> None: |
| 481 | + """Test thermostat that is turned on after starting off with resume support.""" |
| 482 | + node_state = copy.deepcopy(aeotec_radiator_thermostat_state) |
| 483 | + # Add resume thermostat mode so we can test that it prefers the resume mode |
| 484 | + value = next( |
| 485 | + value |
| 486 | + for value in node_state["values"] |
| 487 | + if value["commandClass"] == 64 and value["property"] == "mode" |
| 488 | + ) |
| 489 | + value["metadata"]["states"] = { |
| 490 | + "0": "Off", |
| 491 | + "5": "Resume (on)", |
| 492 | + "6": "Fan", |
| 493 | + "8": "Dry", |
| 494 | + } |
| 495 | + value["value"] = 0 |
| 496 | + node = Node(client, node_state) |
| 497 | + client.driver.controller.emit("node added", {"node": node}) |
| 498 | + await hass.async_block_till_done() |
| 499 | + entity_id = "climate.thermostat_hvac" |
| 500 | + assert hass.states.get(entity_id).state == HVACMode.OFF |
| 501 | + |
| 502 | + client.async_send_command.reset_mock() |
| 503 | + |
| 504 | + # Test turning device on sends resume command |
| 505 | + await hass.services.async_call( |
| 506 | + CLIMATE_DOMAIN, |
| 507 | + SERVICE_TURN_ON, |
| 508 | + {ATTR_ENTITY_ID: entity_id}, |
| 509 | + blocking=True, |
| 510 | + ) |
| 511 | + |
| 512 | + assert len(client.async_send_command.call_args_list) == 1 |
| 513 | + args = client.async_send_command.call_args[0][0] |
| 514 | + assert args["command"] == "node.set_value" |
| 515 | + assert args["nodeId"] == 4 |
| 516 | + assert args["valueId"] == { |
| 517 | + "endpoint": 0, |
| 518 | + "commandClass": 64, |
| 519 | + "property": "mode", |
| 520 | + } |
| 521 | + assert args["value"] == 5 |
| 522 | + |
| 523 | + |
307 | 524 | async def test_thermostat_different_endpoints( |
308 | 525 | hass: HomeAssistant, |
309 | 526 | client, |
|
0 commit comments