Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.2.0 broke manual setting on v2 fans #110

Merged
merged 10 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/siku/api_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ async def _format_response(self, data: dict) -> dict:
"mode": data["operation_mode"],
"humidity": int(data["humidity_level"]),
"alarm": bool(data["alarm_status"] == NoYes.YES),
"filter_timer": int(data["timer_countdown"]),
"timer_countdown": int(data["timer_countdown"]),
"boost": bool(
data["boost_mode_after_sensor"] == NoYesYes.YES
or data["boost_mode_after_sensor"] == NoYesYes.YES2
Expand Down
122 changes: 83 additions & 39 deletions custom_components/siku/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import socket
from homeassistant.util.percentage import percentage_to_ranged_value

from .const import DIRECTION_ALTERNATING
from .const import DIRECTIONS
Expand Down Expand Up @@ -38,6 +39,7 @@
COMMAND_DEVICE_TYPE = "B9"
COMMAND_BOOST = "06"
COMMAND_MODE = "07"
COMMAND_TIMER_COUNTDOWN = "0B"
COMMAND_CURRENT_HUMIDITY = "25"
COMMAND_MANUAL_SPEED = "44"
COMMAND_FAN1RPM = "4A"
Expand Down Expand Up @@ -78,6 +80,11 @@
MODE_PARTY: PRESET_MODE_PARTY,
}

EMPTY_VALUE = "00"

SPEED_MANUAL_MIN = 0
SPEED_MANUAL_MAX = 255


class SikuV2Api:
"""Handle requests to the fan controller."""
Expand All @@ -95,9 +102,11 @@ async def status(self) -> dict:
COMMAND_DEVICE_TYPE,
COMMAND_ON_OFF,
COMMAND_SPEED,
COMMAND_MANUAL_SPEED,
COMMAND_DIRECTION,
COMMAND_BOOST,
COMMAND_MODE,
COMMAND_TIMER_COUNTDOWN,
COMMAND_CURRENT_HUMIDITY,
COMMAND_FAN1RPM,
COMMAND_FILTER_TIMER,
Expand Down Expand Up @@ -129,6 +138,13 @@ async def speed(self, speed: str) -> None:
await self._send_command(FUNC_READ_WRITE, cmd)
return await self.status()

async def speed_manual(self, speed: str) -> None:
"""Set manual fan speed."""
speed = percentage_to_ranged_value(SPEED_MANUAL_MIN, SPEED_MANUAL_MAX, speed)
cmd = f"{COMMAND_MANUAL_SPEED}{speed}".upper()
await self._send_command(FUNC_READ_WRITE, cmd)
return await self.status()

async def direction(self, direction: str) -> None:
"""Set fan direction."""
# if direction is in DIRECTIONS values translate it to the key value
Expand Down Expand Up @@ -156,8 +172,8 @@ async def party(self) -> None:

async def reset_filter_alarm(self) -> None:
"""Reset filter alarm."""
cmd = f"{COMMAND_RESET_ALARMS}".upper()
await self._send_command(FUNC_READ_WRITE, cmd)
cmd = f"{COMMAND_RESET_ALARMS}{EMPTY_VALUE}{COMMAND_RESET_FILTER_TIMER}{EMPTY_VALUE}".upper()
await self._send_command(FUNC_WRITE, cmd)
return await self.status()

def _checksum(self, data: str) -> str:
Expand Down Expand Up @@ -209,38 +225,47 @@ async def _send_command(self, func: str, data: str) -> list[str]:
# enter the data content of the UDP packet as hex
packet_str = self._build_packet(func, data)
packet_data = bytes.fromhex(packet_str)
LOGGER.debug("packet data: %s", packet_data)

# initialize a socket, think of it as a cable
# SOCK_DGRAM specifies that this is UDP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) as s:
s.settimeout(1)

server_address = (self.host, self.port)
LOGGER.debug(
'sending "%s" size(%s) to %s',
packet_data.hex(),
len(packet_data),
server_address,
)
s.sendto(packet_data, server_address)

# Receive response
result_data, server = s.recvfrom(256)
LOGGER.debug(
"receive data: %s size(%s) from %s",
result_data,
len(result_data),
server,
)
result_str = result_data.hex().upper()
LOGGER.debug("receive string: %s", result_str)

result_hexlist = ["".join(x) for x in zip(*[iter(result_str)] * 2)]
if not self._verify_checksum(result_hexlist):
raise Exception("Checksum error")
LOGGER.debug("returning hexlist %s", result_hexlist)
return result_hexlist

for attempt in range(3): # Retry up to 3 times
try:
# initialize a socket, think of it as a cable
# SOCK_DGRAM specifies that this is UDP
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) as s:
s.settimeout(1)
server_address = (self.host, self.port)
LOGGER.debug(
'sending "%s" size(%s) to %s',
packet_data.hex(),
len(packet_data),
server_address,
)
s.sendto(packet_data, server_address)

if func == FUNC_WRITE:
LOGGER.debug("write command, no response expected")
s.close()
return []

# Receive response
result_data, server = s.recvfrom(4096)
LOGGER.debug(
'received "%s" size(%s) from %s',
result_data,
len(result_data),
server,
)
result_str = result_data.hex().upper()
LOGGER.debug("receive string: %s", result_str)

result_hexlist = ["".join(x) for x in zip(*[iter(result_str)] * 2)]
if not self._verify_checksum(result_hexlist):
raise ValueError("Checksum error")
LOGGER.debug("returning hexlist %s", result_hexlist)
return result_hexlist
except TimeoutError:
LOGGER.warning("Timeout occurred, retrying... (%d/3)", attempt + 1)
if attempt == 2:
raise TimeoutError("Failed to send command after 3 attempts")

async def _translate_response(self, data: dict) -> dict:
"""Translate response data to dict."""
Expand All @@ -252,7 +277,11 @@ async def _translate_response(self, data: dict) -> dict:
try:
speed = f"{int(data[COMMAND_SPEED], 16):02}"
except KeyError:
speed = "00"
speed = "FF"
try:
manual_speed = f"{int(data[COMMAND_MANUAL_SPEED], 16):02}"
except KeyError:
manual_speed = "00"
try:
direction = DIRECTIONS[data[COMMAND_DIRECTION]]
oscillating = bool(direction == DIRECTION_ALTERNATING)
Expand All @@ -279,9 +308,9 @@ async def _translate_response(self, data: dict) -> dict:
# Byte 1: Minutes (0...59)
# Byte 2: Hours (0...23)
# Byte 3: Days (0...181)
minutes = int(data[COMMAND_FILTER_TIMER][0:2], 16)
days = int(data[COMMAND_FILTER_TIMER][0:2], 16)
hours = int(data[COMMAND_FILTER_TIMER][2:4], 16)
days = int(data[COMMAND_FILTER_TIMER][4:6], 16)
minutes = int(data[COMMAND_FILTER_TIMER][4:6], 16)
filter_timer = int(minutes + hours * 60 + days * 24 * 60)
except KeyError:
filter_timer = 0
Expand All @@ -298,17 +327,32 @@ async def _translate_response(self, data: dict) -> dict:
firmware = f"{int(data[COMMAND_READ_FIRMWARE_VERSION][0], 16)}.{int(data[COMMAND_READ_FIRMWARE_VERSION][1], 16)}"
except KeyError:
firmware = None
try:
# Byte 1 – seconds (0…59)
# Byte 2 – minutes (0…59)
# Byte 3 – hours (0…23)
hours = int(data[COMMAND_TIMER_COUNTDOWN][0:2], 16)
minutes = int(data[COMMAND_TIMER_COUNTDOWN][2:4], 16)
seconds = int(data[COMMAND_TIMER_COUNTDOWN][4:6], 16)
timer_countdown = int(seconds + minutes * 60 + hours * 60 * 60)
except KeyError:
timer_countdown = 0
return {
"is_on": is_on,
"speed": speed,
"speed_list": FAN_SPEEDS,
"manual_speed_selected": bool(speed == "FF"),
"manual_speed": int(manual_speed, 16),
"manual_speed_low_high_range": (SPEED_MANUAL_MIN, SPEED_MANUAL_MAX),
"oscillating": oscillating,
"direction": direction,
"boost": boost,
"mode": mode,
"humidity": humidity,
"rpm": rpm,
"firmware": firmware,
"filter_timer": filter_timer,
"filter_timer_days": filter_timer,
"timer_countdown": timer_countdown,
"alarm": alarm,
"version": "2",
}
Expand Down Expand Up @@ -397,7 +441,7 @@ async def _parse_response(self, hexlist: list[str]) -> dict:
)
i += 1
except KeyError as ex:
raise Exception(
raise ValueError(
f"Error translating response from fan controller: {str(ex)}"
) from ex
return data
2 changes: 1 addition & 1 deletion custom_components/siku/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ async def async_set_percentage(self, percentage: int) -> None:
self.coordinator.data["manual_speed_selected"]
and self.coordinator.data["manual_speed"]
):
await self.coordinator.api.speed(percentage)
await self.coordinator.api.speed_manual(percentage)
elif self.coordinator.data["speed_list"]:
await self.coordinator.api.speed(
percentage_to_ordered_list_item(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/siku/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"issue_tracker": "https://github.com/hmn/siku-integration/issues",
"requirements": [],
"ssdp": [],
"version": "2.2.0",
"version": "2.2.1",
"zeroconf": []
}
28 changes: 19 additions & 9 deletions custom_components/siku/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,44 +67,54 @@ class SikuSensorEntityDescription(SensorEntityDescription):
icon="mdi:alarm-light",
),
SikuSensorEntityDescription(
key="filter_timer",
key="filter_timer_days",
name="Filter timer countdown",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=2,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.MEASUREMENT,
),
SikuSensorEntityDescription(
key="timer_countdown",
name="Timer countdown",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
SikuSensorEntityDescription(
key="boost_mode_timer",
name="Boost mode timer",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=2,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.MEASUREMENT,
),
SikuSensorEntityDescription(
key="night_mode_timer",
name="Sleep mode timer",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=2,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.MEASUREMENT,
),
SikuSensorEntityDescription(
key="party_mode_timer",
name="Party mode timer",
icon="mdi:timer",
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_display_precision=2,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfTime.DAYS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.TOTAL,
state_class=SensorStateClass.MEASUREMENT,
),
SikuSensorEntityDescription(
key="boost",
Expand Down