Skip to content

Commit

Permalink
Merge pull request #5 from lunDreame/bug-fix
Browse files Browse the repository at this point in the history
Bug fix
  • Loading branch information
lunDreame authored Jan 2, 2025
2 parents 4c42a13 + 37a62e3 commit df3deca
Show file tree
Hide file tree
Showing 14 changed files with 209 additions and 140 deletions.
5 changes: 4 additions & 1 deletion custom_components/kocom_wallpad/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from homeassistant.const import Platform
from homeassistant.const import Platform, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry

Expand Down Expand Up @@ -32,6 +32,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await gateway.async_start()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.async_close)
)

return True

Expand Down
12 changes: 6 additions & 6 deletions custom_components/kocom_wallpad/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ def __init__(
) -> None:
"""Initialize the binary sensor."""
super().__init__(gateway, packet)
self._attr_is_on = self.device.state[STATE]
self._attr_is_on = self.packet._device.state[STATE]
self._attr_extra_state_attributes = {
DEVICE_TYPE: self.device.device_type,
ROOM_ID: self.device.room_id,
SUB_ID: self.device.sub_id,
ERROR_CODE: self.device.state[ERROR_CODE],
DEVICE_TYPE: self.packet._device.device_type,
ROOM_ID: self.packet._device.room_id,
SUB_ID: self.packet._device.sub_id,
ERROR_CODE: self.packet._device.state[ERROR_CODE],
}

if self.packet.device_type == DeviceType.MOTION:
self._attr_device_class = BinarySensorDeviceClass.MOTION
del self._attr_extra_state_attributes[ERROR_CODE]
self._attr_extra_state_attributes[TIME] = self.device.state[TIME]
self._attr_extra_state_attributes[TIME] = self.packet._device.state[TIME]
18 changes: 9 additions & 9 deletions custom_components/kocom_wallpad/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,22 @@ def __init__(self, gateway: KocomGateway, packet: KocomPacket) -> None:
@property
def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode."""
return HVACMode.HEAT if self.device.state[POWER] else HVACMode.OFF
return HVACMode.HEAT if self.packet._device.state[POWER] else HVACMode.OFF

@property
def preset_mode(self) -> str:
"""Return the current preset mode."""
return PRESET_AWAY if self.device.state[AWAY_MODE] else PRESET_NONE
return PRESET_AWAY if self.packet._device.state[AWAY_MODE] else PRESET_NONE

@property
def current_temperature(self) -> int:
"""Return the current temperature."""
return self.device.state[CURRENT_TEMP]
return self.packet._device.state[CURRENT_TEMP]

@property
def target_temperature(self) -> int:
"""Return the target temperature."""
return self.device.state[TARGET_TEMP]
return self.packet._device.state[TARGET_TEMP]

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
Expand Down Expand Up @@ -166,8 +166,8 @@ def __init__(self, gateway: KocomGateway, packet: KocomPacket) -> None:
@property
def hvac_mode(self) -> HVACMode:
"""Return current HVAC mode."""
if self.device.state[POWER]:
op_mode = self.device.state[OP_MODE]
if self.packet._device.state[POWER]:
op_mode = self.packet._device.state[OP_MODE]
return {
OpMode.COOL: HVACMode.COOL,
OpMode.FAN_ONLY: HVACMode.FAN_ONLY,
Expand All @@ -183,17 +183,17 @@ def fan_mode(self) -> str:
FanMode.LOW: FAN_LOW,
FanMode.MEDIUM: FAN_MEDIUM,
FanMode.HIGH: FAN_HIGH,
}.get(self.device.state[FAN_MODE])
}.get(self.packet._device.state[FAN_MODE])

@property
def current_temperature(self) -> int:
"""Return the current temperature."""
return self.device.state[CURRENT_TEMP]
return self.packet._device.state[CURRENT_TEMP]

@property
def target_temperature(self) -> int:
"""Return the target temperature."""
return self.device.state[TARGET_TEMP]
return self.packet._device.state[TARGET_TEMP]

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set a new target HVAC mode."""
Expand Down
1 change: 1 addition & 0 deletions custom_components/kocom_wallpad/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async def send(self, packet: bytearray) -> None:
try:
self.writer.write(packet)
await self.writer.drain()
await asyncio.sleep(0.5)
except Exception as e:
LOGGER.error(f"Failed to send packet data: {e}")
await self.reconnect()
Expand Down
1 change: 1 addition & 0 deletions custom_components/kocom_wallpad/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
SUB_ID = "sub_id"

PACKET_DATA = "packet_data"
LAST_DATA = "last_data"

PLATFORM_MAPPING: dict[type[KocomPacket], Platform] = { # type: ignore
LightPacket: Platform.LIGHT,
Expand Down
54 changes: 26 additions & 28 deletions custom_components/kocom_wallpad/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.restore_state import RestoreEntity, RestoredExtraData
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import Throttle

from datetime import timedelta

from .pywallpad.packet import Device, KocomPacket
from .pywallpad.packet import KocomPacket

from .gateway import KocomGateway
from .util import process_string, create_dev_id, encode_bytes_to_base64
Expand All @@ -25,16 +22,15 @@
ROOM_ID,
SUB_ID,
PACKET_DATA,
LAST_DATA,
)

MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)


class KocomEntity(RestoreEntity):
"""Base class for Kocom Wallpad entities."""

_attr_has_entity_name = True
_attr_should_poll = True
_attr_should_poll = False

def __init__(
self,
Expand All @@ -44,22 +40,23 @@ def __init__(
"""Initialize the Kocom Wallpad entity."""
self.gateway = gateway
self.packet = packet
self.device = packet._device
self.device_update_signal = f"{DOMAIN}_{self.gateway.host}_{self.device_id}"
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}"
self._attr_name = f"{BRAND_NAME} {self.device_name}"
self._attr_extra_state_attributes = {
DEVICE_TYPE: self.device.device_type,
ROOM_ID: self.device.room_id,
SUB_ID: self.device.sub_id,
DEVICE_TYPE: self.packet._device.device_type,
ROOM_ID: self.packet._device.room_id,
SUB_ID: self.packet._device.sub_id,
}

@property
def device_id(self) -> str:
"""Return the device id."""
return create_dev_id(
self.device.device_type, self.device.room_id, self.device.sub_id
self.packet._device.device_type,
self.packet._device.room_id,
self.packet._device.sub_id
)

@property
Expand All @@ -71,10 +68,10 @@ def device_name(self) -> str:
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
identifiers={(DOMAIN, f"{self.gateway.host}_{self.device.device_type}")},
identifiers={(DOMAIN, f"{self.gateway.host}_{self.packet._device.device_type}")},
manufacturer=MANUFACTURER,
model=MODEL,
name=f"{BRAND_NAME} {process_string(self.device.device_type)}",
name=f"{BRAND_NAME} {process_string(self.packet._device.device_type)}",
sw_version=SW_VERSION,
via_device=(DOMAIN, self.gateway.host),
)
Expand All @@ -85,31 +82,32 @@ def available(self) -> bool:
return self.gateway.connection.is_connected()

@callback
def async_handle_device_update(self, device: Device) -> None:
"""Handle device update."""
if self.device.state != device.state:
self.device.state = device.state
def async_handle_packet_update(self, packet: KocomPacket) -> None:
"""Handle packet update."""
if self.packet.packet != packet.packet:
self.packet = packet
self.async_write_ha_state()

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, self.device_update_signal, self.async_handle_device_update)
async_dispatcher_connect(
self.hass,
self.packet_update_signal,
self.async_handle_packet_update
)
)

@property
def extra_restore_state_data(self) -> RestoredExtraData:
"""Return extra state data to be restored."""
return RestoredExtraData({PACKET_DATA: encode_bytes_to_base64(self.packet.packet)})
extra_data = {
PACKET_DATA: encode_bytes_to_base64(self.packet.packet),
LAST_DATA: self.packet._last_data,
}
return RestoredExtraData(extra_data)

