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

Parsing error using INSPELNING smart plug #137

Open
jfhautenauven opened this issue Feb 24, 2025 · 5 comments
Open

Parsing error using INSPELNING smart plug #137

jfhautenauven opened this issue Feb 24, 2025 · 5 comments

Comments

@jfhautenauven
Copy link

Hello,

Just wanted to let you know something that could be fixed :)

I just bought an INSPELNING plug from Ikea.

I see in the logs some parsing errors of dates :)

Here is the log :

Cette erreur provient d'une intégration personnalisée

Enregistreur: custom_components.dirigera_platform
Source: custom_components/dirigera_platform/hub_event_listener.py:276
intégration: IKEA Dirigera Hub Integration (documentation, problèmes)
S'est produit pour la première fois: 20:34:52 (7 occurrences)
Dernier enregistrement: 20:40:51

Failed to convert 2025-02-24T19:36:51.000Z to date/time...
Failed to convert 2025-02-24T19:37:51.000Z to date/time...
Failed to convert 2025-02-24T19:38:51.000Z to date/time...
Failed to convert 2025-02-24T19:39:51.000Z to date/time...
Failed to convert 2025-02-24T19:40:51.000Z to date/time...

Just to let you know, doesn't prevent me from using the plug, but it pops some errors :)

Thanks in advance,

KR,

@MasseR
Copy link

MasseR commented Feb 27, 2025

i'm not absolutely sure, but I think this is causing a lot of error messages such as:

Traceback (most recent call last):
  File "/usr/local/lib/python3.13/asyncio/events.py", line 89, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1014, in _async_write_ha_state_from_call_soon_threadsafe
    self._async_write_ha_state()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1148, in _async_write_ha_state
    self.__async_calculate_state()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1085, in __async_calculate_state
    state = self._stringify_state(available)
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1029, in _stringify_state
    if (state := self.state) is None:
                 ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/datetime/__init__.py", line 106, in state
    if value.tzinfo is None:
       ^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'tzinfo'

I have ~30k such occurrences after the last restart a couple of hours ago.

@jfhautenauven
Copy link
Author

Indeed there is a PR waiting for this repo containing the fix, but the repo maintainer isn't available I guess ...

@Vetti52
Copy link

Vetti52 commented Mar 3, 2025

Same at my installation, with 44 such events in 10 minutes:

Dieser Fehler wurde von einer benutzerdefinierten Integration verursacht

Logger: custom_components.dirigera_platform
Quelle: custom_components/dirigera_platform/hub_event_listener.py:276
Integration: IKEA Dirigera Hub Integration (Dokumentation, Probleme)
Erstmals aufgetreten: 10:20:25 (44 Vorkommnisse)
Zuletzt protokolliert: 10:30:53

Failed to convert 2025-03-03T09:29:53.000Z to date/time...
Failed to convert 2025-03-03T09:30:24.000Z to date/time...
Failed to convert 2025-03-03T09:30:25.000Z to date/time...
Failed to convert 2025-03-03T09:30:51.000Z to date/time...
Failed to convert 2025-03-03T09:30:53.000Z to date/time...

The helper error also occurs. I tried with disabling sensor.inspelningplug_time_energy_consumed_last_updated and sensor.inspelningplug_time_of_last_energy_reset entries, which provide date/time data only. But no success.

@Vetti52
Copy link

Vetti52 commented Mar 3, 2025

BTW: Is it a data retrieving failure, as Inspelning results in 0 (zero) watts, when it is plugged as an input measurement at a mini pv panel? That said, does it not display negative values or does the plug not provide negative values? Amp values are shown, anyway.

@PetterSjo
Copy link

I have the same problem and it started after i integrated "INSPELNING" in my IKEA setup. I could (with some AI use) solve my problems by changeing how hub_event_listener.py handels datetime zones.

I changed all use of datetime.strptime() to dateutil.parser.isoparse() to better handle ISO date formats (%Y-%m-%dT%H:%M:%S.%fZ)

I unfortunately do not know how to work with GitHub to make this a PR but feel free if anyone likes the solution and knows how to do.

import threading
import logging
import time
import json
import re
import websocket
import ssl
from typing import Any
from datetime import datetime, timedelta
from dirigera import Hub
from dateutil import parser # Ny import för bättre datumhantering

from homeassistant.const import ATTR_ENTITY_ID

logger = logging.getLogger("custom_components.dirigera_platform")

process_events_from = {
"motionSensor": ["isDetected", "isOn", "batteryPercentage"],
"outlet": ["isOn", "currentAmps", "currentActivePower", "currentVoltage",
"totalEnergyConsumed", "energyConsumedAtLastReset",
"timeOfLastEnergyReset", "totalEnergyConsumedLastUpdated"],
"light": ["isOn", "lightLevel", "colorTemperature"],
"openCloseSensor": ["isOpen", "batteryPercentage"],
"waterSensor": ["waterLeakDetected", "batteryPercentage"],
"blinds": ["blindsCurrentLevel", "batteryPercentage"],
"environmentSensor": ["currentTemperature", "currentRH", "currentPM25",
"vocIndex", "batteryPercentage"]
}

