|
8 | 8 |
|
9 | 9 | from homeassistant.core import HomeAssistant |
10 | 10 | from homeassistant.exceptions import TemplateError |
| 11 | +from homeassistant.helpers.template.extensions import MathExtension |
11 | 12 |
|
12 | 13 | from tests.helpers.template.helpers import render |
13 | 14 |
|
@@ -340,3 +341,206 @@ def test_min_max_attribute(hass: HomeAssistant, attribute) -> None: |
340 | 341 | ) |
341 | 342 | == 3 |
342 | 343 | ) |
| 344 | + |
| 345 | + |
| 346 | +def test_clamp(hass: HomeAssistant) -> None: |
| 347 | + """Test clamp function.""" |
| 348 | + # Test function and filter usage in templates. |
| 349 | + assert render(hass, "{{ clamp(15, 0, 10) }}") == 10.0 |
| 350 | + assert render(hass, "{{ -5 | clamp(0, 10) }}") == 0.0 |
| 351 | + |
| 352 | + # Test basic clamping behavior |
| 353 | + assert MathExtension.clamp(5, 0, 10) == 5.0 |
| 354 | + assert MathExtension.clamp(-5, 0, 10) == 0.0 |
| 355 | + assert MathExtension.clamp(15, 0, 10) == 10.0 |
| 356 | + assert MathExtension.clamp(0, 0, 10) == 0.0 |
| 357 | + assert MathExtension.clamp(10, 0, 10) == 10.0 |
| 358 | + |
| 359 | + # Test with float values |
| 360 | + assert MathExtension.clamp(5.5, 0, 10) == 5.5 |
| 361 | + assert MathExtension.clamp(5.5, 0.5, 10.5) == 5.5 |
| 362 | + assert MathExtension.clamp(0.25, 0.5, 10.5) == 0.5 |
| 363 | + assert MathExtension.clamp(11.0, 0.5, 10.5) == 10.5 |
| 364 | + |
| 365 | + # Test with negative ranges |
| 366 | + assert MathExtension.clamp(-5, -10, -1) == -5.0 |
| 367 | + assert MathExtension.clamp(-15, -10, -1) == -10.0 |
| 368 | + assert MathExtension.clamp(0, -10, -1) == -1.0 |
| 369 | + |
| 370 | + # Test with non-range |
| 371 | + assert MathExtension.clamp(5, 10, 10) == 10.0 |
| 372 | + |
| 373 | + # Test error handling - invalid input types |
| 374 | + for case in ( |
| 375 | + "{{ clamp('invalid', 0, 10) }}", |
| 376 | + "{{ clamp(5, 'invalid', 10) }}", |
| 377 | + "{{ clamp(5, 0, 'invalid') }}", |
| 378 | + ): |
| 379 | + with pytest.raises(TemplateError): |
| 380 | + render(hass, case) |
| 381 | + |
| 382 | + |
| 383 | +def test_wrap(hass: HomeAssistant) -> None: |
| 384 | + """Test wrap function.""" |
| 385 | + # Test function and filter usage in templates. |
| 386 | + assert render(hass, "{{ wrap(15, 0, 10) }}") == 5.0 |
| 387 | + assert render(hass, "{{ -5 | wrap(0, 10) }}") == 5.0 |
| 388 | + |
| 389 | + # Test basic wrapping behavior |
| 390 | + assert MathExtension.wrap(5, 0, 10) == 5.0 |
| 391 | + assert MathExtension.wrap(10, 0, 10) == 0.0 # max wraps to min |
| 392 | + assert MathExtension.wrap(15, 0, 10) == 5.0 |
| 393 | + assert MathExtension.wrap(25, 0, 10) == 5.0 |
| 394 | + assert MathExtension.wrap(-5, 0, 10) == 5.0 |
| 395 | + assert MathExtension.wrap(-10, 0, 10) == 0.0 |
| 396 | + |
| 397 | + # Test angle wrapping (common use case) |
| 398 | + assert MathExtension.wrap(370, 0, 360) == 10.0 |
| 399 | + assert MathExtension.wrap(-10, 0, 360) == 350.0 |
| 400 | + assert MathExtension.wrap(720, 0, 360) == 0.0 |
| 401 | + assert MathExtension.wrap(361, 0, 360) == 1.0 |
| 402 | + |
| 403 | + # Test with float values |
| 404 | + assert MathExtension.wrap(10.5, 0, 10) == 0.5 |
| 405 | + assert MathExtension.wrap(370.5, 0, 360) == 10.5 |
| 406 | + |
| 407 | + # Test with negative ranges |
| 408 | + assert MathExtension.wrap(-15, -10, 0) == -5.0 |
| 409 | + assert MathExtension.wrap(5, -10, 0) == -5.0 |
| 410 | + |
| 411 | + # Test with arbitrary ranges |
| 412 | + assert MathExtension.wrap(25, 10, 20) == 15.0 |
| 413 | + assert MathExtension.wrap(5, 10, 20) == 15.0 |
| 414 | + |
| 415 | + # Test with non-range |
| 416 | + assert MathExtension.wrap(5, 10, 10) == 10.0 |
| 417 | + |
| 418 | + # Test error handling - invalid input types |
| 419 | + for case in ( |
| 420 | + "{{ wrap('invalid', 0, 10) }}", |
| 421 | + "{{ wrap(5, 'invalid', 10) }}", |
| 422 | + "{{ wrap(5, 0, 'invalid') }}", |
| 423 | + ): |
| 424 | + with pytest.raises(TemplateError): |
| 425 | + render(hass, case) |
| 426 | + |
| 427 | + |
| 428 | +def test_remap(hass: HomeAssistant) -> None: |
| 429 | + """Test remap function.""" |
| 430 | + # Test function and filter usage in templates, with kitchen sink parameters. |
| 431 | + # We don't check the return value; that's covered by the unit tests below. |
| 432 | + assert render(hass, "{{ remap(5, 0, 6, 0, 740, steps=10) }}") |
| 433 | + assert render(hass, "{{ 50 | remap(0, 100, 0, 10, steps=8) }}") |
| 434 | + |
| 435 | + # Test basic remapping - scale from 0-10 to 0-100 |
| 436 | + assert MathExtension.remap(0, 0, 10, 0, 100) == 0.0 |
| 437 | + assert MathExtension.remap(5, 0, 10, 0, 100) == 50.0 |
| 438 | + assert MathExtension.remap(10, 0, 10, 0, 100) == 100.0 |
| 439 | + |
| 440 | + # Test with different input and output ranges |
| 441 | + assert MathExtension.remap(50, 0, 100, 0, 10) == 5.0 |
| 442 | + assert MathExtension.remap(25, 0, 100, 0, 10) == 2.5 |
| 443 | + |
| 444 | + # Test with negative ranges |
| 445 | + assert MathExtension.remap(0, -10, 10, 0, 100) == 50.0 |
| 446 | + assert MathExtension.remap(-10, -10, 10, 0, 100) == 0.0 |
| 447 | + assert MathExtension.remap(10, -10, 10, 0, 100) == 100.0 |
| 448 | + |
| 449 | + # Test inverted output range |
| 450 | + assert MathExtension.remap(0, 0, 10, 100, 0) == 100.0 |
| 451 | + assert MathExtension.remap(5, 0, 10, 100, 0) == 50.0 |
| 452 | + assert MathExtension.remap(10, 0, 10, 100, 0) == 0.0 |
| 453 | + |
| 454 | + # Test values outside input range, and edge modes |
| 455 | + assert MathExtension.remap(15, 0, 10, 0, 100, edges="none") == 150.0 |
| 456 | + assert MathExtension.remap(-4, 0, 10, 0, 100, edges="none") == -40.0 |
| 457 | + assert MathExtension.remap(15, 0, 10, 0, 80, edges="clamp") == 80.0 |
| 458 | + assert MathExtension.remap(-5, 0, 10, -1, 1, edges="clamp") == -1 |
| 459 | + assert MathExtension.remap(15, 0, 10, 0, 100, edges="wrap") == 50.0 |
| 460 | + assert MathExtension.remap(-5, 0, 10, 0, 100, edges="wrap") == 50.0 |
| 461 | + |
| 462 | + # Test sensor conversion use case: Celsius to Fahrenheit: 0-100°C to 32-212°F |
| 463 | + assert MathExtension.remap(0, 0, 100, 32, 212) == 32.0 |
| 464 | + assert MathExtension.remap(100, 0, 100, 32, 212) == 212.0 |
| 465 | + assert MathExtension.remap(50, 0, 100, 32, 212) == 122.0 |
| 466 | + |
| 467 | + # Test time conversion use case: 0-60 minutes to 0-360 degrees, with wrap |
| 468 | + assert MathExtension.remap(80, 0, 60, 0, 360, edges="wrap") == 120.0 |
| 469 | + |
| 470 | + # Test percentage to byte conversion (0-100% to 0-255) |
| 471 | + assert MathExtension.remap(0, 0, 100, 0, 255) == 0.0 |
| 472 | + assert MathExtension.remap(50, 0, 100, 0, 255) == 127.5 |
| 473 | + assert MathExtension.remap(100, 0, 100, 0, 255) == 255.0 |
| 474 | + |
| 475 | + # Test with float precision |
| 476 | + assert MathExtension.remap(2.5, 0, 10, 0, 100) == 25.0 |
| 477 | + assert MathExtension.remap(7.5, 0, 10, 0, 100) == 75.0 |
| 478 | + |
| 479 | + # Test error handling |
| 480 | + for case in ( |
| 481 | + "{{ remap(5, 10, 10, 0, 100) }}", |
| 482 | + "{{ remap('invalid', 0, 10, 0, 100) }}", |
| 483 | + "{{ remap(5, 'invalid', 10, 0, 100) }}", |
| 484 | + "{{ remap(5, 0, 'invalid', 0, 100) }}", |
| 485 | + "{{ remap(5, 0, 10, 'invalid', 100) }}", |
| 486 | + "{{ remap(5, 0, 10, 0, 'invalid') }}", |
| 487 | + ): |
| 488 | + with pytest.raises(TemplateError): |
| 489 | + render(hass, case) |
| 490 | + |
| 491 | + |
| 492 | +def test_remap_with_steps(hass: HomeAssistant) -> None: |
| 493 | + """Test remap function with steps parameter.""" |
| 494 | + # Test basic stepping - quantize to 10 steps |
| 495 | + assert MathExtension.remap(0.2, 0, 10, 0, 100, steps=10) == 0.0 |
| 496 | + assert MathExtension.remap(5.3, 0, 10, 0, 100, steps=10) == 50.0 |
| 497 | + assert MathExtension.remap(10, 0, 10, 0, 100, steps=10) == 100.0 |
| 498 | + |
| 499 | + # Test stepping with intermediate values - should snap to nearest step |
| 500 | + # With 10 steps, normalized values are rounded: 0.0, 0.1, 0.2, ..., 1.0 |
| 501 | + assert MathExtension.remap(2.4, 0, 10, 0, 100, steps=10) == 20.0 |
| 502 | + assert MathExtension.remap(2.5, 0, 10, 0, 100, steps=10) == 20.0 |
| 503 | + assert MathExtension.remap(2.6, 0, 10, 0, 100, steps=10) == 30.0 |
| 504 | + |
| 505 | + # Test with 4 steps (0%, 25%, 50%, 75%, 100%) |
| 506 | + assert MathExtension.remap(0, 0, 10, 0, 100, steps=4) == 0.0 |
| 507 | + assert MathExtension.remap(2.5, 0, 10, 0, 100, steps=4) == 25.0 |
| 508 | + assert MathExtension.remap(5, 0, 10, 0, 100, steps=4) == 50.0 |
| 509 | + assert MathExtension.remap(7.5, 0, 10, 0, 100, steps=4) == 75.0 |
| 510 | + assert MathExtension.remap(10, 0, 10, 0, 100, steps=4) == 100.0 |
| 511 | + |
| 512 | + # Test with 2 steps (0%, 50%, 100%) |
| 513 | + assert MathExtension.remap(2, 0, 10, 0, 100, steps=2) == 0.0 |
| 514 | + assert MathExtension.remap(6, 0, 10, 0, 100, steps=2) == 50.0 |
| 515 | + assert MathExtension.remap(8, 0, 10, 0, 100, steps=2) == 100.0 |
| 516 | + |
| 517 | + # Test with 1 step (0%, 100%) |
| 518 | + assert MathExtension.remap(0, 0, 10, 0, 100, steps=1) == 0.0 |
| 519 | + assert MathExtension.remap(5, 0, 10, 0, 100, steps=1) == 0.0 |
| 520 | + assert MathExtension.remap(6, 0, 10, 0, 100, steps=1) == 100.0 |
| 521 | + assert MathExtension.remap(10, 0, 10, 0, 100, steps=1) == 100.0 |
| 522 | + |
| 523 | + # Test with inverted output range and steps |
| 524 | + assert MathExtension.remap(4.8, 0, 10, 100, 0, steps=4) == 50.0 |
| 525 | + |
| 526 | + # Test with 0 or negative steps (should be ignored/no quantization) |
| 527 | + assert MathExtension.remap(5, 0, 10, 0, 100, steps=0) == 50.0 |
| 528 | + assert MathExtension.remap(2.7, 0, 10, 0, 100, steps=0) == 27.0 |
| 529 | + assert MathExtension.remap(5, 0, 10, 0, 100, steps=-1) == 50.0 |
| 530 | + |
| 531 | + |
| 532 | +def test_remap_with_mirror(hass: HomeAssistant) -> None: |
| 533 | + """Test the mirror edge mode of the remap function.""" |
| 534 | + |
| 535 | + assert [ |
| 536 | + MathExtension.remap(i, 0, 4, 0, 1, edges="mirror") for i in range(-4, 9) |
| 537 | + ] == [1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0] |
| 538 | + |
| 539 | + # Test with different output range |
| 540 | + assert MathExtension.remap(15, 0, 10, 50, 150, edges="mirror") == 100.0 |
| 541 | + assert MathExtension.remap(25, 0, 10, 50, 150, edges="mirror") == 100.0 |
| 542 | + # Test with inverted output range |
| 543 | + assert MathExtension.remap(15, 0, 10, 100, 0, edges="mirror") == 50.0 |
| 544 | + assert MathExtension.remap(12, 0, 10, 100, 0, edges="mirror") == 20.0 |
| 545 | + # Test without remapping |
| 546 | + assert MathExtension.remap(-0.1, 0, 1, 0, 1, edges="mirror") == pytest.approx(0.1) |
0 commit comments