async def send_packet(self, packet: bytes) -> None:
"""Send a packet to the gateway."""
await self.gateway.client.send_packet(packet)

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> None:
"""Update device state."""
if hasattr(self.packet, "make_scan") and callable(self.packet.make_scan):
make_packet = self.packet.make_scan()
await self.send_packet(make_packet)
6 changes: 3 additions & 3 deletions custom_components/kocom_wallpad/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,20 @@ def __init__(
@property
def is_on(self) -> bool:
"""Return the state of the fan."""
return self.device.state[POWER]
return self.packet._device.state[POWER]

@property
def percentage(self) -> int:
"""Return the current speed percentage."""
fan_speed = self.device.state[FAN_SPEED]
fan_speed = self.packet._device.state[FAN_SPEED]
if fan_speed == FanSpeed.OFF:
return 0
return ordered_list_item_to_percentage(self._attr_speed_list, fan_speed.value)

@property
def preset_mode(self) -> str:
"""Return the current preset mode."""
vent_mode = self.device.state[VENT_MODE]
vent_mode = self.packet._device.state[VENT_MODE]
return vent_mode.name

async def async_set_percentage(self, percentage: int) -> None:
Expand Down
22 changes: 17 additions & 5 deletions custom_components/kocom_wallpad/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from homeassistant.const import Platform, CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, Event
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers import entity_registry as er, restore_state
Expand All @@ -22,7 +22,7 @@

from .connection import Connection
from .util import create_dev_id, decode_base64_to_bytes
from .const import LOGGER, DOMAIN, PACKET_DATA, PLATFORM_MAPPING
from .const import LOGGER, DOMAIN, PACKET_DATA, LAST_DATA, PLATFORM_MAPPING


class KocomGateway:
Expand Down Expand Up @@ -60,6 +60,10 @@ async def async_start(self) -> None:
await self.client.start()
self.client.add_device_callback(self._handle_device_update)

async def async_close(self, event: Event) -> None:
"""Close the gateway."""
await self.async_disconnect()

def get_entities(self, platform: Platform) -> list[KocomPacket]:
"""Get the entities for the platform."""
return list(self.entities.get(platform, {}).values())
Expand All @@ -71,8 +75,16 @@ async def _async_fetch_last_packets(self, entity_id: str) -> list[KocomPacket]:

if not state or not state.extra_data:
return []

packet_data = state.extra_data.as_dict().get(PACKET_DATA)
return PacketParser.parse_state(decode_base64_to_bytes(packet_data)) if packet_data else []
if not packet_data:
return []

packet = decode_base64_to_bytes(packet_data)
last_data = state.extra_data.as_dict().get(LAST_DATA)
LOGGER.debug(f"Last data: {last_data}")

return PacketParser.parse_state(packet, last_data)

async def async_update_entity_registry(self) -> None:
"""Update the entity registry."""
Expand Down Expand Up @@ -104,8 +116,8 @@ async def _handle_device_update(self, packet: KocomPacket) -> None:
add_signal = f"{DOMAIN}_{platform.value}_add"
async_dispatcher_send(self.hass, add_signal, packet)

device_update_signal = f"{DOMAIN}_{self.host}_{dev_id}"
async_dispatcher_send(self.hass, device_update_signal, device)
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."""
Expand Down
12 changes: 6 additions & 6 deletions custom_components/kocom_wallpad/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,27 @@ def __init__(
@property
def is_on(self) -> bool:
"""Return true if light is on."""
if self.device.state.get(BRIGHTNESS):
if self.packet._device.state.get(BRIGHTNESS):
self.has_brightness = True
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
self._attr_color_mode = ColorMode.BRIGHTNESS
self.max_brightness = len(self.device.state[LEVEL]) + 1
self.max_brightness = len(self.packet._device.state[LEVEL]) + 1

return self.device.state[POWER]
return self.packet._device.state[POWER]

@property
def brightness(self) -> int:
"""Return the brightness of this light between 0..255."""
if self.device.state[BRIGHTNESS] not in self.device.state[LEVEL]:
if self.packet._device.state[BRIGHTNESS] not in self.packet._device.state[LEVEL]:
return 255
return ((225 // self.max_brightness) * self.device.state[BRIGHTNESS]) + 1
return ((225 // self.max_brightness) * self.packet._device.state[BRIGHTNESS]) + 1

async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on light."""
if self.has_brightness:
brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255))
brightness = ((brightness * 3) // 225) + 1
if brightness not in self.device.state[LEVEL]:
if brightness not in self.packet._device.state[LEVEL]:
brightness = 255
make_packet = self.packet.make_brightness_status(brightness)
else:
Expand Down
10 changes: 7 additions & 3 deletions custom_components/kocom_wallpad/pywallpad/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __init__(
self,
connection: Connection,
timeout: float = 0.25,
max_retries = 3
max_retries = 5
) -> None:
"""Initialize the KocomClient."""
self.connection = connection
Expand Down Expand Up @@ -89,7 +89,7 @@ async def stop(self) -> None:
def add_device_callback(self, callback: Callable[[dict], Awaitable[None]]) -> None:
"""Add callback for device updates."""
self.device_callbacks.append(callback)

async def _listen(self) -> None:
"""Listen for incoming packets."""
while self.connection.is_connected():
Expand All @@ -103,9 +103,13 @@ async def _listen(self) -> None:
if not verify_checksum(packet):
_LOGGER.debug("Checksum verification failed for packet: %s", packet.hex())
continue

parsed_packets = PacketParser.parse_state(packet)
for parsed_packet in parsed_packets:
_LOGGER.debug(
"Received packet: %s, %s, %s",
parsed_packet, parsed_packet._device, parsed_packet._last_data
)
for callback in self.device_callbacks:
await callback(parsed_packet)
except Exception as e:
Expand Down
Loading

0 comments on commit df3deca

Please sign in to comment.