Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
8 changes: 5 additions & 3 deletions custom_components/liebherr/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]

await asyncio.sleep(30)
appliances = await api.get_appliances()
entities = []
for appliance in appliances:
await asyncio.sleep(30)
controls = await api.get_controls(appliance["deviceId"])
if not controls:
_LOGGER.warning("No controls found for appliance %s",
Expand Down Expand Up @@ -120,7 +122,7 @@ async def async_set_temperature(self, **kwargs):
)

await self.api.set_value(self._appliance["deviceId"], "temperature", data)
await asyncio.sleep(5)
await asyncio.sleep(30)
await self.coordinator.async_request_refresh()

@property
Expand Down Expand Up @@ -184,10 +186,10 @@ async def async_set_hvac_mode(self, hvac_mode):
"""Set the HVAC mode."""
if hvac_mode in self._attr_hvac_modes:
self._attr_hvac_mode = hvac_mode
await asyncio.sleep(5)
await asyncio.sleep(30)
await self.coordinator.async_request_refresh()

async def async_update(self):
"""Fetch the latest data from the API."""
await asyncio.sleep(5)
await asyncio.sleep(30)
await self.coordinator.async_request_refresh()
170 changes: 113 additions & 57 deletions custom_components/liebherr/cover.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
"""Support for Liebherr autodoor devices."""
"""Support for Liebherr autodoor devices with debounce logic."""

import asyncio
import logging
from datetime import datetime, timedelta

from homeassistant.components.cover import CoverEntity
from homeassistant.components.cover import CoverEntity, CoverEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_UNKNOWN

from .const import DOMAIN
from .models import AutoDoorControl

_LOGGER = logging.getLogger(__name__)

DEBOUNCE_SECONDS = 30 # time to wait before confirming final door state


async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
Expand All @@ -19,95 +24,146 @@ async def async_setup_entry(
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]

await asyncio.sleep(30)
appliances = await api.get_appliances()

entities = []

for appliance in appliances:
await asyncio.sleep(30)
controls = await api.get_controls(appliance["deviceId"])
if not controls:
_LOGGER.warning("No controls found for appliance %s",
appliance["deviceId"])
_LOGGER.warning("No controls found for appliance %s", appliance["deviceId"])
continue

for control in controls:
if control["type"] == "autodoor":
entities.extend(
[
LiebherrCover(api, coordinator, appliance, control),
]
)
# entities.append(LiebherrCover(api, coordinator, appliance, control))
if control["type"] == "AutoDoorControl":
entities.append(LiebherrCover(api, coordinator, appliance, control))

async_add_entities(entities)


class LiebherrCover(CoverEntity):
"""Representation of a Liebherr cover entity."""
"""Representation of a Liebherr auto door cover with debounce."""

def __init__(self, api, coordinator, appliance, control) -> None:
"""Initialize the cover entity."""
self._api = api
self._coordinator = coordinator
self._appliance = appliance
self._control = control

self._device_id = appliance["deviceId"]
self._identifier = control.get("identifier", control["type"])
self._attr_name = f"{appliance['nickname']} {self._identifier}"
self._attr_unique_id = f"{appliance['deviceId']}_{self._identifier}"
self._is_opening = False
self._attr_is_closed = not self.is_open

self._attr_name = f"{appliance.get('nickname', 'Liebherr')} {self._identifier}"
self._attr_unique_id = f"{self._device_id}_{self._identifier}"
self._attr_device_class = "door"
self._attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE

# For debounce:
self._last_state = None
self._last_state_change = None
self._debounce_task = None
self._confirmed_state = STATE_UNKNOWN

@property
def device_info(self):
"""Return device information for the cover."""
"""Return device information."""
return {
"identifiers": {(DOMAIN, self._appliance["deviceId"])},
"name": self._appliance.get(
"nickname", f"Liebherr HomeAPI Appliance {self._appliance['deviceId']}"
),
"identifiers": {(DOMAIN, self._device_id)},
"name": self._appliance.get("nickname", f"Liebherr {self._device_id}"),
"manufacturer": "Liebherr",
"model": self._appliance.get("model", self._appliance["model"]),
"model": self._appliance.get("model", ""),
"sw_version": self._appliance.get("softwareVersion", ""),
}

