controllerx icon indicating copy to clipboard operation
controllerx copied to clipboard

[BUG] ControllerX and Appdeamon

Open fir3drag0n opened this issue 7 months ago โ€ข 53 comments

Bug report

After migrating to the new version of Appdeamon, I get the error attached, appdeamon crashes, see log attached, see https://github.com/AppDaemon/appdaemon/issues/2286

Description

Add a description of the bug. Detail the expected behaviour in contrast with the behaviour you're observing.

Additional information

ControllerX installed as HACS component

CircularDependency: Visited [...] already, but cx_core.type.switch_controller depends on {'cx_const', 'cx_core.type_controller', 'cx_core.controller'}

This seems to be a structural import cycle in cx_core, which is now explicitly detected by AppDaemonโ€™s new dependency graph logic.

Steps to reproduce:

Use AppDaemon 4.5.2

Install ControllerX 4.29 via HACS

Configure basic ControllerX apps in apps.yaml

Workarounds tried:

Added disable: true dummy apps for all cx_devices.* modules

AppDaemon still crashes due to internal cx_core cycle

Downgrade to AppDaemon 4.4.4 works fine

Expected: ControllerX should be structured in a way that does not lead to circular import chains detectable by AppDaemon >= 4.5.

Request: Can you consider refactoring or deferring imports in the relevant modules (like switch_controller, controller, type_controller) to break the dependency loop?

AppDaemon app configuration

apps.yaml:

cx_devices___init___dummy:
  module: cx_devices.__init__
  class: Dummy
  disable: true

cx_devices_adeo_dummy:
  module: cx_devices.adeo
  class: Dummy
  disable: true

cx_devices_aqara_dummy:
  module: cx_devices.aqara
  class: Dummy
  disable: true

cx_devices_aurora_dummy:
  module: cx_devices.aurora
  class: Dummy
  disable: true

cx_devices_homematic_dummy:
  module: cx_devices.homematic
  class: Dummy
  disable: true

cx_devices_ikea_dummy:
  module: cx_devices.ikea
  class: Dummy
  disable: true

cx_devices_legrand_dummy:
  module: cx_devices.legrand
  class: Dummy
  disable: true

cx_devices_linkind_dummy:
  module: cx_devices.linkind
  class: Dummy
  disable: true

cx_devices_livarno_dummy:
  module: cx_devices.livarno
  class: Dummy
  disable: true

cx_devices_lutron_dummy:
  module: cx_devices.lutron
  class: Dummy
  disable: true

cx_devices_muller_licht_dummy:
  module: cx_devices.muller_licht
  class: Dummy
  disable: true

cx_devices_osram_dummy:
  module: cx_devices.osram
  class: Dummy
  disable: true

cx_devices_philips_dummy:
  module: cx_devices.philips
  class: Dummy
  disable: true

cx_devices_prolight_dummy:
  module: cx_devices.prolight
  class: Dummy
  disable: true

cx_devices_rgb_genie_dummy:
  module: cx_devices.rgb_genie
  class: Dummy
  disable: true

cx_devices_robb_dummy:
  module: cx_devices.robb
  class: Dummy
  disable: true

cx_devices_sengled_dummy:
  module: cx_devices.sengled
  class: Dummy
  disable: true

cx_devices_shelly_dummy:
  module: cx_devices.shelly
  class: Dummy
  disable: true

cx_devices_smartkontakten_dummy:
  module: cx_devices.smartkontakten
  class: Dummy
  disable: true

cx_devices_smartthings_dummy:
  module: cx_devices.smartthings
  class: Dummy
  disable: true

cx_devices_sonoff_dummy:
  module: cx_devices.sonoff
  class: Dummy
  disable: true

cx_devices_tasmota_dummy:
  module: cx_devices.tasmota
  class: Dummy
  disable: true

cx_devices_terncy_dummy:
  module: cx_devices.terncy
  class: Dummy
  disable: true

cx_devices_trust_dummy:
  module: cx_devices.trust
  class: Dummy
  disable: true

cx_devices_tuya_dummy:
  module: cx_devices.tuya
  class: Dummy
  disable: true

nachttisch_links:
  module: controllerx
  class: E1743Controller
  integration: mqtt
  controller: zigbee2mqtt/DIM-Nachttisch-links/action
  light: light.hue_color_lamp_1

nachttisch_rechts:
  module: controllerx
  class: E1743Controller
  integration: mqtt
  controller: zigbee2mqtt/DIM-Nachttisch-rechts/action
  light: light.schlafzimmer_nachttisch_rechts

wohnzimmer_dim:
  module: controllerx
  class: E1743Controller
  integration: mqtt
  controller: zigbee2mqtt/DIM-Wohnzimmer/action
  light: light.wohnzimmer_decke
  
flur_dim:
  module: controllerx
  class: E1743Controller
  integration: mqtt
  controller: zigbee2mqtt/DIM-Flur3/action
  light: light.hue_flur_gruppe
  merge_mapping:
    "on":
      - scene: scene.flur_tagszene

kueche_dim:
  module: controllerx
  class: E1743Controller
  integration: mqtt
  controller: zigbee2mqtt/DIM-Kueche/action
  light: light.unterschranke_kuche
  
flur_dim2:
  module: controllerx
  class: E2002LightController
  integration: mqtt
  controller: zigbee2mqtt/DIM-Flur2/action
  light: light.hue_flur_gruppe
  merge_mapping:
    "on":
      - scene: scene.flur_tagszene

esszimmer_dim:
  module: controllerx
  class: E1743CoverController
  integration: mqtt
  controller: zigbee2mqtt/DIM-Esszimmer/action
  cover: cover.esszimmer
  
wohnzimmer_dim_vorhang:
  module: controllerx
  class: E1743CoverController
  integration: mqtt
  controller: zigbee2mqtt/DIM-Wohnzimmer-Vorhang/action
  cover: cover.wohnzimmer_fenster

Logs

2025-05-28 08:21:31.989236 INFO AppDaemon: ------------------------------------------------------------

2025-05-28 08:21:31.989577 INFO AppDaemon: AppDaemon Version 4.5.2 starting

2025-05-28 08:21:31.989670 INFO AppDaemon: Additional version info: dev

2025-05-28 08:21:31.989732 INFO AppDaemon: ------------------------------------------------------------

2025-05-28 08:21:31.989815 INFO AppDaemon: Python version is 3.12.10

2025-05-28 08:21:31.990037 INFO AppDaemon: Configuration read from: /conf/appdaemon.yaml

2025-05-28 08:21:31.990204 INFO AppDaemon: Added log: AppDaemon

2025-05-28 08:21:31.990283 INFO AppDaemon: Added log: Error

2025-05-28 08:21:31.990367 INFO AppDaemon: Added log: Access

2025-05-28 08:21:31.990446 INFO AppDaemon: Added log: Diag

2025-05-28 08:21:31.991086 INFO AppDaemon: Using /conf/apps as app_dir

2025-05-28 08:21:31.991996 INFO AppDaemon: Loading Plugin HASS using class HassPlugin from module appdaemon.plugins.hass.hassplugin

2025-05-28 08:21:31.992225 INFO HASS: HASS Plugin initialization complete

2025-05-28 08:21:31.992421 INFO AppDaemon: Loading Plugin MQTT using class MqttPlugin from module appdaemon.plugins.mqtt.mqttplugin

2025-05-28 08:21:31.992579 INFO MQTT: MQTT Plugin Initializing

2025-05-28 08:21:31.992682 INFO MQTT: Using 'appdaemon_mqtt_client/status' as birth topic with payload 'online'

2025-05-28 08:21:31.992772 INFO MQTT: Using 'appdaemon_mqtt_client/status' as will topic with payload 'offline'

2025-05-28 08:21:31.996831 INFO AppDaemon: Initializing HTTP

2025-05-28 08:21:31.996995 INFO AppDaemon: Using 'ws' for event stream

2025-05-28 08:21:31.997249 INFO AppDaemon: HTTP Listening on port 5050

2025-05-28 08:21:31.999338 INFO AppDaemon: Starting API

2025-05-28 08:21:32.000499 INFO AppDaemon: Starting Admin Interface

2025-05-28 08:21:32.000654 INFO AppDaemon: Starting Dashboards

2025-05-28 08:21:32.009564 INFO HASS: Connected to Home Assistant 2025.5.3 with aiohttp websocket

2025-05-28 08:21:32.010281 INFO HASS: Authenticated to Home Assistant 2025.5.3

2025-05-28 08:21:32.011257 INFO HASS: Waiting for Home Assistant to start

2025-05-28 08:21:32.011583 INFO MQTT: Connected to MQTT broker at URL 192.168.1.191:1883 with paho-mqtt

2025-05-28 08:21:32.017891 INFO MQTT: MQTT Plugin initialization complete

2025-05-28 08:21:32.050830 INFO AppDaemon: Starting Apps with 8 workers and 8 pins

2025-05-28 08:21:32.054465 INFO AppDaemon: Running on port 5050

2025-05-28 08:21:32.054626 INFO AppDaemon: Waiting for plugins to be ready

2025-05-28 08:21:32.056067 INFO AppDaemon: All plugins ready

2025-05-28 08:21:32.057453 INFO AppDaemon: Scheduler running in realtime

2025-05-28 08:21:32.064480 WARNING HASS: Disconnected from Home Assistant, retrying in 5 seconds

2025-05-28 08:21:32.269105 INFO MQTT: Unable to decode MQTT message from topic home/OMG_lilygo_rtl_433_ESP_FSK/RTL_433toMQTT/Fineoffset-WH51/0dbd49, ignoring message

2025-05-28 08:21:32.269341 ERROR MQTT: Unable to decode MQTT message from topic home/OMG_lilygo_rtl_433_ESP_FSK/RTL_433toMQTT/Fineoffset-WH51/0dbd49, with error: 'utf-8' codec can't decode byte 0xd6 in position 247: invalid continuation byte

2025-05-28 08:21:32.511604 INFO AppDaemon: New app config: nachttisch_links

2025-05-28 08:21:32.511772 INFO AppDaemon: New app config: nachttisch_rechts

2025-05-28 08:21:32.511897 INFO AppDaemon: New app config: wohnzimmer_dim

2025-05-28 08:21:32.511989 INFO AppDaemon: New app config: flur_dim

2025-05-28 08:21:32.512088 INFO AppDaemon: New app config: kueche_dim

2025-05-28 08:21:32.512187 INFO AppDaemon: New app config: flur_dim2

2025-05-28 08:21:32.512271 INFO AppDaemon: New app config: esszimmer_dim

2025-05-28 08:21:32.512392 INFO AppDaemon: New app config: wohnzimmer_dim_vorhang

