From 3b820bcccd82b395f00eb4192518166c8d6fb057 Mon Sep 17 00:00:00 2001 From: lunDreame <87955512+lunDreame@users.noreply.github.com> Date: Sat, 18 Jan 2025 23:41:44 +0900 Subject: [PATCH] Minor Update --- custom_components/kocom_wallpad/connection.py | 39 +++++++++++++------ custom_components/kocom_wallpad/const.py | 2 +- custom_components/kocom_wallpad/entity.py | 4 +- custom_components/kocom_wallpad/gateway.py | 6 +-- custom_components/kocom_wallpad/light.py | 32 ++++++++++----- custom_components/kocom_wallpad/manifest.json | 4 +- .../kocom_wallpad/pywallpad/client.py | 32 +++++++-------- .../kocom_wallpad/pywallpad/packet.py | 17 ++++---- .../kocom_wallpad/translations/en.json | 3 ++ .../kocom_wallpad/translations/ko.json | 3 ++ 10 files changed, 90 insertions(+), 52 deletions(-) diff --git a/custom_components/kocom_wallpad/connection.py b/custom_components/kocom_wallpad/connection.py index a942e20..546a341 100644 --- a/custom_components/kocom_wallpad/connection.py +++ b/custom_components/kocom_wallpad/connection.py @@ -1,15 +1,18 @@ from __future__ import annotations from typing import Optional + import asyncio +import re +import serial_asyncio from .const import LOGGER class RS485Connection: - """Connection class for RS485 communication with read lock protection.""" + """Connection class for RS485 communication with IP or serial support.""" - def __init__(self, host: str, port: int): + def __init__(self, host: str, port: Optional[int] = None): """Initialize the connection.""" self.host = host self.port = port @@ -19,17 +22,31 @@ def __init__(self, host: str, port: int): self.reconnect_interval = 5 self._running = True + def is_ip_address(self) -> bool: + """Check if the host is an IP address.""" + ip_pattern = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$") + return bool(ip_pattern.match(self.host)) + async def connect(self) -> bool: - """Connect to the device.""" + """Connect to the device using IP or serial.""" try: - self.reader, self.writer = await asyncio.open_connection( - self.host, self.port - ) + if self.is_ip_address(): + if not self.port: + raise ValueError("Port must be provided for IP connections.") + self.reader, self.writer = await asyncio.open_connection( + self.host, self.port + ) + LOGGER.info(f"Connected to {self.host}:{self.port}") + else: + self.reader, self.writer = await serial_asyncio.open_serial_connection( + url=self.host, baudrate=9600 + ) + LOGGER.info(f"Connected to serial port {self.host}") + self.is_connected = True - LOGGER.info(f"Connected to {self.host}:{self.port}") return True except Exception as e: - LOGGER.error(f"Socket connection failed: {e}") + LOGGER.error(f"Connection failed: {e}") self.is_connected = False return False @@ -71,10 +88,10 @@ async def send(self, packet: bytearray) -> bool: return False async def receive(self) -> Optional[bytes]: - """Receive data from the device with read lock protection.""" + """Receive data from the device.""" if not self.is_connected or not self.reader: return None - + try: data = await self.reader.read(1024) if not data: @@ -92,7 +109,7 @@ async def receive(self) -> Optional[bytes]: return None -async def test_connection(host: str, port: int, timeout: int = 5) -> bool: +async def test_connection(host: str, port: Optional[int] = None, timeout: int = 5) -> bool: """Test the connection with a timeout.""" connection = RS485Connection(host, port) try: diff --git a/custom_components/kocom_wallpad/const.py b/custom_components/kocom_wallpad/const.py index f390b40..1c4f5af 100644 --- a/custom_components/kocom_wallpad/const.py +++ b/custom_components/kocom_wallpad/const.py @@ -27,7 +27,7 @@ BRAND_NAME = "Kocom" MANUFACTURER = "KOCOM Co., Ltd" MODEL = "Smart Wallpad" -SW_VERSION = "1.1.2" +SW_VERSION = "1.1.3" DEVICE_TYPE = "device_type" ROOM_ID = "room_id" diff --git a/custom_components/kocom_wallpad/entity.py b/custom_components/kocom_wallpad/entity.py index 1e5c2ce..2a6516b 100644 --- a/custom_components/kocom_wallpad/entity.py +++ b/custom_components/kocom_wallpad/entity.py @@ -42,7 +42,7 @@ def __init__( self.packet = packet self.packet_update_signal = f"{DOMAIN}_{self.gateway.host}_{self.device_id}" - self._attr_unique_id = f"{BRAND_NAME}_{self.device_id}-{self.gateway.host}".lower() + self._attr_unique_id = f"{BRAND_NAME}_{self.device_id}:{self.gateway.host}".lower() self._attr_name = f"{BRAND_NAME} {self.device_name}" self._attr_extra_state_attributes = { DEVICE_TYPE: self.packet._device.device_type, @@ -86,7 +86,6 @@ def async_handle_packet_update(self, packet: KocomPacket) -> None: async def async_added_to_hass(self) -> None: """When entity is added to hass.""" - await super().async_added_to_hass() self.async_on_remove( async_dispatcher_connect( self.hass, @@ -94,6 +93,7 @@ async def async_added_to_hass(self) -> None: self.async_handle_packet_update ) ) + await super().async_added_to_hass() @property def extra_restore_state_data(self) -> RestoredExtraData: diff --git a/custom_components/kocom_wallpad/gateway.py b/custom_components/kocom_wallpad/gateway.py index 9ae5f97..62f1c54 100644 --- a/custom_components/kocom_wallpad/gateway.py +++ b/custom_components/kocom_wallpad/gateway.py @@ -123,15 +123,15 @@ async def _handle_device_update(self, packet: KocomPacket) -> None: device = packet._device dev_id = create_dev_id(device.device_type, device.room_id, device.sub_id) + packet_update_signal = f"{DOMAIN}_{self.host}_{dev_id}" + async_dispatcher_send(self.hass, packet_update_signal, packet) + if dev_id not in self.entities[platform]: self.entities[platform][dev_id] = packet add_signal = f"{DOMAIN}_{platform.value}_add" async_dispatcher_send(self.hass, add_signal, packet) - packet_update_signal = f"{DOMAIN}_{self.host}_{dev_id}" - async_dispatcher_send(self.hass, packet_update_signal, packet) - def parse_platform(self, packet: KocomPacket) -> Platform | None: """Parse the platform from the packet.""" platform = PLATFORM_MAPPING.get(type(packet)) diff --git a/custom_components/kocom_wallpad/light.py b/custom_components/kocom_wallpad/light.py index ac83039..b3008c0 100644 --- a/custom_components/kocom_wallpad/light.py +++ b/custom_components/kocom_wallpad/light.py @@ -55,33 +55,47 @@ def __init__( ) -> None: """Initialize the light.""" super().__init__(gateway, packet) - self.has_brightness = False - self.max_brightness = 0 + + if self.is_brightness: + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS + + @property + def is_brightness(self) -> bool: + """Return whether brightness is supported.""" + return bool(self.packet._last_data[self.packet.device_id]["bri_lv"]) + + @property + def max_brightness(self) -> int: + """Return the maximum supported brightness.""" + return len(self.packet._last_data[self.packet.device_id]["bri_lv"]) + 1 @property def is_on(self) -> bool: """Return true if light is on.""" - if self.packet._device.state.get(BRIGHTNESS): - self.has_brightness = True + if self.is_brightness: self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS - self.max_brightness = len(self.packet._device.state[LEVEL]) + 1 return self.packet._device.state[POWER] @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self.packet._device.state[BRIGHTNESS] not in self.packet._device.state[LEVEL]: + brightness = self.packet._device.state.get(BRIGHTNESS, 0) + level = self.packet._device.state.get(LEVEL, []) + + if brightness not in level: return 255 - return ((225 // self.max_brightness) * self.packet._device.state[BRIGHTNESS]) + 1 + return ((225 // self.max_brightness) * brightness) + 1 async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" - if self.has_brightness: + if self.is_brightness: brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255)) brightness = ((brightness * 3) // 225) + 1 - if brightness not in self.packet._device.state[LEVEL]: + brightness_level = self.packet._device.state.get(LEVEL, []) + if brightness not in brightness_level: brightness = 255 make_packet = self.packet.make_brightness_status(brightness) else: diff --git a/custom_components/kocom_wallpad/manifest.json b/custom_components/kocom_wallpad/manifest.json index 7339d53..e0e1351 100644 --- a/custom_components/kocom_wallpad/manifest.json +++ b/custom_components/kocom_wallpad/manifest.json @@ -8,6 +8,6 @@ "documentation": "https://github.com/lunDreame/kocom-wallpad", "iot_class": "local_push", "issue_tracker": "https://github.com/lunDreame/kocom-wallpad/issues", - "requirements": [], - "version": "1.1.2" + "requirements": ["pyserial-asyncio"], + "version": "1.1.3" } \ No newline at end of file diff --git a/custom_components/kocom_wallpad/pywallpad/client.py b/custom_components/kocom_wallpad/pywallpad/client.py index f9387e7..b423461 100644 --- a/custom_components/kocom_wallpad/pywallpad/client.py +++ b/custom_components/kocom_wallpad/pywallpad/client.py @@ -44,7 +44,7 @@ def __init__(self, connection: RS485Connection) -> None: self.device_callbacks: list[Callable[[KocomPacket], Awaitable[None]]] = [] self.packet_queue: asyncio.Queue[PacketQueue] = asyncio.Queue() self.last_packet: KocomPacket | None = None - self.packet_lock: asyncio.Lock = asyncio.Lock() + #self.packet_lock: asyncio.Lock = asyncio.Lock() async def start(self) -> None: """Start the client.""" @@ -108,9 +108,9 @@ async def _process_packet(self, packet: bytes) -> None: f"{log_message}: {parsed_packet}, {parsed_packet._device}, {parsed_packet._last_data}" ) if isinstance(parsed_packet, KocomPacket): - async with self.packet_lock: + #async with self.packet_lock: self.last_packet = parsed_packet - #_LOGGER.debug(f"Updated last packet: {parsed_packet}") + # _LOGGER.debug(f"Updated last packet: {parsed_packet}") if parsed_packet._device is None: continue @@ -139,20 +139,20 @@ async def _process_queue(self) -> None: start_time = time.time() while (time.time() - start_time) < 1.0: - async with self.packet_lock: - if self.last_packet is None: - await asyncio.sleep(0.1) - continue - - if (self.last_packet.device_id == packet.device_id and - self.last_packet.sequence == packet.sequence and - self.last_packet.dest == packet.src and - self.last_packet.src == packet.dest): - found_match = True - self.last_packet = None - break - + #async with self.packet_lock: + if self.last_packet is None: await asyncio.sleep(0.1) + continue + + if (self.last_packet.device_id == packet.device_id and + self.last_packet.sequence == packet.sequence and + self.last_packet.dest == packet.src and + self.last_packet.src == packet.dest): + found_match = True + self.last_packet = None + break + + await asyncio.sleep(0.1) if not found_match: _LOGGER.debug("not received ack retrying..") diff --git a/custom_components/kocom_wallpad/pywallpad/packet.py b/custom_components/kocom_wallpad/pywallpad/packet.py index cf0a8b0..d50f6c8 100644 --- a/custom_components/kocom_wallpad/pywallpad/packet.py +++ b/custom_components/kocom_wallpad/pywallpad/packet.py @@ -271,9 +271,9 @@ def __init__(self, packet: bytes) -> None: super().__init__(packet) if self.device_id not in self._class_last_data: self._class_last_data[self.device_id] = { + "is_hot_water": False, "last_target_temp": 22, } - self._class_last_data["is_hotwater"] = False self._last_data.update(self._class_last_data) def parse_data(self) -> list[Device]: @@ -316,9 +316,9 @@ def parse_data(self) -> list[Device]: ) ) - #if is_hotwater or self._last_data["is_hotwater"]: + #if is_hotwater or self._last_data[self.device_id]["is_hot_water"]: # _LOGGER.debug(f"Hot water: {is_hotwater}") - # self._last_data["is_hotwater"] = True + # self._last_data[self.device_id]["is_hot_water"] = True # devices.append( # Device( # device_type=self.device_name(), @@ -676,6 +676,8 @@ def parse_data(self) -> list[Device]: state={POWER: self._ev_invoke}, ) ) + self._ev_invoke = False + devices.append( Device( device_type=self.device_name(capital=True), @@ -686,6 +688,9 @@ def parse_data(self) -> list[Device]: ) ) + if self._ev_direction == self.Direction.ARRIVAL: + self._ev_direction = self.Direction.IDLE + if int(self._ev_floor) >> 4 == 0x08: # 지하 층 self._ev_floor = f"B{str(int(self._ev_floor) & 0x0F)}" @@ -703,11 +708,7 @@ def parse_data(self) -> list[Device]: sub_id=FLOOR, ) ) - - if self._ev_direction == self.Direction.ARRIVAL: - self._ev_invoke = False - self._ev_direction = self.Direction.IDLE - + return devices def make_power_status(self, power: bool) -> bytearray: diff --git a/custom_components/kocom_wallpad/translations/en.json b/custom_components/kocom_wallpad/translations/en.json index ae2894c..9f9ddd4 100644 --- a/custom_components/kocom_wallpad/translations/en.json +++ b/custom_components/kocom_wallpad/translations/en.json @@ -7,6 +7,9 @@ "data": { "host": "Host", "port": "Port" + }, + "data_description": { + "host": "If it is serial communication, enter Host and ignore Port." } } }, diff --git a/custom_components/kocom_wallpad/translations/ko.json b/custom_components/kocom_wallpad/translations/ko.json index 6b27422..89ff7f9 100644 --- a/custom_components/kocom_wallpad/translations/ko.json +++ b/custom_components/kocom_wallpad/translations/ko.json @@ -7,6 +7,9 @@ "data": { "host": "호스트", "port": "포트" + }, + "data_description": { + "host": "시리얼 통신인 경우 호스트에 입력하고 포트는 무시하세요." } } },