@property
def is_open(self):
"""Return true if the cover is open."""
def _get_control_state(self):
"""Return the current state of the control."""
if not self._coordinator.data:
_LOGGER.error("Coordinator data is empty")
return False

controls = []
appliances = self._coordinator.data.get("appliances", [])
for device in appliances:
if device.get("deviceId") == self._appliance["deviceId"]:
controls = device.get("controls", [])
for control in controls:
if control.get("identifier", control["type"]) == self._identifier:
return control.get("active", False)
return False
return None

for device in self._coordinator.data.get("appliances", []):
if device.get("deviceId") != self._device_id:
continue
for control in device.get("controls", []):
if control.get("identifier", control["type"]) == self._identifier:
return control.get("value")

return None

async def _debounce_state(self, new_state):
"""Debounce door state changes to avoid flickering."""
# Cancel existing debounce task if any
if self._debounce_task and not self._debounce_task.done():
self._debounce_task.cancel()

# If door is MOVING, update immediately but do not confirm
if new_state == "MOVING":
self._confirmed_state = STATE_OPENING
self.async_write_ha_state()
return

# If state changed from last confirmed state, start debounce delay
if new_state != self._confirmed_state:
self._last_state = new_state
self._last_state_change = datetime.now()

async def wait_and_confirm():
try:
await asyncio.sleep(DEBOUNCE_SECONDS)
# After waiting, confirm the new state
self._confirmed_state = (
STATE_OPEN if new_state == "OPEN" else STATE_CLOSED
)
self.async_write_ha_state()
except asyncio.CancelledError:
# Debounce canceled because new update arrived
pass

self._debounce_task = asyncio.create_task(wait_and_confirm())

@property
def available(self):
"""Return True if the cover is available."""
return True
def state(self):
"""Return the current debounced state of the cover."""
return self._confirmed_state

async def async_open_cover(self, **kwargs):
"""Open the cover."""
if self._control["type"] == "autodoor":
await self._api.set_value(
self._appliance["deviceId"] + "/" + self._control["endpoint"],
{"autoDoorMode": "OPEN"},
)
self._is_opening = True
await asyncio.sleep(5)
self._is_opening = False
await self._coordinator.async_request_refresh()
@property
def is_closed(self):
"""Return True if the cover is closed."""
return self._confirmed_state == STATE_CLOSED

@property
def is_opening(self):
"""Return if the cover is opening or not."""
return self._is_opening
def is_open(self):
"""Return True if the cover is open."""
return self._confirmed_state == STATE_OPEN

async def async_open_cover(self, **kwargs):
"""Send command to open the cover."""
try:
data = AutoDoorControl(zoneId=self._control.get("zoneId"), value=True)
await self._api.set_value(self._device_id, self._control["name"], data)
await asyncio.sleep(30) # Let the door start moving
except Exception as e:
_LOGGER.error("Failed to open door %s: %s", self._identifier, e)
await asyncio.sleep(30)
await self._coordinator.async_request_refresh()

async def async_close_cover(self, **kwargs):
"""Close the cover."""
# Closing is automatic, no action needed
"""Send command to close the cover."""
try:
data = AutoDoorControl(zoneId=self._control.get("zoneId"), value=False)
await self._api.set_value(self._device_id, self._control["name"], data)
await asyncio.sleep(30) # Let the door start moving
except Exception as e:
_LOGGER.error("Failed to close door %s: %s", self._identifier, e)
await asyncio.sleep(30)
await self._coordinator.async_request_refresh()

async def async_update(self):
"""Called by coordinator on data update; update state with debounce."""
raw_state = self._get_control_state()
if raw_state is None:
self._confirmed_state = STATE_UNKNOWN
self.async_write_ha_state()
return
await self._debounce_state(raw_state)
10 changes: 10 additions & 0 deletions custom_components/liebherr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ class ModeZoneControlRequest:
@dataclass
class ModeControlRequest:
mode: str

@dataclass
class IceMakerControlRequest:
zoneId: int
iceMakerMode: str # "OFF", "ON", or "MAX_ICE"

@dataclass
class AutoDoorControl:
zoneId: int
value: bool # True = open, False = close
Loading