2025-05-28 08:21:32.668853 WARNING AppDaemon: ------------------------------------------------------------

2025-05-28 08:21:32.669062 WARNING AppDaemon: Unexpected error during run()

2025-05-28 08:21:32.669183 WARNING AppDaemon: ------------------------------------------------------------

Traceback (most recent call last):

  File "/usr/local/lib/python3.12/site-packages/appdaemon/__main__.py", line 157, in run

    loop.run_until_complete(asyncio.gather(*pending))

  File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete

    return future.result()

           ^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.12/site-packages/appdaemon/utility_loop.py", line 144, in loop

    await self.AD.app_management.check_app_updates(mode=UpdateMode.INIT)

  File "/usr/local/lib/python3.12/site-packages/appdaemon/app_management.py", line 809, in check_app_updates

    await self._import_modules(update_actions)

  File "/usr/local/lib/python3.12/site-packages/appdaemon/app_management.py", line 1055, in _import_modules

    load_order = self.dependency_manager.python_sort(modules)

                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.12/site-packages/appdaemon/dependency_manager.py", line 210, in python_sort

    order = [n for n in topo_sort(self.python_deps.dep_graph) if n in modules]

                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.12/site-packages/appdaemon/dependency.py", line 261, in topo_sort

    raise CircularDependency(f"Visited {visited} already, but {node} depends on {deps}")

appdaemon.dependency.CircularDependency: Visited ['cx_devices.sengled', 'cx_core', 'cx_core.type.z2m_light_controller', 'cx_const', 'collections.abc', 'typing', 'cx_core.controller', 'ast', 'appdaemon.adapi', 'time', 'cx_version', 'appdaemon.plugins.hass.hassapi', 'cx_core.action_type.base', 'abc', 'cx_core.integration', 'cx_helper', 'pkgutil', 'importlib', 'os', 'functools', 'appdaemon.plugins.mqtt.mqttapi', 'asyncio', 'cx_core.action_type', 'cx_core.action_type.delay_action_type', 'cx_core.action_type.call_service_action_type', 'cx_core.action_type.scene_action_type', 'cx_core.action_type.predefined_action_type', 'inspect', 're', 'collections', 'appdaemon.utils', 'cx_core.integration.z2m', 'json', 'cx_core.type_controller', 'cx_core.feature_support', 'cx_core.stepper', 'dataclasses', 'cx_core.type.cover_controller', 'cx_core.feature_support.cover', 'cx_core.type.media_player_controller', 'cx_core.stepper.index_loop_stepper', 'cx_core.stepper.stop_stepper', 'cx_core.feature_support.media_player', 'cx_core.release_hold_controller', 'cx_core.type.switch_controller', 'cx_core.type.light_controller', 'cx_core.stepper.bounce_stepper', 'cx_core.feature_support.light', 'cx_core.stepper.loop_stepper', 'cx_core.integration.deconz', 'cx_core.integration.zha', 'cx_core.color_helper'] already, but cx_devices.sengled depends on {'cx_core', 'cx_const'}

2025-05-28 08:21:32.671808 INFO AppDaemon: Previous message repeated 1 times

2025-05-28 08:21:32.671945 INFO AppDaemon: AppDaemon Exited

Additional Context

Add any other context or screenshots about the bug here.

fir3drag0n avatar May 28 '25 13:05 fir3drag0n

same here

Petervanoos avatar May 28 '25 19:05 Petervanoos

Hi @fir3drag0n ,

Thank you for opening this ticket. I am currently exploring what the issue is and if there is any workaround we can apply.

Regards,

xaviml avatar May 28 '25 19:05 xaviml

Hi @fir3drag0n,

Could you try adding the exclude_dirs in the appdaemon.yaml like this?

appdaemon:
  ....
  exclude_dirs:
      - cx_core
  plugins:
     ....  

xaviml avatar May 28 '25 19:05 xaviml

Tried it with exclude dir. AddOn is starting now.

But still a lot of errors:

2025-05-28 22:07:07.020969 ERROR Error:   AttributeError: module 'appdaemon.utils' has no attribute 'sync_wrapper'
2025-05-28 22:07:07.023435 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 1063, in safe_import
2025-05-28 22:07:07.023565 ERROR Error:       await self.import_module(module_name)
2025-05-28 22:07:07.023669 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 529, in wrapper
2025-05-28 22:07:07.023762 ERROR Error:       return await run_in_executor(self, func, *args, **kwargs)
2025-05-28 22:07:07.023846 ERROR Error:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:07:07.023926 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 554, in run_in_executor
2025-05-28 22:07:07.024020 ERROR Error:       return await future
2025-05-28 22:07:07.024129 ERROR Error:              ^^^^^^^^^^^^
2025-05-28 22:07:07.024507 ERROR Error:     File "/usr/lib/python3.12/concurrent/futures/thread.py", line 59, in run
2025-05-28 22:07:07.024598 ERROR Error:       result = self.fn(*self.args, **self.kwargs)
2025-05-28 22:07:07.024680 ERROR Error:                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:07:07.024756 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 695, in import_module
2025-05-28 22:07:07.024836 ERROR Error:       raise exc
2025-05-28 22:07:07.024912 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 687, in import_module
2025-05-28 22:07:07.024992 ERROR Error:       importlib.import_module(module_name)
2025-05-28 22:07:07.025083 ERROR Error:     File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
2025-05-28 22:07:07.025438 ERROR Error:       return _bootstrap._gcd_import(name[level:], package, level)
2025-05-28 22:07:07.025535 ERROR Error:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:07:07.025608 ERROR Error:     File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
2025-05-28 22:07:07.025686 ERROR Error:     File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
2025-05-28 22:07:07.025760 ERROR Error:     File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
2025-05-28 22:07:07.025834 ERROR Error:     File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
2025-05-28 22:07:07.025914 ERROR Error:     File "<frozen importlib._bootstrap_external>", line 999, in exec_module
2025-05-28 22:07:07.025988 ERROR Error:     File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
2025-05-28 22:07:07.026078 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/controllerx.py", line 7, in <module>
2025-05-28 22:07:07.026576 ERROR Error:       from cx_core import (
2025-05-28 22:07:07.026671 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/__init__.py", line 1, in <module>
2025-05-28 22:07:07.026759 ERROR Error:       from cx_core.controller import Controller, action
2025-05-28 22:07:07.026848 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 65, in <module>
2025-05-28 22:07:07.026928 ERROR Error:       class Controller(Hass, Mqtt):  # type: ignore[misc]
2025-05-28 22:07:07.027171 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 350, in Controller
2025-05-28 22:07:07.027308 ERROR Error:       @utils.sync_wrapper  # type: ignore[misc]
2025-05-28 22:07:07.027411 ERROR Error:        ^^^^^^^^^^^^^^^^^^
2025-05-28 22:07:07.027546 ERROR Error: ===========================================================================
2025-05-28 22:07:07.028393 WARNING AppDaemon: Failed to start apps: {'wz_dimmer'}
2025-05-28 22:07:07.029943 INFO AppDaemon: App initialization complete
2025-05-28 22:07:20.564846 INFO AppDaemon: New client Admin Client connected
2025-05-28 22:09:01.201535 INFO AppDaemon: Client disconnection from Admin Client
Missing return statement on request handler
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/aiohttp/web_protocol.py", line 698, in finish_response
    prepare_meth = resp.prepare
                   ^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'prepare'
s6-rc: info: service legacy-services: stopping
s6-rc: info: service legacy-services successfully stopped
s6-rc: info: service appdaemon: stopping
2025-05-28 22:11:06.741398 INFO AppDaemon: SIGTERM Received
2025-05-28 22:11:06.741613 INFO AppDaemon: AppDaemon is shutting down
2025-05-28 22:11:06.741834 INFO MQTT: Stopping MQTT Plugin and Unsubscribing from URL core-mosquitto:1883
2025-05-28 22:11:06.746082 INFO HASS: Disconnecting from Home Assistant
2025-05-28 22:11:07.723702 INFO AppDaemon: Shutting down webserver
2025-05-28 22:11:07.724395 INFO AppDaemon: Saving all namespaces
2025-05-28 22:11:07.724646 INFO AppDaemon: AppDaemon is stopped.
[22:11:07] INFO: Service AppDaemon exited with code 0 (by signal 0)
s6-rc: info: service appdaemon successfully stopped
s6-rc: info: service init-appdaemon: stopping
s6-rc: info: service init-appdaemon successfully stopped
s6-rc: info: service legacy-cont-init: stopping
s6-rc: info: service legacy-cont-init successfully stopped
s6-rc: info: service fix-attrs: stopping
s6-rc: info: service base-addon-log-level: stopping
s6-rc: info: service fix-attrs successfully stopped
s6-rc: info: service base-addon-log-level successfully stopped
s6-rc: info: service base-addon-banner: stopping
s6-rc: info: service base-addon-banner successfully stopped
s6-rc: info: service s6rc-oneshot-runner: stopping
s6-rc: info: service s6rc-oneshot-runner successfully stopped
s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service base-addon-banner: starting
-----------------------------------------------------------
 Add-on: AppDaemon
 Python Apps and Dashboard using AppDaemon 4.x for Home Assistant
-----------------------------------------------------------
 Add-on version: 0.17.2
 You are running the latest version of this add-on.
 System: Home Assistant OS 15.2  (amd64 / qemux86-64)
 Home Assistant Core: 2025.5.3
 Home Assistant Supervisor: 2025.05.3
-----------------------------------------------------------
 Please, share the above information when looking for help
 or support in, e.g., GitHub, forums or the Discord chat.
-----------------------------------------------------------
s6-rc: info: service base-addon-banner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service base-addon-log-level: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service base-addon-log-level successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service init-appdaemon: starting
s6-rc: info: service init-appdaemon successfully started
s6-rc: info: service appdaemon: starting
s6-rc: info: service appdaemon successfully started
s6-rc: info: service legacy-services: starting
[22:11:41] INFO: Starting AppDaemon...
s6-rc: info: service legacy-services successfully started
2025-05-28 22:11:44.184801 INFO AppDaemon: ------------------------------------------------------------
2025-05-28 22:11:44.185293 INFO AppDaemon: AppDaemon Version 4.5.3 starting
2025-05-28 22:11:44.185420 INFO AppDaemon: ------------------------------------------------------------
2025-05-28 22:11:44.185539 INFO AppDaemon: Python version is 3.12.10
2025-05-28 22:11:44.185814 INFO AppDaemon: Configuration read from: /config/appdaemon.yaml
2025-05-28 22:11:44.186020 INFO AppDaemon: Added log: AppDaemon
2025-05-28 22:11:44.186145 INFO AppDaemon: Added log: Error
2025-05-28 22:11:44.186250 INFO AppDaemon: Added log: Access
2025-05-28 22:11:44.186353 INFO AppDaemon: Added log: Diag
2025-05-28 22:11:44.186987 INFO AppDaemon: Using /homeassistant/appdaemon/apps as app_dir
2025-05-28 22:11:44.189575 INFO AppDaemon: Loading Plugin HASS using class HassPlugin from module appdaemon.plugins.hass.hassplugin
2025-05-28 22:11:44.189860 INFO HASS: HASS Plugin initialization complete
2025-05-28 22:11:44.190084 INFO AppDaemon: Loading Plugin MQTT using class MqttPlugin from module appdaemon.plugins.mqtt.mqttplugin
2025-05-28 22:11:44.190295 INFO MQTT: MQTT Plugin Initializing
2025-05-28 22:11:44.190445 INFO MQTT: Using 'appdaemon_mqtt_client/status' as birth topic with payload 'online'
2025-05-28 22:11:44.190591 INFO MQTT: Using 'appdaemon_mqtt_client/status' as will topic with payload 'offline'
2025-05-28 22:11:44.193618 INFO AppDaemon: Initializing HTTP
2025-05-28 22:11:44.193833 INFO AppDaemon: Using 'ws' for event stream
2025-05-28 22:11:44.194107 INFO AppDaemon: HTTP Listening on port 5050
2025-05-28 22:11:44.195660 INFO AppDaemon: Starting API
2025-05-28 22:11:44.196942 INFO AppDaemon: Starting Admin Interface
2025-05-28 22:11:44.197209 INFO AppDaemon: Starting Dashboards
2025-05-28 22:11:44.206136 INFO HASS: Connected to Home Assistant 2025.5.3 with aiohttp websocket
2025-05-28 22:11:44.209368 INFO HASS: Authenticated to Home Assistant 2025.5.3
2025-05-28 22:11:44.212218 INFO HASS: Waiting for Home Assistant to start
2025-05-28 22:11:44.225686 INFO AppDaemon: Starting Apps with 1 workers and 1 pins
2025-05-28 22:11:44.230560 INFO AppDaemon: Running on port 5050
2025-05-28 22:11:44.232879 INFO AppDaemon: Waiting for plugins to be ready
2025-05-28 22:11:44.254466 INFO HASS: Completed initialization in 55.796ms
2025-05-28 22:11:44.288356 INFO MQTT: Connected to MQTT broker at URL core-mosquitto:1883 with paho-mqtt
2025-05-28 22:11:44.724205 INFO MQTT: MQTT Plugin initialization complete
2025-05-28 22:11:44.724612 INFO AppDaemon: All plugins ready
2025-05-28 22:11:44.725305 INFO AppDaemon: Scheduler running in realtime
2025-05-28 22:11:44.784528 INFO AppDaemon: New app config: wz_dimmer
2025-05-28 22:11:44.861589 ERROR Error: =====  Error importing 'controllerx'  =====================================
2025-05-28 22:11:44.863794 ERROR Error: FailedImport: Failed to import 'controllerx'
2025-05-28 22:11:44.864857 ERROR Error:   AttributeError: module 'appdaemon.utils' has no attribute 'sync_wrapper'
2025-05-28 22:11:44.867023 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 1063, in safe_import
2025-05-28 22:11:44.867454 ERROR Error:       await self.import_module(module_name)
2025-05-28 22:11:44.867783 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 529, in wrapper
2025-05-28 22:11:44.868131 ERROR Error:       return await run_in_executor(self, func, *args, **kwargs)
2025-05-28 22:11:44.868530 ERROR Error:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:11:44.868883 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 554, in run_in_executor
2025-05-28 22:11:44.870211 ERROR Error:       return await future
2025-05-28 22:11:44.870305 ERROR Error:              ^^^^^^^^^^^^
2025-05-28 22:11:44.870384 ERROR Error:     File "/usr/lib/python3.12/concurrent/futures/thread.py", line 59, in run
2025-05-28 22:11:44.870473 ERROR Error:       result = self.fn(*self.args, **self.kwargs)
2025-05-28 22:11:44.870549 ERROR Error:                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:11:44.870623 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 695, in import_module
2025-05-28 22:11:44.870694 ERROR Error:       raise exc
2025-05-28 22:11:44.870764 ERROR Error:     File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 687, in import_module
2025-05-28 22:11:44.870833 ERROR Error:       importlib.import_module(module_name)
2025-05-28 22:11:44.870904 ERROR Error:     File "/usr/lib/python3.12/importlib/__init__.py", line 90, in import_module
2025-05-28 22:11:44.870971 ERROR Error:       return _bootstrap._gcd_import(name[level:], package, level)
2025-05-28 22:11:44.871036 ERROR Error:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-05-28 22:11:44.871103 ERROR Error:     File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
2025-05-28 22:11:44.872024 ERROR Error:     File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
2025-05-28 22:11:44.872247 ERROR Error:     File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
2025-05-28 22:11:44.872325 ERROR Error:     File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
2025-05-28 22:11:44.872392 ERROR Error:     File "<frozen importlib._bootstrap_external>", line 999, in exec_module
2025-05-28 22:11:44.872457 ERROR Error:     File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
2025-05-28 22:11:44.872522 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/controllerx.py", line 7, in <module>
2025-05-28 22:11:44.872589 ERROR Error:       from cx_core import (
2025-05-28 22:11:44.872657 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/__init__.py", line 1, in <module>
2025-05-28 22:11:44.872723 ERROR Error:       from cx_core.controller import Controller, action
2025-05-28 22:11:44.872789 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 65, in <module>
2025-05-28 22:11:44.872852 ERROR Error:       class Controller(Hass, Mqtt):  # type: ignore[misc]
2025-05-28 22:11:44.872915 ERROR Error:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 350, in Controller
2025-05-28 22:11:44.872978 ERROR Error:       @utils.sync_wrapper  # type: ignore[misc]
2025-05-28 22:11:44.873042 ERROR Error:        ^^^^^^^^^^^^^^^^^^
2025-05-28 22:11:44.873112 ERROR Error: ===========================================================================
2025-05-28 22:11:44.873755 WARNING AppDaemon: Failed to start apps: {'wz_dimmer'}
2025-05-28 22:11:44.873925 INFO AppDaemon: App initialization complete

i4mr000t avatar May 28 '25 20:05 i4mr000t

Hi @i4mr000t ,

Could you go to this line within your instance:

https://github.com/xaviml/controllerx/blob/main/apps/controllerx/cx_core/controller.py#L350

And change it for:

@utils.sync_decorator

Somehow, I cannot install the new addon version for some reason, this is why I am asking you to try it out.

EDIT: This is the reason why I cannot install new AppDaemon addon: https://www.home-assistant.io/blog/2025/05/22/deprecating-core-and-supervised-installation-methods-and-32-bit-systems/

I have to install HA again.

P.S.: keep the exclude_dirs as well.

Thanks,

xaviml avatar May 28 '25 21:05 xaviml

Hi @i4mr000t ,

Could you go to this line within your instance:

https://github.com/xaviml/controllerx/blob/main/apps/controllerx/cx_core/controller.py#L350

And change it for:

@utils.sync_decorator P.S.: keep the exclude_dirs as well.

Confirmed working.

meetyourlaser avatar May 29 '25 01:05 meetyourlaser

Thank you @meetyourlaser ,

I will release new code for the sync_decorator, and mention in documentation about exclude_dirs. This would do as a workaround, and I will try to find a solution that would not imply adding the exclude_dirs.

Thanks, Xavi M.

xaviml avatar May 29 '25 07:05 xaviml

I'm running AppDaemon 4.5.3, tried the above fix (modifying line 350 and adding exclude_dirs) and this was going to be a "doesn't work for me", but double checking things I had two appdaemon.yaml files, so happy to report this works (when modifying the correct one). ๐Ÿ‘

charrus avatar May 29 '25 16:05 charrus

Hi @charrus ,

Thank you for reporting this. I am still experiencing issues when dimming lights, but I have to debug further.

Does everything work for you?

Thanks,

xaviml avatar May 29 '25 16:05 xaviml

Hi @charrus ,

Thank you for reporting this. I am still experiencing issues when dimming lights, but I have to debug further.

Does everything work for you?

Thanks,

Sadly, I hadn't tested it worked, and nothing was working. Once I rolled back to 4.4.2 (0.16.7) I noticed compile errors on my controllers. Once I rolled back the controller.py change, everything was working again,

I'll have a more thorough look tomorrow and report back.

charrus avatar May 29 '25 18:05 charrus

I edited both appdaemon.yaml and controller.py, and it's starting now, but none of my switches work. I don't know how to roll back to 4.4.2. Do I need to compile the addon myself?

zillion42 avatar May 30 '25 01:05 zillion42

Hi @charrus ,

Thank you for reporting this. I am still experiencing issues when dimming lights, but I have to debug further.

Does everything work for you?

Thanks,

@xaviml Can confirm your changes work ๐Ÿ‘๐Ÿผ however like you mentioned I also cannot dim, but at least turning them on/off is working again! Thanks for the work so far!!!

JDFS404 avatar May 30 '25 06:05 JDFS404

I have to install HA again.

Hi @xaviml Don't worry about the reinstall, the backup function and restore works like a charm, no problems there :-) Do you have the HW for it?

fribse avatar May 30 '25 07:05 fribse

Hi @fribse ,

I was able to install HA again with new CPU architecture, so all good on that side. I was then able to update AppDaemon and see everything failing.

Until this is further debugged, I do not recommend anyone to upgrade to latest AppDaemon addon version.

Regards, Xavi M.

xaviml avatar May 30 '25 07:05 xaviml

+1 one here as well, Version 4.29 from HACS.

2025-05-30 15:01:27.441920 WARNING AppDaemon: ------------------------------------------------------------ 2025-05-30 15:01:27.442281 WARNING AppDaemon: Unexpected error during run() 2025-05-30 15:01:27.447654 WARNING AppDaemon: ------------------------------------------------------------ Traceback (most recent call last): File "/usr/local/lib/python3.12/site-packages/appdaemon/__main__.py", line 157, in run loop.run_until_complete(asyncio.gather(*pending)) File "/usr/local/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete return future.result() ^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/appdaemon/utility_loop.py", line 144, in loop await self.AD.app_management.check_app_updates(mode=UpdateMode.INIT) File "/usr/local/lib/python3.12/site-packages/appdaemon/app_management.py", line 812, in check_app_updates await self._import_modules(update_actions) File "/usr/local/lib/python3.12/site-packages/appdaemon/app_management.py", line 1058, in _import_modules load_order = self.dependency_manager.python_sort(modules) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/appdaemon/dependency_manager.py", line 210, in python_sort order = [n for n in topo_sort(self.python_deps.dep_graph) if n in modules] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/appdaemon/dependency.py", line 261, in topo_sort raise CircularDependency(f"Visited {visited} already, but {node} depends on {deps}") appdaemon.dependency.CircularDependency: Visited ['cx_devices.sengled', 'cx_core', 'cx_core.type.light_controller', 'cx_const', 'typing', 'collections.abc', 'cx_core.integration.deconz', 'cx_core.integration', 'abc', 'cx_helper', 'pkgutil', 'importlib', 'os', 'appdaemon.plugins.hass.hassapi', 'cx_core.integration.z2m', 'appdaemon.plugins.mqtt.mqttapi', 'json', 'cx_core.integration.zha', 'cx_core.color_helper', 'cx_core.release_hold_controller', 'cx_core.controller', 'time', 'ast', 'functools', 're', 'asyncio', 'cx_version', 'cx_core.action_type.base', 'appdaemon.utils', 'appdaemon.adapi', 'cx_core.action_type', 'cx_core.action_type.predefined_action_type', 'inspect', 'cx_core.action_type.scene_action_type', 'cx_core.action_type.delay_action_type', 'cx_core.action_type.call_service_action_type', 'collections', 'cx_core.stepper', 'dataclasses', 'cx_core.type_controller', 'cx_core.feature_support', 'cx_core.stepper.index_loop_stepper', 'cx_core.feature_support.light', 'cx_core.stepper.stop_stepper', 'cx_core.stepper.bounce_stepper', 'cx_core.stepper.loop_stepper', 'cx_core.type.cover_controller', 'cx_core.feature_support.cover', 'cx_core.type.media_player_controller', 'cx_core.feature_support.media_player', 'cx_core.type.z2m_light_controller', 'cx_core.type.switch_controller'] already, but cx_devices.sengled depends on {'cx_core', 'cx_const'} 2025-05-30 15:01:27.448160 INFO AppDaemon: Previous message repeated 1 times 2025-05-30 15:01:27.448415 INFO AppDaemon: AppDaemon Exited

FauthD avatar May 30 '25 12:05 FauthD

@xaviml Can confirm your changes work ๐Ÿ‘๐Ÿผ however like you mentioned I also cannot dim, but at least turning them on/off is working again! Thanks for the work so far!!!

Not for me, can not turn on or off a single switch, whole house is not working, since yesterday. Maybe because I use z2m with listen_to: mqtt ? I would like to restore AppDaemon 4..4.2 (0.16.7) but I don't know how. I tried installing it as a local package but it keeps failing the build. Can anyone help me please. How to restore the old Version of AppDaemon?

EDIT: I finally managed.

Image

zillion42 avatar May 30 '25 13:05 zillion42

@xaviml I've still got my HA core development environment still from when I submitted a PR for the kodi component; let me know if I can help at all.

Edit: Like @zillion42 I use z2m with listen_to mqtt, and also experience on/off not working.

charrus avatar May 30 '25 13:05 charrus

Thank you @charrus ,

I believe the issue need to be debugged with AppDaemon more than HA since AppDaemon bumped its version and everything started failing.

I will not be able to keep debugging this issue until next week unfortunately, but if anyone finds anything, please, let me know.

Thanks, Xavi M.

xaviml avatar May 30 '25 13:05 xaviml

This mitigates the issue for me (using z2m and mqtt) so turning on/off and dimming work:

In controller.py, change:

    @utils.sync_wrapper  # type: ignore[misc]
    async def get_state(
        self,
        entity_id: Optional[str] = None,
        attribute: Optional[str] = None,
        default: Any = None,
        copy: bool = True,
        **kwargs: Any,
    ) -> Optional[Any]:
        rendered_entity_id = await self.render_value(entity_id)
        return await super().get_state(
            rendered_entity_id, attribute, default, copy, **kwargs
        )

to:

    @utils.sync_decorator
    async def get_state(
        self,
        entity_id: str | None = None,
        attribute: str | None = None,
        default: Any | None = None,
        copy: bool = True,
    ) -> Optional[Any]:
        rendered_entity_id = await self.render_value(entity_id)
        return await super().get_state(rendered_entity_id, attribute, default, copy)

And also add to appdaemon.yaml under appdaemon:

  exclude_dirs:
    - cx_core

Or - tl;dr version:

Change the decorator to @utils.sync_decorator and remove **kwargs from get_state in controllerx/apps/controllerx/cx_core/controller.py.

Exclude cx_core in appdaemon.yaml as before.

Edit: I've got a draft PR: https://github.com/xaviml/controllerx/pull/1188 if that helps anyone.

Second Edit: Changed "I think this mitigates" to "This mitigates" after annoying my partner and checking all light switches and dimmers. The fix will be to fix the circular dependency (new checks were introduced in https://github.com/AppDaemon/appdaemon/commit/bb98fd1fda765ad8f8e676286703c3e248d79c9e) as stated in https://github.com/xaviml/controllerx/issues/1184#issuecomment-2918568424. Had a look myself, but it looks like a more complex change.

Third Edit: ~~Something is not quite right - it maybe my fiddling after this patch, or more work needs to be done, but it seems to have rendered HA a little funky (AddOns setting page not loading),~~ Unrelated - this is a separate issue.

charrus avatar May 30 '25 17:05 charrus

Thank you @charrus ,

I will checkout your branch next Monday and check on my setup.

In my local I got same changes as you, but without the kwargs from get_state, so I will check if the rest is working.

Regards,

xaviml avatar May 30 '25 17:05 xaviml

Or - tl;dr version:

Change the decorator to @utils.sync_decorator and remove **kwargs from get_state in controllerx/apps/controllerx/cx_core/controller.py.

Exclude cx_core in appdaemon.yaml as before.

This does not work for me.

I edited:

/root/addon_configs/a0d7b954_appdaemon/apps/controllerx/cx_core/controller.py

and

/root/addon_configs/a0d7b954_appdaemon/appdaemon.yaml

I'm using ZHA.

[10:58:15] INFO: Starting AppDaemon...
2025-06-01 10:58:26.850818 INFO AppDaemon: ------------------------------------------------------------
2025-06-01 10:58:26.852169 INFO AppDaemon: AppDaemon Version 4.5.3 starting
2025-06-01 10:58:26.852618 INFO AppDaemon: ------------------------------------------------------------
2025-06-01 10:58:26.852989 INFO AppDaemon: Python version is 3.12.10
2025-06-01 10:58:26.854370 INFO AppDaemon: Configuration read from: /config/appdaemon.yaml
2025-06-01 10:58:26.855056 INFO AppDaemon: Added log: AppDaemon
2025-06-01 10:58:26.855465 INFO AppDaemon: Added log: Error
2025-06-01 10:58:26.855820 INFO AppDaemon: Added log: Access
2025-06-01 10:58:26.856153 INFO AppDaemon: Added log: Diag
2025-06-01 10:58:26.858790 INFO AppDaemon: Using /config/apps as app_dir
2025-06-01 10:58:26.869771 INFO AppDaemon: Loading Plugin HASS using class HassPlugin from module appdaemon.plugins.hass.hassplugin
2025-06-01 10:58:26.870564 INFO HASS: HASS Plugin initialization complete
2025-06-01 10:58:26.882907 INFO AppDaemon: Initializing HTTP
2025-06-01 10:58:26.883913 INFO AppDaemon: Using 'ws' for event stream
2025-06-01 10:58:26.884925 INFO AppDaemon: HTTP Listening on port 5050
2025-06-01 10:58:26.891324 INFO AppDaemon: Starting API
2025-06-01 10:58:26.896492 INFO AppDaemon: Starting Admin Interface
2025-06-01 10:58:26.897306 INFO AppDaemon: Starting Dashboards
2025-06-01 10:58:26.952133 INFO HASS: Connected to Home Assistant 2025.5.3 with aiohttp websocket
2025-06-01 10:58:26.972736 INFO HASS: Authenticated to Home Assistant 2025.5.3
2025-06-01 10:58:26.987791 INFO AppDaemon: Starting Apps with 7 workers and 7 pins
2025-06-01 10:58:26.996509 INFO HASS: Waiting for Home Assistant to start
2025-06-01 10:58:27.000235 INFO AppDaemon: Running on port 5050
2025-06-01 10:58:27.001138 INFO AppDaemon: Waiting for plugins to be ready
2025-06-01 10:58:27.013600 INFO AppDaemon: All plugins ready
2025-06-01 10:58:27.016403 INFO AppDaemon: Scheduler running in realtime
2025-06-01 10:58:27.091047 INFO HASS: Completed initialization in 188ms
2025-06-01 10:58:27.437649 INFO AppDaemon: New app config: terassen_lampe
2025-06-01 10:58:27.438390 INFO AppDaemon: New app config: kuechen_lampen
2025-06-01 10:58:27.439145 INFO AppDaemon: New app config: dachboden_lampe
2025-06-01 10:58:27.439783 INFO AppDaemon: New app config: wohnzimmer_lampen
2025-06-01 10:58:27.679370 WARNING AppDaemon: ------------------------------------------------------------
2025-06-01 10:58:27.681207 WARNING AppDaemon: Unexpected error during run()
2025-06-01 10:58:27.681952 WARNING AppDaemon: ------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/appdaemon/__main__.py", line 157, in run
    loop.run_until_complete(asyncio.gather(*pending))
  File "/usr/lib/python3.12/asyncio/base_events.py", line 691, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/appdaemon/utility_loop.py", line 144, in loop
    await self.AD.app_management.check_app_updates(mode=UpdateMode.INIT)
  File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 809, in check_app_updates
    await self._import_modules(update_actions)
  File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 1055, in _import_modules
    load_order = self.dependency_manager.python_sort(modules)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/appdaemon/dependency_manager.py", line 210, in python_sort
    order = [n for n in topo_sort(self.python_deps.dep_graph) if n in modules]
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/appdaemon/dependency.py", line 261, in topo_sort
    raise CircularDependency(f"Visited {visited} already, but {node} depends on {deps}")
appdaemon.dependency.CircularDependency: Visited ['cx_devices.terncy', 'cx_core', 'cx_core.type.media_player_controller', 'cx_core.stepper.index_loop_stepper', 'cx_core.stepper', 'cx_const', 'typing', 'abc', 'dataclasses', 'cx_core.stepper.stop_stepper', 'cx_core.integration', 'cx_helper', 'os', 'pkgutil', 'importlib', 'cx_core.type_controller', 'cx_core.feature_support', 'cx_core.controller', 'appdaemon.plugins.hass.hassapi', 'ast', 'cx_version', 'functools', 'appdaemon.utils', 'appdaemon.adapi', 'time', 'cx_core.action_type', 'cx_core.action_type.scene_action_type', 'cx_core.action_type.base', 'cx_core.action_type.predefined_action_type', 'inspect', 'cx_core.action_type.delay_action_type', 'cx_core.action_type.call_service_action_type', 're', 'asyncio', 'collections', 'appdaemon.plugins.mqtt.mqttapi', 'cx_core.release_hold_controller', 'cx_core.integration.z2m', 'json', 'cx_core.feature_support.media_player', 'cx_core.type.cover_controller', 'cx_core.feature_support.cover', 'cx_core.type.light_controller', 'cx_core.integration.deconz', 'cx_core.stepper.bounce_stepper', 'cx_core.feature_support.light', 'cx_core.integration.zha', 'cx_core.color_helper', 'cx_core.stepper.loop_stepper', 'cx_core.type.switch_controller', 'cx_core.type.z2m_light_controller'] already, but cx_devices.terncy depends on {'cx_core', 'cx_const', 'cx_core.integration'}
2025-06-01 10:58:27.694731 INFO AppDaemon: Previous message repeated 1 times
2025-06-01 10:58:27.695315 INFO AppDaemon: AppDaemon Exited
[10:58:28] INFO: Service AppDaemon exited with code 0 (by signal 0)

mietzen avatar Jun 01 '25 09:06 mietzen

Rolling back to appdaemon addon v0.16.7 using HA backups (and disabling auto update) worked around the issue. Subscribing to this issue to know when it's properly fixed.

Crocmagnon avatar Jun 01 '25 09:06 Crocmagnon

@mietzen That looks like the first issue I ran into - and that's editing the wrong appdaemon.yaml.

If it's running in a docker container, then run:

docker inspect <container_name>

And look for the /config bind mount - for me, it's:

            {                                                     
                "Type": "bind",                                                                                                     
                "Source": "/usr/share/hassio/addon_configs/a0d7b954_appdaemon",                                                     
                "Destination": "/config",                         
                "Mode": "",                                       
                "RW": true,                                                                                                         
                "Propagation": "rprivate"                         
            },   

So it's /usr/share/hassio/addon_configs/a0d7b954_appdaemon/appdaemon.yaml.

And from your log:

2025-06-01 10:58:26.858790 INFO AppDaemon: Using /config/apps as app_dir

That's also be where controllerx is installed.

Something else to confirm is to change something innocuous in appdaermon.yaml, for example, adding to http:

http:
...
    transport: socketio

And confirm that:

2025-06-01 10:58:26.883913 INFO AppDaemon: Using 'ws' for event stream

has changed to:

2025-06-01 10:29:45.378183 INFO AppDaemon: Using 'socketio' for event stream

Or, as @Crocmagnon said, just rollback and wait for the fix where you don't have to modify appdaemoin.yaml to exclude cx_core.

charrus avatar Jun 01 '25 09:06 charrus

I directly looked into the container

docker exec -it addon_a0d7b954_appdaemon sh
root@a0d7b954-appdaemon:/$ cat config/appdaemon.yaml
---
secrets: /homeassistant/secrets.yaml
appdaemon:
  latitude: 0
  longitude: 0
  elevation: 30
  time_zone: Europe/Berlin
  plugins:
    HASS:
      type: hass
      token: !env_var SUPERVISOR_TOKEN
http:
  url: http://0.0.0.0:5050
admin:
api:
hadashboard:
exclude_dirs:
  - cx_core
root@a0d7b954-appdaemon:/$ cat /config/apps/controllerx/cx_core/controller.py
import asyncio
import re
import time
from ast import literal_eval
from asyncio import CancelledError, Task
from collections import defaultdict
from functools import wraps
from typing import (
    Any,
    Awaitable,
    Callable,
    Counter,
    DefaultDict,
    Dict,
    List,
    Optional,
    Set,
    Tuple,
    TypeVar,
    Union,
    overload,
)

import appdaemon.utils as utils
import cx_version
from appdaemon.adapi import ADAPI
from appdaemon.plugins.hass.hassapi import Hass
from appdaemon.plugins.mqtt.mqttapi import Mqtt
from cx_const import (
    ActionEvent,
    ActionFunction,
    CustomActionsMapping,
    DefaultActionsMapping,
    PredefinedActionsMapping,
)
from cx_core import integration as integration_module
from cx_core.action_type import ActionsMapping, parse_actions
from cx_core.action_type.base import ActionType
from cx_core.integration import EventData, Integration

Service = Tuple[str, Dict]
Services = List[Service]


DEFAULT_ACTION_DELTA = 300  # In milliseconds
DEFAULT_MULTIPLE_CLICK_DELAY = 500  # In milliseconds
MULTIPLE_CLICK_TOKEN = "$"

MODE_SINGLE = "single"
MODE_RESTART = "restart"
MODE_QUEUED = "queued"
MODE_PARALLEL = "parallel"

T = TypeVar("T")


def action(method: Callable[..., Awaitable[Any]]) -> ActionFunction:
    @wraps(method)
    async def _action_impl(controller: "Controller", *args: Any, **kwargs: Any) -> None:
        continue_call = await controller.before_action(method.__name__, *args, **kwargs)
        if continue_call:
            await method(controller, *args, **kwargs)

    return _action_impl


def run_in(fn: Callable[..., Any], delay: float, **kwargs: Any) -> "Task[None]":
    """
    It runs the function (fn) in running event loop in `delay` seconds.
    This function has been created because the default run_in function
    from AppDaemon does not accept microseconds.
    """

    async def inner() -> None:
        await asyncio.sleep(delay)
        await fn(kwargs)

    task = asyncio.create_task(inner())
    return task


class Controller(Hass, Mqtt):  # type: ignore[misc]
    """
    This is the parent Controller, all controllers must extend from this class.
    """

    args: Dict[str, Any]
    integration: Integration
    actions_mapping: ActionsMapping
    action_handles: DefaultDict[ActionEvent, Optional["Task[None]"]]
    action_delay_handles: Dict[ActionEvent, Optional[str]]
    multiple_click_actions: Set[ActionEvent]
    action_delay: Dict[ActionEvent, int]
    action_delta: Dict[ActionEvent, int]
    action_times: Dict[str, float]
    previous_states: Dict[ActionEvent, Optional[str]]
    multiple_click_action_times: Dict[str, float]
    click_counter: Counter[ActionEvent]
    multiple_click_action_delay_tasks: DefaultDict[ActionEvent, Optional["Task[None]"]]
    multiple_click_delay: int

    async def initialize(self) -> None:
        self.log(f"๐ŸŽฎ ControllerX {cx_version.__version__}", ascii_encode=False)
        await self.init()

    async def init(self) -> None:
        controllers_ids: List[str] = self.get_list(self.args["controller"])
        self.integration = self.get_integration(self.args["integration"])

        if "mapping" in self.args and "merge_mapping" in self.args:
            raise ValueError("`mapping` and `merge_mapping` cannot be used together")

        custom_mapping: Optional[CustomActionsMapping] = self.args.get("mapping", None)
        merge_mapping: Optional[CustomActionsMapping] = self.args.get(
            "merge_mapping", None
        )

        if custom_mapping is None:
            default_actions_mapping = self.get_default_actions_mapping(self.integration)
            self.actions_mapping = self.parse_action_mapping(default_actions_mapping)  # type: ignore[arg-type]
        else:
            self.actions_mapping = self.parse_action_mapping(custom_mapping)

        if merge_mapping is not None:
            self.actions_mapping.update(self.parse_action_mapping(merge_mapping))

        # Filter actions with include and exclude
        if "actions" in self.args and "excluded_actions" in self.args:
            raise ValueError("`actions` and `excluded_actions` cannot be used together")
        include: List[ActionEvent] = self.get_list(
            self.args.get("actions", list(self.actions_mapping.keys()))
        )
        exclude: List[ActionEvent] = self.get_list(
            self.args.get("excluded_actions", [])
        )
        self.actions_mapping = self.filter_actions(
            self.actions_mapping, set(include), set(exclude)
        )

        # Action delay
        self.action_delay = self.get_mapping_per_action(
            self.actions_mapping,
            custom=self.args.get("action_delay"),
            default=0,
        )
        self.action_delay_handles = defaultdict(lambda: None)
        self.action_handles = defaultdict(lambda: None)

        # Action delta
        self.action_delta = self.get_mapping_per_action(
            self.actions_mapping,
            custom=self.args.get("action_delta"),
            default=DEFAULT_ACTION_DELTA,
        )
        self.action_times = defaultdict(lambda: 0.0)

        # Previous state
        self.previous_states = self.get_mapping_per_action(
            self.actions_mapping,
            custom=self.args.get("previous_state"),
            default=None,
        )

        # Multiple click
        self.multiple_click_actions = self.get_multiple_click_actions(
            self.actions_mapping
        )
        self.multiple_click_delay = self.args.get(
            "multiple_click_delay", DEFAULT_MULTIPLE_CLICK_DELAY
        )
        self.multiple_click_action_times = defaultdict(lambda: 0.0)
        self.click_counter = Counter()
        self.multiple_click_action_delay_tasks = defaultdict(lambda: None)

        # Mode
        self.mode = self.get_mapping_per_action(
            self.actions_mapping, custom=self.args.get("mode"), default=MODE_SINGLE
        )

        # Listen for device changes
        for controller_id in controllers_ids:
            await self.integration.listen_changes(controller_id)

    def filter_actions(
        self,
        actions_mapping: ActionsMapping,
        include: Set[ActionEvent],
        exclude: Set[ActionEvent],
    ) -> ActionsMapping:
        allowed_actions = include - exclude
        return {
            key: value
            for key, value in actions_mapping.items()
            if key in allowed_actions
        }

    @staticmethod
    def get_option(value: str, options: List[str], ctx: Optional[str] = None) -> str:
        if value in options:
            return value
        else:
            raise ValueError(
                f"{f'{ctx} - ' if ctx is not None else ''}`{value}` is not an option. "
                f"The options are {options}"
            )

    def parse_integration(
        self, integration: Union[str, Dict[str, Any], Any]
    ) -> Dict[str, str]:
        if isinstance(integration, str):
            return {"name": integration}
        elif isinstance(integration, dict):
            if "name" in integration:
                return integration
            else:
                raise ValueError("'name' attribute is mandatory")
        else:
            raise ValueError(
                f"Type {type(integration)} is not supported for `integration` attribute"
            )

    def get_integration(self, integration: Union[str, Dict[str, Any]]) -> Integration:
        parsed_integration = self.parse_integration(integration)
        kwargs = {k: v for k, v in parsed_integration.items() if k != "name"}
        integrations = integration_module.get_integrations(self, kwargs)
        integration_argument = self.get_option(
            parsed_integration["name"], [i.name for i in integrations]
        )
        return next(i for i in integrations if i.name == integration_argument)

    def get_default_actions_mapping(
        self, integration: Integration
    ) -> DefaultActionsMapping:
        actions_mapping = integration.get_default_actions_mapping()
        if actions_mapping is None:
            raise ValueError(f"This controller does not support {integration.name}.")
        return actions_mapping

    @overload
    def get_list(self, entities: List[T]) -> List[T]:
        ...

    @overload
    def get_list(self, entities: T) -> List[T]:
        ...

    def get_list(self, entities: Union[List[T], T]) -> List[T]:
        if isinstance(entities, (list, tuple)):
            return list(entities)
        return [entities]

    @overload
    def get_mapping_per_action(
        self,
        actions_mapping: ActionsMapping,
        *,
        custom: Optional[Union[T, Dict[ActionEvent, T]]],
        default: None,
    ) -> Dict[ActionEvent, Optional[T]]:
        ...

    @overload
    def get_mapping_per_action(
        self,
        actions_mapping: ActionsMapping,
        *,
        custom: Optional[Union[T, Dict[ActionEvent, T]]],
        default: T,
    ) -> Dict[ActionEvent, T]:
        ...

    def get_mapping_per_action(
        self,
        actions_mapping: ActionsMapping,
        *,
        custom: Optional[Union[T, Dict[ActionEvent, T]]],
        default: Union[None, T],
    ) -> Union[Dict[ActionEvent, Optional[T]], Dict[ActionEvent, T]]:
        if custom is not None and not isinstance(custom, dict):
            default = custom
        mapping = {action: default for action in actions_mapping}
        if custom is not None and isinstance(custom, dict):
            mapping.update(custom)
        return mapping

    def parse_action_mapping(self, mapping: CustomActionsMapping) -> ActionsMapping:
        return {
            event: parse_actions(self, action)
            for event, action in mapping.items()
            if action is not None
        }

    def get_multiple_click_actions(self, mapping: ActionsMapping) -> Set[ActionEvent]:
        to_return: Set[ActionEvent] = set()
        for key in mapping.keys():
            if not isinstance(key, str) or MULTIPLE_CLICK_TOKEN not in key:
                continue
            splitted = key.split(MULTIPLE_CLICK_TOKEN)
            assert 1 <= len(splitted) <= 2
            action_key, _ = splitted
            try:
                to_return.add(int(action_key))
            except ValueError:
                to_return.add(action_key)
        return to_return

    def format_multiple_click_action(
        self, action_key: ActionEvent, click_count: int
    ) -> str:
        return (
            str(action_key) + MULTIPLE_CLICK_TOKEN + str(click_count)
        )  # e.g. toggle$2

    async def _render_template(self, template: str) -> Any:
        result = await self.call_service(
            "template/render",
            render_template=False,
            template=template,
            return_result=True,
        )
        if result is None:
            raise ValueError(f"Template {template} returned None")
        try:
            return literal_eval(result)
        except (SyntaxError, ValueError):
            return result

    _TEMPLATE_RE = re.compile(r"\s*\{\{.*\}\}")

    def contains_templating(self, template: str) -> bool:
        is_template = self._TEMPLATE_RE.search(template) is not None
        if not is_template:
            self.log(f"`{template}` is not recognized as a template", level="DEBUG")
        return is_template

    async def render_value(self, value: Any) -> Any:
        if isinstance(value, str) and self.contains_templating(value):
            return await self._render_template(value)
        else:
            return value

    async def render_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
        new_attributes: Dict[str, Any] = {}
        for key, value in attributes.items():
            new_value = await self.render_value(value)
            if isinstance(value, dict):
                new_value = await self.render_attributes(value)
            new_attributes[key] = new_value
        return new_attributes

    async def call_service(
        self, service: str, render_template: bool = True, **attributes: Any
    ) -> Optional[Any]:
        service = service.replace(".", "/")
        to_log = ["\n", f"๐Ÿค– Service: \033[1m{service.replace('/', '.')}\033[0m"]
        if service != "template/render" and render_template:
            attributes = await self.render_attributes(attributes)
        for attribute, value in attributes.items():
            if isinstance(value, float):
                value = f"{value:.2f}"
            to_log.append(f"  - {attribute}: {value}")
        self.log("\n".join(to_log), level="INFO", ascii_encode=False)
        return await ADAPI.call_service(self, service, **attributes)

    @utils.sync_decorator
    async def get_state(
        self,
        entity_id: str | None = None,
        attribute: str | None = None,
        default: Any | None = None,
        copy: bool = True,
    ) -> Optional[Any]:
        rendered_entity_id = await self.render_value(entity_id)
        return await super().get_state(rendered_entity_id, attribute, default, copy)

    async def handle_action(
        self,
        action_key: str,
        previous_state: Optional[str] = None,
        extra: Optional[EventData] = None,
    ) -> None:
        if (
            action_key in self.actions_mapping
            and self.previous_states[action_key] is not None
            and previous_state != self.previous_states[action_key]
        ):
            self.log(
                f"๐ŸŽฎ `{action_key}` not triggered because previous action was `{previous_state}`",
                level="DEBUG",
                ascii_encode=False,
            )
            return
        if (
            action_key in self.actions_mapping
            and action_key not in self.multiple_click_actions
        ):
            previous_call_time = self.action_times[action_key]
            now = time.time() * 1000
            self.action_times[action_key] = now
            if now - previous_call_time > self.action_delta[action_key]:
                await self.call_action(action_key, extra=extra)
        elif action_key in self.multiple_click_actions:
            now = time.time() * 1000
            previous_call_time = self.multiple_click_action_times.get(action_key, now)
            self.multiple_click_action_times[action_key] = now
            if now - previous_call_time > self.multiple_click_delay:
                pass

            previous_task = self.multiple_click_action_delay_tasks[action_key]
            if previous_task is not None:
                previous_task.cancel()

            self.click_counter[action_key] += 1
            click_count = self.click_counter[action_key]

            new_task = run_in(
                self.multiple_click_call_action,
                self.multiple_click_delay / 1000,
                action_key=action_key,
                extra=extra,
                click_count=click_count,
            )
            self.multiple_click_action_delay_tasks[action_key] = new_task
        else:
            self.log(
                f"๐ŸŽฎ Button event triggered, but not registered: `{action_key}`",
                level="DEBUG",
                ascii_encode=False,
            )

    async def multiple_click_call_action(self, kwargs: Dict[str, Any]) -> None:
        action_key: ActionEvent = kwargs["action_key"]
        extra: EventData = kwargs["extra"]
        click_count: int = kwargs["click_count"]
        self.multiple_click_action_delay_tasks[action_key] = None
        self.log(
            f"๐ŸŽฎ {action_key} clicked `{click_count}` time(s)",
            level="DEBUG",
            ascii_encode=False,
        )
        self.click_counter[action_key] = 0
        click_action_key = self.format_multiple_click_action(action_key, click_count)
        if click_action_key in self.actions_mapping:
            await self.call_action(click_action_key, extra=extra)
        elif action_key in self.actions_mapping and click_count == 1:
            await self.call_action(action_key, extra=extra)

    async def call_action(
        self, action_key: ActionEvent, extra: Optional[EventData] = None
    ) -> None:
        self.log(
            f"๐ŸŽฎ Button event triggered: `{action_key}`",
            level="INFO",
            ascii_encode=False,
        )
        self.log(
            f"Extra:\n{extra}",
            level="DEBUG",
        )
        delay = self.action_delay[action_key]
        if delay > 0:
            handle = self.action_delay_handles[action_key]
            if handle is not None:
                await self.cancel_timer(handle)
            self.log(
                f"๐Ÿ•’ Running action(s) from `{action_key}` in {delay} seconds",
                level="INFO",
                ascii_encode=False,
            )
            new_handle = await self.run_in(
                self.action_timer_callback, delay, action_key=action_key, extra=extra
            )
            self.action_delay_handles[action_key] = new_handle
        else:
            await self.action_timer_callback({"action_key": action_key, "extra": extra})

    async def _apply_mode_strategy(self, action_key: ActionEvent) -> bool:
        previous_task = self.action_handles[action_key]
        if previous_task is None or previous_task.done():
            return False
        if self.mode[action_key] == MODE_SINGLE:
            self.log(
                f"There is already an action executing for `{action_key}`. "
                "If you want a different behaviour change `mode` parameter, "
                "the default value is `single`.",
                level="WARNING",
            )
            return True
        elif self.mode[action_key] == MODE_RESTART:
            previous_task.cancel()
        elif self.mode[action_key] == MODE_QUEUED:
            await previous_task
        elif self.mode[action_key] == MODE_PARALLEL:
            pass
        else:
            raise ValueError(
                f"`{self.mode[action_key]}` is not a possible value for `mode` parameter."
                "Possible values: `single`, `restart`, `queued` and `parallel`."
            )
        return False

    async def action_timer_callback(self, kwargs: Dict[str, Any]) -> None:
        action_key: ActionEvent = kwargs["action_key"]
        extra: EventData = kwargs["extra"]
        self.action_delay_handles[action_key] = None
        skip = await self._apply_mode_strategy(action_key)
        if skip:
            return
        action_types = self.actions_mapping[action_key]
        task = asyncio.create_task(self.call_action_types(action_types, extra))
        self.action_handles[action_key] = task
        try:
            await task
        except CancelledError:
            self.log(
                f"Task(s) from `{action_key}` was/were canceled and executed again",
                level="DEBUG",
            )

    async def call_action_types(
        self, action_types: List[ActionType], extra: Optional[EventData] = None
    ) -> None:
        for action_type in action_types:
            self.log(
                f"๐Ÿƒ Running `{action_type}` now",
                level="INFO",
                ascii_encode=False,
            )
            await action_type.run(extra=extra)

    async def before_action(self, action: str, *args: str, **kwargs: Any) -> bool:
        """
        Controllers have the option to implement this function, which is called
        everytime before an action is called and it has the check_before_action decorator.
        It should return True if the action shoul be called.
        Otherwise it should return False.
        """
        return True

    def get_z2m_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the states that a controller can take and the functions as values.
        This is used for zigbee2mqtt support.
        """
        return None

    def get_deconz_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the event id that a controller can take and the functions as values.
        This is used for deCONZ support.
        """
        return None

    def get_zha_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for ZHA support.
        """
        return None

    def get_zha_action(self, data: EventData) -> Optional[str]:
        """
        This method can be override for controllers that do not support
        the standard extraction of the actions on cx_core/integration/zha.py
        """
        return None

    def get_lutron_caseta_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for Lutron support.
        """
        return None

    def get_state_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for State integration support.
        """
        return None

    def get_homematic_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for Homematic support.
        """
        return None

    def get_shelly_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for Shelly support.
        """
        return None

    def get_shellyforhass_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for Shelly for HASS support.
        """
        return None

    def get_tasmota_actions_mapping(self) -> Optional[DefaultActionsMapping]:
        """
        Controllers can implement this function. It should return a dict
        with the command that a controller can take and the functions as values.
        This is used for Tasmota support.
        """
        return None

    def get_predefined_actions_mapping(self) -> PredefinedActionsMapping:
        return {}

mietzen avatar Jun 01 '25 10:06 mietzen

@mietzen Ah - that's your issue:

secrets: /homeassistant/secrets.yaml
appdaemon:
  latitude: 0
  longitude: 0
  elevation: 30
  time_zone: Europe/Berlin
  plugins:
    HASS:
      type: hass
      token: !env_var SUPERVISOR_TOKEN
http:
  url: http://0.0.0.0:5050
admin:
api:
hadashboard:
exclude_dirs:
  - cx_core

Move:

exclude_dirs:
  - cx_core

So it's under appdaermon, like:

appdaemon:
  exclude_dirs:
    - cx_core

charrus avatar Jun 01 '25 10:06 charrus

Sorry I'm just a user, not a developer by any stretch, but I can confirm what I think has happened for others has also happened for me:

-updated AppDaemon to 0.17.2 and all ControllerX switches stopped working -made the changes above around the exclude_dirs and decorator lines, confirmed switches were now able to switch on and off, but not dim up or down, or change colour temperature of lights -Restored AppDaemon to 0.16.6 via HA Restore (the AppDaemon plug-in only) and all functionality is now working again.

Subscribing to monitor progress, thank you for all your hard work :)

see-dos-run avatar Jun 02 '25 23:06 see-dos-run

@charrus thank you for your pull request #1188 , this makes my remotes usable again. However, it only helps me to turn the lights off and on. Changing brightness or color gives me a crash on "supported features", however I don't know whether this is related to your PR or some other changes in AppDaemon, but I did not have this issue before the bug from the issue from this thread arose. If it helps, this is my crash log, where first the lamp is turned off and on just fine, but then crashes on brightness and on color change:

025-06-04 12:41:01.417989 INFO remote_bathroom: ๐ŸŽฎ Button event triggered: `on`
2025-06-04 12:41:01.425534 INFO remote_bathroom: ๐Ÿƒ Running `Predefined (on)` now
2025-06-04 12:41:01.429416 INFO remote_bathroom: 
๐Ÿค– Service: light.turn_on
  - entity_id: light.bathroom
2025-06-04 12:41:03.153013 INFO remote_bathroom: ๐ŸŽฎ Button event triggered: `off`
2025-06-04 12:41:03.165483 INFO remote_bathroom: ๐Ÿƒ Running `Predefined (off)` now
2025-06-04 12:41:03.172041 INFO remote_bathroom: 
๐Ÿค– Service: light.turn_off
  - entity_id: light.bathroom
2025-06-04 12:41:06.957937 INFO remote_bathroom: ๐ŸŽฎ Button event triggered: `brightness_move_up`
2025-06-04 12:41:06.964704 INFO remote_bathroom: ๐Ÿƒ Running `Predefined (hold_brightness_up)` now
2025-06-04 12:41:06.969432 ERROR remote_bathroom: =====  event_callback() in remote_bathroom  ===============================
2025-06-04 12:41:06.969664 ERROR remote_bathroom: EventCallbackFail: Scheduled callback failed for app 'remote_bathroom'
2025-06-04 12:41:06.969834 ERROR remote_bathroom:   args: ('MQTT_MESSAGE', {'topic': 'zigbee2mqtt/Remote bathroom', 'wildcard': 'zigbee2mqtt/#', 'payload': '{"action":"brightness_move_up","action_rate":83,"battery":95,"identify":null,"linkquality":180,"update":{"installed_version":-1,"latest_version":-1,"state":null}}'}, {'topic': 'zigbee2mqtt/Remote bathroom', '__thread_id': 'MainThread'})
2025-06-04 12:41:06.970264 ERROR remote_bathroom:   ValueError: `supported_features` could not be read from `light.bathroom`. Entity might not be available.
2025-06-04 12:41:06.971319 ERROR remote_bathroom:     File "/usr/lib/python3.12/site-packages/appdaemon/threads.py", line 986, in safe_callback
2025-06-04 12:41:06.971521 ERROR remote_bathroom:       await funcref()
2025-06-04 12:41:06.971718 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/integration/z2m.py", line 75, in event_callback
2025-06-04 12:41:06.971884 ERROR remote_bathroom:       await self.controller.handle_action(payload[action_key], extra=payload)
2025-06-04 12:41:06.972044 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 386, in handle_action
2025-06-04 12:41:06.972186 ERROR remote_bathroom:       await self.call_action(action_key, extra=extra)
2025-06-04 12:41:06.972314 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 460, in call_action
2025-06-04 12:41:06.972462 ERROR remote_bathroom:       await self.action_timer_callback({"action_key": action_key, "extra": extra})
2025-06-04 12:41:06.972585 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 498, in action_timer_callback
2025-06-04 12:41:06.972710 ERROR remote_bathroom:       await task
2025-06-04 12:41:06.972836 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 514, in call_action_types
2025-06-04 12:41:06.972976 ERROR remote_bathroom:       await action_type.run(extra=extra)
2025-06-04 12:41:06.973104 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py", line 118, in run
2025-06-04 12:41:06.973227 ERROR remote_bathroom:       await action(*positional, **action_args)
2025-06-04 12:41:06.973358 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 43, in _action_impl
2025-06-04 12:41:06.973484 ERROR remote_bathroom:       continue_call = await controller.before_action(method.__name__, *args, **kwargs)
2025-06-04 12:41:06.973604 ERROR remote_bathroom:                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:06.973728 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/type/light_controller.py", line 799, in before_action
2025-06-04 12:41:06.973845 ERROR remote_bathroom:       self.remove_transition_check = await self.check_remove_transition(
2025-06-04 12:41:06.973988 ERROR remote_bathroom:                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:06.974123 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/type/light_controller.py", line 438, in check_remove_transition
2025-06-04 12:41:06.974254 ERROR remote_bathroom:       or await self.feature_support.not_supported(LightSupport.TRANSITION)
2025-06-04 12:41:06.974388 ERROR remote_bathroom:          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:06.974528 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 40, in not_supported
2025-06-04 12:41:06.974668 ERROR remote_bathroom:       return not await self.is_supported(feature)
2025-06-04 12:41:06.974804 ERROR remote_bathroom:                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:06.974940 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 37, in is_supported
2025-06-04 12:41:06.975071 ERROR remote_bathroom:       return feature & await self.supported_features != 0
2025-06-04 12:41:06.975566 ERROR remote_bathroom:                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:06.975728 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 31, in supported_features
2025-06-04 12:41:06.975875 ERROR remote_bathroom:       raise ValueError(
2025-06-04 12:41:06.976023 ERROR remote_bathroom: ===========================================================================
2025-06-04 12:41:07.490286 INFO remote_bathroom: ๐ŸŽฎ Button event triggered: `brightness_stop`
2025-06-04 12:41:07.497425 INFO remote_bathroom: ๐Ÿƒ Running `Predefined (release)` now
2025-06-04 12:41:08.705442 INFO remote_bathroom: ๐ŸŽฎ Button event triggered: `arrow_right_click`
2025-06-04 12:41:08.721573 INFO remote_bathroom: ๐Ÿƒ Running `Predefined (click_color_up)` now
2025-06-04 12:41:08.732859 ERROR remote_bathroom: =====  event_callback() in remote_bathroom  ===============================
2025-06-04 12:41:08.733270 ERROR remote_bathroom: EventCallbackFail: Scheduled callback failed for app 'remote_bathroom'
2025-06-04 12:41:08.733621 ERROR remote_bathroom:   args: ('MQTT_MESSAGE', {'topic': 'zigbee2mqtt/Remote bathroom', 'wildcard': 'zigbee2mqtt/#', 'payload': '{"action":"arrow_right_click","battery":95,"identify":null,"linkquality":176,"update":{"installed_version":-1,"latest_version":-1,"state":null}}'}, {'topic': 'zigbee2mqtt/Remote bathroom', '__thread_id': 'MainThread'})
2025-06-04 12:41:08.734550 ERROR remote_bathroom:   ValueError: `supported_features` could not be read from `light.bathroom`. Entity might not be available.
2025-06-04 12:41:08.736568 ERROR remote_bathroom:     File "/usr/lib/python3.12/site-packages/appdaemon/threads.py", line 986, in safe_callback
2025-06-04 12:41:08.736915 ERROR remote_bathroom:       await funcref()
2025-06-04 12:41:08.737260 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/integration/z2m.py", line 75, in event_callback
2025-06-04 12:41:08.737573 ERROR remote_bathroom:       await self.controller.handle_action(payload[action_key], extra=payload)
2025-06-04 12:41:08.737862 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 386, in handle_action
2025-06-04 12:41:08.738212 ERROR remote_bathroom:       await self.call_action(action_key, extra=extra)
2025-06-04 12:41:08.738560 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 460, in call_action
2025-06-04 12:41:08.738868 ERROR remote_bathroom:       await self.action_timer_callback({"action_key": action_key, "extra": extra})
2025-06-04 12:41:08.739198 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 498, in action_timer_callback
2025-06-04 12:41:08.739513 ERROR remote_bathroom:       await task
2025-06-04 12:41:08.739851 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 514, in call_action_types
2025-06-04 12:41:08.740181 ERROR remote_bathroom:       await action_type.run(extra=extra)
2025-06-04 12:41:08.740502 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/action_type/predefined_action_type.py", line 118, in run
2025-06-04 12:41:08.740777 ERROR remote_bathroom:       await action(*positional, **action_args)
2025-06-04 12:41:08.741068 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/controller.py", line 43, in _action_impl
2025-06-04 12:41:08.741432 ERROR remote_bathroom:       continue_call = await controller.before_action(method.__name__, *args, **kwargs)
2025-06-04 12:41:08.741723 ERROR remote_bathroom:                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:08.742018 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/type/light_controller.py", line 799, in before_action
2025-06-04 12:41:08.742278 ERROR remote_bathroom:       self.remove_transition_check = await self.check_remove_transition(
2025-06-04 12:41:08.742557 ERROR remote_bathroom:                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:08.742805 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/type/light_controller.py", line 438, in check_remove_transition
2025-06-04 12:41:08.743042 ERROR remote_bathroom:       or await self.feature_support.not_supported(LightSupport.TRANSITION)
2025-06-04 12:41:08.743274 ERROR remote_bathroom:          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:08.743524 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 40, in not_supported
2025-06-04 12:41:08.743757 ERROR remote_bathroom:       return not await self.is_supported(feature)
2025-06-04 12:41:08.743987 ERROR remote_bathroom:                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:08.744218 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 37, in is_supported
2025-06-04 12:41:08.744478 ERROR remote_bathroom:       return feature & await self.supported_features != 0
2025-06-04 12:41:08.744696 ERROR remote_bathroom:                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-06-04 12:41:08.744907 ERROR remote_bathroom:     File "/homeassistant/appdaemon/apps/controllerx/cx_core/feature_support/__init__.py", line 31, in supported_features
2025-06-04 12:41:08.745113 ERROR remote_bathroom:       raise ValueError(
2025-06-04 12:41:08.745326 ERROR remote_bathroom: ===========================================================================

bakeromso avatar Jun 04 '25 10:06 bakeromso

@bakeromso That's an odd one. My setup is different to yours, I managed to reproduce it with:

hall_lights_controller_3:
  module: controllerx
  class: HueDimmerController
  integration:
    name: z2m
    listen_to: mqtt
  controller: Hue dimmer switch 2
  light: light.hall_lights

Whereas I use the light from z2m, rather than the entity in HA:

hall_lights_controller_2:
  module: controllerx
  class: HueDimmerZ2MLightController
  integration:
    name: z2m
    listen_to: mqtt
  controller: Hue dimmer switch 2
  light: hall/lights

If you're controlling a light from zigbee2mqtt then try the above.

I don't have time currently to debug further, so I'll just echo what @xaviml 's official stance on this is in: https://github.com/xaviml/controllerx/issues/1184#issuecomment-2921497437:

Until this is further debugged, I do not recommend anyone to upgrade to latest AppDaemon addon version.

Edit: This actually helped troubleshoot, so I've updated the PR so it should work with entities in HA now - it's very hacky:

    @utils.sync_decorator  # type: ignore[misc]
    async def get_state(
        self,
        entity_id: Optional[str] = None,
        attribute: Optional[str] = None,
        default: Any = None,
        copy: bool = True,
    ) -> Any | dict[str, Any] | None:
        rendered_entity_id = await self.render_value(entity_id)
        return await super().get_state(
            rendered_entity_id, attribute, default=default, copy=copy
        )

Second Edit: Noticed get_state has changed since earlier versions, so explicitly pass in named arguments (namespace is new, so positional would break with default and copy). Also, kwargs is back - but leaving out to avoid warning.

Third Edit. Backed out most changes - the simplest fix is too just to pass default and copy as named arguments due to the addition of namespace in the AppDaemon method. Hey ho!

But including it here if it helps get to the bottom of the dimming issue.

charrus avatar Jun 04 '25 11:06 charrus

Hi everyone! :)

I just released a beta version of ControllerX v5.0.0b0. This version includes @charrus changes (Thank you for your support!), so this new version only works with the new AppDaemon addon.

In addition, you should all include the "exclude_dirs" like specified here: https://github.com/xaviml/controllerx/issues/1184#issuecomment-2917412699

Everything is working on my setup, but I would like to hear from you all and see if there are any other issues. I will also explore further the possibility of avoiding the "exclude_dirs" workaround.

Regards, Xavi M.

xaviml avatar Jun 04 '25 23:06 xaviml

Thankyou for the work @xaviml I can't seem to get it working here, it still picks up the cx_core folder. HA Core 2025.5.3 AppDaemon: 0.17.4 ControllerX: 5.0.0b

I find controllerx two places, the old under config, and the new under addon_configs, not sure which is right?

I've added the exlude_dirs in /addon_configs/a0d7b954_appdaemon/appdaemon.yaml

And example of an app:

bathroom:
  module: controllerx
  class: E1743Controller
  constrain_input_boolean: binary_sensor.video_running_in_bathroom,off
  controller: bathroom_control
  integration: 
    name: z2m
    listen_to: mqtt
  light: light.all_bathroom
appdaemon:
  ...
  exclude_dirs:
      - cx_core
  plugins:
...

2025-06-05 08:25:17.599914 INFO AppDaemon: New app config: bathroom 2025-06-05 08:25:17.600236 INFO AppDaemon: New app config: trappeskab 2025-06-05 08:25:17.600431 INFO AppDaemon: New app config: livingroom_rooms 2025-06-05 08:25:17.600567 INFO AppDaemon: New app config: lians_magic_cube1 2025-06-05 08:25:17.600699 INFO AppDaemon: New app config: Hallway 2025-06-05 08:25:17.600824 INFO AppDaemon: New app config: Adrian_Bedlight 2025-06-05 08:25:17.600957 INFO AppDaemon: New app config: Bedroom-cover-up 2025-06-05 08:25:17.601073 INFO AppDaemon: New app config: Bedroom-cover-down 2025-06-05 08:25:17.601200 INFO AppDaemon: New app config: livingroom-small-cover-up 2025-06-05 08:25:17.601323 INFO AppDaemon: New app config: livingroom-small-cover-down 2025-06-05 08:25:17.601450 INFO AppDaemon: New app config: livingroom-big-cover-up 2025-06-05 08:25:17.601561 INFO AppDaemon: New app config: livingroom-big-cover-down 2025-06-05 08:25:17.601674 INFO AppDaemon: New app config: livingroom_at_stairs 2025-06-05 08:25:17.601790 INFO AppDaemon: New app config: top_of_stairs_light 2025-06-05 08:25:17.601898 INFO AppDaemon: New app config: Kitchen_speakers 2025-06-05 08:25:17.601995 INFO AppDaemon: New app config: office_ambient 2025-06-05 08:25:17.602132 INFO AppDaemon: New app config: office_desk_light 2025-06-05 08:25:17.602269 INFO AppDaemon: New app config: office_ceiling_light 2025-06-05 08:25:17.602398 INFO AppDaemon: New app config: office_sofa_light 2025-06-05 08:25:17.602518 INFO AppDaemon: New app config: office_cover_up 2025-06-05 08:25:17.602639 INFO AppDaemon: New app config: office_cover_down 2025-06-05 08:25:17.604734 INFO HASS: Completed initialization in 124ms 2025-06-05 08:25:17.642017 ERROR Error: ===== Error importing 'controllerx' ===================================== 2025-06-05 08:25:17.642278 ERROR Error: FailedImport: Failed to import 'controllerx' 2025-06-05 08:25:17.642871 ERROR Error: AttributeError: module 'appdaemon.utils' has no attribute 'sync_wrapper' 2025-06-05 08:25:17.643793 ERROR Error: File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 1075, in safe_import 2025-06-05 08:25:17.643981 ERROR Error: await self.import_module(module_name) 2025-06-05 08:25:17.644134 ERROR Error: File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 534, in wrapper 2025-06-05 08:25:17.644275 ERROR Error: return await run_in_executor(self, func, *args, **kwargs) 2025-06-05 08:25:17.644410 ERROR Error: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2025-06-05 08:25:17.644543 ERROR Error: File "/usr/lib/python3.12/site-packages/appdaemon/utils.py", line 559, in run_in_executor 2025-06-05 08:25:17.644671 ERROR Error: return await future 2025-06-05 08:25:17.644783 ERROR Error: ^^^^^^^^^^^^ 2025-06-05 08:25:17.644880 ERROR Error: File "/usr/lib/python3.12/concurrent/futures/thread.py", line 59, in run 2025-06-05 08:25:17.644987 ERROR Error: result = self.fn(*self.args, **self.kwargs) 2025-06-05 08:25:17.645114 ERROR Error: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2025-06-05 08:25:17.645239 ERROR Error: File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 701, in import_module 2025-06-05 08:25:17.645343 ERROR Error: raise exc 2025-06-05 08:25:17.645473 ERROR Error: File "/usr/lib/python3.12/site-packages/appdaemon/app_management.py", line 693, in import_module 2025-06-05 08:25:17.645576 ERROR Error: importlib.import_module(module_name) 2025-06-05 08:25:17.645692 ERROR Error: File "/usr/lib/python3.12/importlib/init.py", line 90, in import_module 2025-06-05 08:25:17.645800 ERROR Error: return _bootstrap._gcd_import(name[level:], package, level) 2025-06-05 08:25:17.645890 ERROR Error: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 2025-06-05 08:25:17.646000 ERROR Error: File "", line 1387, in _gcd_import 2025-06-05 08:25:17.646126 ERROR Error: File "", line 1360, in _find_and_load 2025-06-05 08:25:17.646252 ERROR Error: File "", line 1331, in _find_and_load_unlocked 2025-06-05 08:25:17.646348 ERROR Error: File "", line 935, in _load_unlocked 2025-06-05 08:25:17.646474 ERROR Error: File "", line 999, in exec_module 2025-06-05 08:25:17.646575 ERROR Error: File "", line 488, in _call_with_frames_removed 2025-06-05 08:25:17.646700 ERROR Error: File "/config/apps/controllerx/controllerx.py", line 6, in 2025-06-05 08:25:17.646802 ERROR Error: from cx_core import ( 2025-06-05 08:25:17.646898 ERROR Error: File "/config/apps/controllerx/cx_core/init.py", line 1, in 2025-06-05 08:25:17.647003 ERROR Error: from cx_core.controller import Controller, action 2025-06-05 08:25:17.647132 ERROR Error: File "/config/apps/controllerx/cx_core/controller.py", line 82, in 2025-06-05 08:25:17.647254 ERROR Error: class Controller(Hass, Mqtt): # type: ignore[misc] 2025-06-05 08:25:17.647291 ERROR Error: File "/config/apps/controllerx/cx_core/controller.py", line 365, in Controller 2025-06-05 08:25:17.647325 ERROR Error: @utils.sync_wrapper # type: ignore[misc] 2025-06-05 08:25:17.647578 ERROR Error: ^^^^^^^^^^^^^^^^^^ 2025-06-05 08:25:17.647692 ERROR Error: =========================================================================== 2025-06-05 08:25:17.647954 WARNING AppDaemon: Failed to start apps: {'lians_magic_cube1', 'livingroom-small-cover-up', 'livingroom-small-cover-down', 'Adrian_Bedlight', 'bathroom', 'office_cover_down', 'top_of_stairs_light', 'Bedroom-cover-up', 'office_desk_light', 'livingroom_at_stairs', 'Kitchen_speakers', 'trappeskab', 'office_ceiling_light', 'Hallway', 'Bedroom-cover-down', 'office_sofa_light', 'office_cover_up', 'office_ambient', 'livingroom-big-cover-up', 'livingroom_rooms', 'livingroom-big-cover-down'} 2025-06-05 08:25:17.648207 INFO AppDaemon: App initialization complete

fribse avatar Jun 05 '25 06:06 fribse