controller_trigger_last_time_map = {}

def to_snake_case(name: str) -> str:
return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()

class registry_entry:
def init(self, entity: Any, cascade_entity: Any = None):
self._entity = entity
self._cascade_entity = cascade_entity

@property
def entity(self):
    return self._entity

@property
def cascade_entity(self):
    return self._cascade_entity

@cascade_entity.setter
def cascade_entity(self, value):
    self._cascade_entity = value

def __str__(self):
    return f"registry_entry: id {self._entity.unique_id}, cascade_entry : {self._cascade_entity or 'None'}"

class hub_event_listener(threading.Thread):
device_registry = {}

@staticmethod
def register(id: str, entry: registry_entry):
    if id not in hub_event_listener.device_registry:
        hub_event_listener.device_registry[id] = entry

@staticmethod
def get_registry_entry(id: str) -> registry_entry:
    return hub_event_listener.device_registry.get(id, None)

def __init__(self, hub: Hub, hass):
    super().__init__()
    self._hub: Hub = hub
    self._request_to_stop = False
    self._hass = hass

def on_error(self, ws: Any, ws_msg: str):
    logger.debug(f"on_error hub event listener {ws_msg}")

def parse_scene_update(self, msg):
    global controller_trigger_last_time_map
    if "data" not in msg or "triggers" not in msg["data"]:
        logger.warning(f"discarding message due to missing keys: {msg}")
        return

    for trigger in msg["data"]["triggers"]:
        if "type" not in trigger or trigger["type"] != "controller":
            continue
        if "trigger" not in trigger:
            continue

        details = trigger["trigger"]
        if "controllerType" not in details or "clickPattern" not in details or "deviceId" not in details:
            continue

        controller_type = details["controllerType"]
        click_pattern = details["clickPattern"]
        device_id = details["deviceId"]

        if controller_type != "shortcutController":
            continue

        trigger_type = {
            "singlePress": "single_click",
            "longPress": "long_press",
            "doublePress": "double_click"
        }.get(click_pattern, None)

        if not trigger_type:
            continue

        registry_value = hub_event_listener.get_registry_entry(device_id)
        if not isinstance(registry_value, registry_entry):
            continue

        entity = registry_value.entity
        unique_key = f"{entity.registry_entry.device_id}_{trigger_type}"
        controller_trigger_last_time_map[unique_key] = datetime.now()

        if "lastTriggered" in msg["data"]:
            try:
                current_triggered = parser.isoparse(msg["data"]["lastTriggered"])
                controller_trigger_last_time_map[unique_key] = current_triggered
            except Exception as ex:
                logger.warning(f"Failed to parse lastTriggered timestamp: {msg['data']['lastTriggered']}")
                logger.warning(ex)

        event_data = {
            "type": trigger_type,
            "device_id": entity.registry_entry.device_id,
            ATTR_ENTITY_ID: entity.registry_entry.entity_id
        }
        self._hass.bus.fire(event_type="dirigera_platform_event", event_data=event_data)

def on_message(self, ws: Any, ws_msg: str):
    try:
        msg = json.loads(ws_msg)
        if msg.get('type') == "sceneUpdated":
            return self.parse_scene_update(msg)
        if msg.get('type') != "deviceStateChanged":
            return

        if "data" not in msg or "id" not in msg["data"]:
            return

        id = msg["data"]["id"]
        device_type = msg["data"].get("deviceType", msg["data"].get("type"))

        if device_type not in process_events_from or id not in hub_event_listener.device_registry:
            return

        registry_value = hub_event_listener.get_registry_entry(id)
        entity = registry_value.entity

        to_process_attr = process_events_from[device_type]

        if "attributes" in msg["data"]:
            for key, value in msg["data"]["attributes"].items():
                if key not in to_process_attr:
                    continue

                key_attr = to_snake_case(key)
                try:
                    if key in ["timeOfLastEnergyReset", "totalEnergyConsumedLastUpdated"]:
                        try:
                            value = parser.isoparse(value)
                        except Exception as ex:
                            logger.warning(f"Failed to convert {value} to date/time...")
                            logger.warning(ex)

                    setattr(entity._json_data.attributes, key_attr, value)
                except Exception as ex:
                    logger.warning(f"Failed to set attribute {key_attr} on device {id}")
                    logger.warning(ex)

            entity.schedule_update_ha_state(False)

    except Exception as ex:
        logger.error("Error processing hub event")
        logger.error(ex)

def create_listener(self):
    try:
        logger.info("Starting dirigera hub event listener")
        self._wsapp = websocket.WebSocketApp(
            self._hub.websocket_base_url,
            header={"Authorization": f"Bearer {self._hub.token}"},
            on_message=self.on_message)
        self._wsapp.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
    except Exception as ex:
        logger.error("Error creating event listener...")
        logger.error(ex)

def stop(self):
    self._request_to_stop = True
    try:
        if self._wsapp:
            self._wsapp.close()
    except:
        pass
    self.join()
    hub_event_listener.device_registry.clear()

def run(self):
    while not self._request_to_stop:
        self.create_listener()
        time.sleep(10)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants