|
11 | 11 | MODEL_FAN_P10 = "dmaker.fan.p10" |
12 | 12 | MODEL_FAN_P11 = "dmaker.fan.p11" |
13 | 13 | MODEL_FAN_1C = "dmaker.fan.1c" |
| 14 | +MODEL_FAN_ZA5 = "zhimi.fan.za5" |
14 | 15 |
|
15 | 16 | MIOT_MAPPING = { |
16 | 17 | MODEL_FAN_1C: { |
|
67 | 68 | "power_off_time": {"siid": 3, "piid": 1}, |
68 | 69 | "set_move": {"siid": 6, "piid": 1}, |
69 | 70 | }, |
| 71 | + MODEL_FAN_ZA5: { |
| 72 | + # https://miot-spec.org/miot-spec-v2/instance?type=urn:miot-spec-v2:device:fan:0000A005:zhimi-za5:1 |
| 73 | + "power": {"siid": 2, "piid": 1}, |
| 74 | + "fan_level": {"siid": 2, "piid": 2}, |
| 75 | + "swing_mode": {"siid": 2, "piid": 3}, |
| 76 | + "swing_mode_angle": {"siid": 2, "piid": 5}, |
| 77 | + "mode": {"siid": 2, "piid": 7}, |
| 78 | + "power_off_time": {"siid": 2, "piid": 10}, |
| 79 | + "anion": {"siid": 2, "piid": 11}, |
| 80 | + "child_lock": {"siid": 3, "piid": 1}, |
| 81 | + "light": {"siid": 4, "piid": 3}, |
| 82 | + "buzzer": {"siid": 5, "piid": 1}, |
| 83 | + "buttons_pressed": {"siid": 6, "piid": 1}, |
| 84 | + "battery_supported": {"siid": 6, "piid": 2}, |
| 85 | + "set_move": {"siid": 6, "piid": 3}, |
| 86 | + "speed_rpm": {"siid": 6, "piid": 4}, |
| 87 | + "powersupply_attached": {"siid": 6, "piid": 5}, |
| 88 | + "fan_speed": {"siid": 6, "piid": 8}, |
| 89 | + "humidity": {"siid": 7, "piid": 1}, |
| 90 | + "temperature": {"siid": 7, "piid": 7}, |
| 91 | + }, |
70 | 92 | } |
71 | 93 |
|
72 | 94 | SUPPORTED_ANGLES = { |
73 | 95 | MODEL_FAN_P9: [30, 60, 90, 120, 150], |
74 | 96 | MODEL_FAN_P10: [30, 60, 90, 120, 140], |
75 | 97 | MODEL_FAN_P11: [30, 60, 90, 120, 140], |
| 98 | + MODEL_FAN_ZA5: [30, 60, 90, 120], |
76 | 99 | } |
77 | 100 |
|
78 | 101 |
|
@@ -164,8 +187,7 @@ class FanStatus1C(DeviceStatus): |
164 | 187 |
|
165 | 188 | def __init__(self, data: Dict[str, Any]) -> None: |
166 | 189 | self.data = data |
167 | | - """ |
168 | | - Response of a Fan1C (dmaker.fan.1c): |
| 190 | + """Response of a Fan1C (dmaker.fan.1c): |
169 | 191 |
|
170 | 192 | { |
171 | 193 | 'id': 1, |
@@ -525,3 +547,312 @@ def delay_off(self, minutes: int): |
525 | 547 | raise FanException("Invalid value for a delayed turn off: %s" % minutes) |
526 | 548 |
|
527 | 549 | return self.set_property("power_off_time", minutes) |
| 550 | + |
| 551 | + |
| 552 | +class OperationModeFanZA5(enum.Enum): |
| 553 | + Nature = 0 |
| 554 | + Normal = 1 |
| 555 | + |
| 556 | + |
| 557 | +class FanStatusZA5(DeviceStatus): |
| 558 | + """Container for status reports for FanZA5.""" |
| 559 | + |
| 560 | + def __init__(self, data: Dict[str, Any]) -> None: |
| 561 | + """Response of FanZA5 (zhimi.fan.za5): |
| 562 | +
|
| 563 | + {'code': -4005, 'did': 'set_move', 'piid': 3, 'siid': 6}, |
| 564 | + {'code': 0, 'did': 'anion', 'piid': 11, 'siid': 2, 'value': True}, |
| 565 | + {'code': 0, 'did': 'battery_supported', 'piid': 2, 'siid': 6, 'value': False}, |
| 566 | + {'code': 0, 'did': 'buttons_pressed', 'piid': 1, 'siid': 6, 'value': 0}, |
| 567 | + {'code': 0, 'did': 'buzzer', 'piid': 1, 'siid': 5, 'value': False}, |
| 568 | + {'code': 0, 'did': 'child_lock', 'piid': 1, 'siid': 3, 'value': False}, |
| 569 | + {'code': 0, 'did': 'fan_level', 'piid': 2, 'siid': 2, 'value': 4}, |
| 570 | + {'code': 0, 'did': 'fan_speed', 'piid': 8, 'siid': 6, 'value': 100}, |
| 571 | + {'code': 0, 'did': 'humidity', 'piid': 1, 'siid': 7, 'value': 55}, |
| 572 | + {'code': 0, 'did': 'light', 'piid': 3, 'siid': 4, 'value': 100}, |
| 573 | + {'code': 0, 'did': 'mode', 'piid': 7, 'siid': 2, 'value': 0}, |
| 574 | + {'code': 0, 'did': 'power', 'piid': 1, 'siid': 2, 'value': False}, |
| 575 | + {'code': 0, 'did': 'power_off_time', 'piid': 10, 'siid': 2, 'value': 0}, |
| 576 | + {'code': 0, 'did': 'powersupply_attached', 'piid': 5, 'siid': 6, 'value': True}, |
| 577 | + {'code': 0, 'did': 'speed_rpm', 'piid': 4, 'siid': 6, 'value': 0}, |
| 578 | + {'code': 0, 'did': 'swing_mode', 'piid': 3, 'siid': 2, 'value': True}, |
| 579 | + {'code': 0, 'did': 'swing_mode_angle', 'piid': 5, 'siid': 2, 'value': 60}, |
| 580 | + {'code': 0, 'did': 'temperature', 'piid': 7, 'siid': 7, 'value': 26.4}, |
| 581 | + """ |
| 582 | + self.data = data |
| 583 | + |
| 584 | + @property |
| 585 | + def ionizer(self) -> str: |
| 586 | + """Ionizer state.""" |
| 587 | + return "on" if self.data["anion"] else "off" |
| 588 | + |
| 589 | + @property |
| 590 | + def is_ionizer_enabled(self) -> bool: |
| 591 | + """True if negative ions generation is enabled.""" |
| 592 | + return self.data["anion"] |
| 593 | + |
| 594 | + @property |
| 595 | + def battery_supported(self) -> bool: |
| 596 | + """True if battery is supported.""" |
| 597 | + return self.data["battery_supported"] |
| 598 | + |
| 599 | + @property |
| 600 | + def buttons_pressed(self) -> str: |
| 601 | + """What buttons on the fan are pressed now.""" |
| 602 | + code = self.data["buttons_pressed"] |
| 603 | + if code == 0: |
| 604 | + return "None" |
| 605 | + if code == 1: |
| 606 | + return "Power" |
| 607 | + if code == 2: |
| 608 | + return "Swing" |
| 609 | + return "Unknown" |
| 610 | + |
| 611 | + @property |
| 612 | + def buzzer(self) -> bool: |
| 613 | + """True if buzzer is turned on.""" |
| 614 | + return self.data["buzzer"] |
| 615 | + |
| 616 | + @property |
| 617 | + def child_lock(self) -> bool: |
| 618 | + """True if child lock if on.""" |
| 619 | + return self.data["child_lock"] |
| 620 | + |
| 621 | + @property |
| 622 | + def fan_level(self) -> int: |
| 623 | + """Fan level (1-4).""" |
| 624 | + return self.data["fan_level"] |
| 625 | + |
| 626 | + @property |
| 627 | + def fan_speed(self) -> int: |
| 628 | + """Fan speed (1-100).""" |
| 629 | + return self.data["fan_speed"] |
| 630 | + |
| 631 | + @property |
| 632 | + def humidity(self) -> int: |
| 633 | + """Air humidity in percent.""" |
| 634 | + return self.data["humidity"] |
| 635 | + |
| 636 | + @property |
| 637 | + def light(self) -> int: |
| 638 | + """LED brightness (1-100).""" |
| 639 | + return self.data["light"] |
| 640 | + |
| 641 | + @property |
| 642 | + def mode(self) -> OperationMode: |
| 643 | + """Operation mode (normal or nature).""" |
| 644 | + return OperationMode[OperationModeFanZA5(self.data["mode"]).name] |
| 645 | + |
| 646 | + @property |
| 647 | + def power(self) -> str: |
| 648 | + """Power state.""" |
| 649 | + return "on" if self.data["power"] else "off" |
| 650 | + |
| 651 | + @property |
| 652 | + def is_on(self) -> bool: |
| 653 | + """True if device is currently on.""" |
| 654 | + return self.data["power"] |
| 655 | + |
| 656 | + @property |
| 657 | + def delay_off_countdown(self) -> int: |
| 658 | + """Countdown until turning off in minutes.""" |
| 659 | + return self.data["power_off_time"] |
| 660 | + |
| 661 | + @property |
| 662 | + def powersupply_attached(self) -> bool: |
| 663 | + """True is power supply is attached.""" |
| 664 | + return self.data["powersupply_attached"] |
| 665 | + |
| 666 | + @property |
| 667 | + def speed_rpm(self) -> int: |
| 668 | + """Fan rotations per minute.""" |
| 669 | + return self.data["speed_rpm"] |
| 670 | + |
| 671 | + @property |
| 672 | + def oscillate(self) -> bool: |
| 673 | + """True if oscillation is enabled.""" |
| 674 | + return self.data["swing_mode"] |
| 675 | + |
| 676 | + @property |
| 677 | + def angle(self) -> int: |
| 678 | + """Oscillation angle.""" |
| 679 | + return self.data["swing_mode_angle"] |
| 680 | + |
| 681 | + @property |
| 682 | + def temperature(self) -> Any: |
| 683 | + """Air temperature (degree celsius).""" |
| 684 | + return self.data["temperature"] |
| 685 | + |
| 686 | + |
| 687 | +class FanZA5(MiotDevice): |
| 688 | + mapping = MIOT_MAPPING[MODEL_FAN_ZA5] |
| 689 | + |
| 690 | + def __init__( |
| 691 | + self, |
| 692 | + ip: str = None, |
| 693 | + token: str = None, |
| 694 | + start_id: int = 0, |
| 695 | + debug: int = 0, |
| 696 | + lazy_discover: bool = True, |
| 697 | + model: str = MODEL_FAN_ZA5, |
| 698 | + ) -> None: |
| 699 | + super().__init__(ip, token, start_id, debug, lazy_discover) |
| 700 | + self.model = model |
| 701 | + |
| 702 | + @command( |
| 703 | + default_output=format_output( |
| 704 | + "", |
| 705 | + "Angle: {result.angle}\n" |
| 706 | + "Battery Supported: {result.battery_supported}\n" |
| 707 | + "Buttons Pressed: {result.buttons_pressed}\n" |
| 708 | + "Buzzer: {result.buzzer}\n" |
| 709 | + "Child Lock: {result.child_lock}\n" |
| 710 | + "Delay Off Countdown: {result.delay_off_countdown}\n" |
| 711 | + "Fan Level: {result.fan_level}\n" |
| 712 | + "Fan Speed: {result.fan_speed}\n" |
| 713 | + "Humidity: {result.humidity}\n" |
| 714 | + "Ionizer: {result.ionizer}\n" |
| 715 | + "Light: {result.light}\n" |
| 716 | + "Mode: {result.mode.name}\n" |
| 717 | + "Oscillate: {result.oscillate}\n" |
| 718 | + "Power: {result.power}\n" |
| 719 | + "Powersupply Attached: {result.powersupply_attached}\n" |
| 720 | + "Speed RPM: {result.speed_rpm}\n" |
| 721 | + "Temperature: {result.temperature}\n", |
| 722 | + ) |
| 723 | + ) |
| 724 | + def status(self): |
| 725 | + """Retrieve properties.""" |
| 726 | + return FanStatusZA5( |
| 727 | + { |
| 728 | + prop["did"]: prop["value"] if prop["code"] == 0 else None |
| 729 | + for prop in self.get_properties_for_mapping() |
| 730 | + } |
| 731 | + ) |
| 732 | + |
| 733 | + @command(default_output=format_output("Powering on")) |
| 734 | + def on(self): |
| 735 | + """Power on.""" |
| 736 | + return self.set_property("power", True) |
| 737 | + |
| 738 | + @command(default_output=format_output("Powering off")) |
| 739 | + def off(self): |
| 740 | + """Power off.""" |
| 741 | + return self.set_property("power", False) |
| 742 | + |
| 743 | + @command(default_output=format_output("Turning ionizer on")) |
| 744 | + def ionizer_on(self): |
| 745 | + """Turn ionizer on.""" |
| 746 | + return self.set_property("anion", True) |
| 747 | + |
| 748 | + @command(default_output=format_output("Turning ionizer off")) |
| 749 | + def ionizer_off(self): |
| 750 | + """Turn ionizer off.""" |
| 751 | + return self.set_property("anion", False) |
| 752 | + |
| 753 | + @command( |
| 754 | + click.argument("speed", type=int), |
| 755 | + default_output=format_output("Setting speed to {speed}%"), |
| 756 | + ) |
| 757 | + def set_speed(self, speed: int): |
| 758 | + """Set fan speed.""" |
| 759 | + if speed < 1 or speed > 100: |
| 760 | + raise FanException("Invalid speed: %s" % speed) |
| 761 | + |
| 762 | + return self.set_property("fan_speed", speed) |
| 763 | + |
| 764 | + @command( |
| 765 | + click.argument("angle", type=int), |
| 766 | + default_output=format_output("Setting angle to {angle}"), |
| 767 | + ) |
| 768 | + def set_angle(self, angle: int): |
| 769 | + """Set the oscillation angle.""" |
| 770 | + if angle not in SUPPORTED_ANGLES[self.model]: |
| 771 | + raise FanException( |
| 772 | + "Unsupported angle. Supported values: " |
| 773 | + + ", ".join("{0}".format(i) for i in SUPPORTED_ANGLES[self.model]) |
| 774 | + ) |
| 775 | + |
| 776 | + return self.set_property("swing_mode_angle", angle) |
| 777 | + |
| 778 | + @command( |
| 779 | + click.argument("oscillate", type=bool), |
| 780 | + default_output=format_output( |
| 781 | + lambda oscillate: "Turning on oscillate" |
| 782 | + if oscillate |
| 783 | + else "Turning off oscillate" |
| 784 | + ), |
| 785 | + ) |
| 786 | + def set_oscillate(self, oscillate: bool): |
| 787 | + """Set oscillate on/off.""" |
| 788 | + if oscillate: |
| 789 | + return self.set_property("swing_mode", True) |
| 790 | + else: |
| 791 | + return self.set_property("swing_mode", False) |
| 792 | + |
| 793 | + @command( |
| 794 | + click.argument("buzzer", type=bool), |
| 795 | + default_output=format_output( |
| 796 | + lambda buzzer: "Turning on buzzer" if buzzer else "Turning off buzzer" |
| 797 | + ), |
| 798 | + ) |
| 799 | + def set_buzzer(self, buzzer: bool): |
| 800 | + """Set buzzer on/off.""" |
| 801 | + if buzzer: |
| 802 | + return self.set_property("buzzer", True) |
| 803 | + else: |
| 804 | + return self.set_property("buzzer", False) |
| 805 | + |
| 806 | + @command( |
| 807 | + click.argument("lock", type=bool), |
| 808 | + default_output=format_output( |
| 809 | + lambda lock: "Turning on child lock" if lock else "Turning off child lock" |
| 810 | + ), |
| 811 | + ) |
| 812 | + def set_child_lock(self, lock: bool): |
| 813 | + """Set child lock on/off.""" |
| 814 | + return self.set_property("child_lock", lock) |
| 815 | + |
| 816 | + @command( |
| 817 | + click.argument("light", type=int), |
| 818 | + default_output=format_output("Setting light to {light}%"), |
| 819 | + ) |
| 820 | + def set_light(self, light: int): |
| 821 | + """Set indicator brightness.""" |
| 822 | + if light < 0 or light > 100: |
| 823 | + raise FanException("Invalid light: %s" % light) |
| 824 | + |
| 825 | + return self.set_property("light", light) |
| 826 | + |
| 827 | + @command( |
| 828 | + click.argument("mode", type=EnumType(OperationMode)), |
| 829 | + default_output=format_output("Setting mode to '{mode.value}'"), |
| 830 | + ) |
| 831 | + def set_mode(self, mode: OperationMode): |
| 832 | + """Set mode.""" |
| 833 | + return self.set_property("mode", OperationModeFanZA5[mode.name].value) |
| 834 | + |
| 835 | + @command( |
| 836 | + click.argument("seconds", type=int), |
| 837 | + default_output=format_output("Setting delayed turn off to {seconds} seconds"), |
| 838 | + ) |
| 839 | + def delay_off(self, seconds: int): |
| 840 | + """Set delay off seconds.""" |
| 841 | + |
| 842 | + if seconds < 0 or seconds > 10 * 60 * 60: |
| 843 | + raise FanException("Invalid value for a delayed turn off: %s" % seconds) |
| 844 | + |
| 845 | + return self.set_property("power_off_time", seconds) |
| 846 | + |
| 847 | + @command( |
| 848 | + click.argument("direction", type=EnumType(MoveDirection)), |
| 849 | + default_output=format_output("Rotating the fan to the {direction}"), |
| 850 | + ) |
| 851 | + def set_rotate(self, direction: MoveDirection): |
| 852 | + """Rotate fan 7.5 degrees horizontally to given direction.""" |
| 853 | + status = self.status() |
| 854 | + if status.oscillate: |
| 855 | + raise FanException( |
| 856 | + "Rotation requires oscillation to be turned off to function." |
| 857 | + ) |
| 858 | + return self.set_property("set_move", direction.name.lower()) |
0 commit comments