Disconnect errors from Home Assistant

Hi there,
I recently started to get errors with AppDaemon stating that HA has disconnected for a few seconds. This randomly occurs and disables callbacks from the listen_state function in the initialization. The rest still works fine. The only way to get it back to working again is restarting the Add On. Everything is running on a Pi 4.

Here’s the log:

-----------------------------------------------------------

Add-on: AppDaemon

Python Apps and Dashboard using AppDaemon 4.x for Home Assistant

-----------------------------------------------------------

Add-on version: 0.16.7

You are running the latest version of this add-on.

System: Home Assistant OS 13.2 (aarch64 / raspberrypi4-64)

Home Assistant Core: 2024.11.3

Home Assistant Supervisor: 2024.11.4

-----------------------------------------------------------

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

Log level is set to INFO

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

[12:26:09] INFO: Starting AppDaemon...

s6-rc: info: service legacy-services successfully started

2024-11-24 12:26:14.863114 INFO AppDaemon: AppDaemon Version 4.4.2 starting

2024-11-24 12:26:14.863434 INFO AppDaemon: Python version is 3.11.10

2024-11-24 12:26:14.863606 INFO AppDaemon: Configuration read from: /config/appdaemon.yaml

2024-11-24 12:26:14.863765 INFO AppDaemon: Added log: AppDaemon

2024-11-24 12:26:14.863934 INFO AppDaemon: Added log: Error

2024-11-24 12:26:14.864078 INFO AppDaemon: Added log: Access

2024-11-24 12:26:14.864223 INFO AppDaemon: Added log: Diag

2024-11-24 12:26:15.163870 INFO AppDaemon: Loading Plugin HASS using class HassPlugin from module hassplugin

2024-11-24 12:26:15.511975 INFO HASS: HASS Plugin Initializing

2024-11-24 12:26:15.512291 INFO HASS: HASS Plugin initialization complete

2024-11-24 12:26:15.513154 INFO AppDaemon: Initializing HTTP

2024-11-24 12:26:15.513770 INFO AppDaemon: Using 'ws' for event stream

2024-11-24 12:26:15.518869 INFO AppDaemon: Starting API

2024-11-24 12:26:15.522131 INFO AppDaemon: Starting Admin Interface

2024-11-24 12:26:15.522893 INFO AppDaemon: Starting Dashboards

2024-11-24 12:26:15.545569 INFO HASS: Connected to Home Assistant 2024.11.3

2024-11-24 12:26:15.556635 INFO AppDaemon: App 'canbus_processor' added

2024-11-24 12:26:15.558595 INFO AppDaemon: Found 1 active apps

2024-11-24 12:26:15.559240 INFO AppDaemon: Found 0 inactive apps

2024-11-24 12:26:15.559708 INFO AppDaemon: Found 0 global libraries

2024-11-24 12:26:15.560222 INFO AppDaemon: Starting Apps with 1 workers and 1 pins

2024-11-24 12:26:15.561997 INFO AppDaemon: Running on port 5050

2024-11-24 12:26:15.624007 INFO HASS: Evaluating startup conditions

2024-11-24 12:26:15.628783 INFO HASS: Startup condition met: hass state=RUNNING

2024-11-24 12:26:15.629174 INFO HASS: All startup conditions met

2024-11-24 12:26:15.668198 INFO AppDaemon: Got initial state from namespace default

2024-11-24 12:26:17.572287 INFO AppDaemon: Scheduler running in realtime

2024-11-24 12:26:17.580791 INFO AppDaemon: Adding /config/apps to module import path

2024-11-24 12:26:17.584794 INFO AppDaemon: Loading App Module: /config/apps/canbus_processor.py

2024-11-24 12:26:17.644975 INFO AppDaemon: Loading app canbus_processor using class CANBusDataProcessor from module canbus_processor

2024-11-24 12:26:17.647252 INFO AppDaemon: Calling initialize() for canbus_processor

2024-11-24 12:26:17.739601 INFO canbus_processor: Launching CANBUS reader ...

2024-11-24 12:26:17.744376 INFO canbus_processor: Connecting to TCP socket ...

2024-11-24 12:26:17.856656 INFO canbus_processor: Connected to CANBUS source.

2024-11-24 12:28:22.839751 WARNING HASS: Disconnected from Home Assistant, retrying in 5 seconds

2024-11-24 12:28:22.848533 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:24.034149 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:24.038056 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:24.042527 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:32.911687 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:32.914425 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:32.916994 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:33.808212 INFO HASS: Connected to Home Assistant 2024.11.3

2024-11-24 12:28:33.811216 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:34.144559 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:34.149903 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:34.154324 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:42.846107 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:42.855810 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:42.857769 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:43.885881 INFO HASS: Evaluating startup conditions

2024-11-24 12:28:43.891534 WARNING HASS: Attempt to call Home Assistant while disconnected: set_plugin_state

2024-11-24 12:28:43.892852 INFO HASS: Startup condition met: hass state=RUNNING

2024-11-24 12:28:43.893228 INFO HASS: All startup conditions met

Here’s the code:

import appdaemon.plugins.hass.hassapi as hass
import socket
import time
from datetime import datetime
import asyncio

class CANBusDataProcessor(hass.Hass):

    async def initialize(self):
        self.data_buffer = ""  # Initialize an empty data buffer as a string
        self.tcp_host = "192.168.178.201"  # CANBUS source IP address
        self.tcp_port = 8881  # CANBUS source port
        self.tcp_socket = None
        self.process_interval = 30

        # Define a parameter mapping dictionary
        self.parameter_mapping = {
            '0001': 'fehlermeldung',
            '0004': 'heizkreistemperatur_soll',
            '000c': 'aussentemperatur',
            '000e': 'speichertemperatur',
            '000f': 'heizkreistemperatur_ist',
            '0011': 'raumtemperatur',
            '0014': 'verdampfertemperatur',
            '0075': 'raumfeuchte',
            '001a': 'kollektortemperatur',
            '0112': 'programm',
            '0121': 'wochentag',
            '0122': 'tag',
            '0123': 'monat',
            '0124': 'jahr',
            '0125': 'stunde',
            '0126': 'minute',
            '0176': 'betriebsstatus',
            '019a': 'softwareversion',
            '0596': 'zuluft_soll',
            '0597': 'zuluft_ist',
            '0598': 'abluft_soll',
            '0599': 'abluft_ist',
            '059c': 'verfluessigertemperatur',
            '05dd': 'lueftungsstufe',
            '0571': 'lueftungszeit_ausserplan_stufe_0',
            '0572': 'lueftungszeit_ausserplan_stufe_1',
            '0573': 'lueftungszeit_ausserplan_stufe_2',
            '0574': 'lueftungszeit_ausserplan_stufe_3',
            '069e': 'motordrehzahl',
            '06a0': 'motorleistung',
            '0693': 'oelsumpftemperatur',
            '0698': 'solarpumpe_pwm',
            '091a': 'elekt_energie_ww_tag_wh',
            '091b': 'elekt_energie_ww_tag_kwh',
            '091c': 'elekt_energie_ww_summe_kwh',
            '091d': 'elekt_energie_ww_summe_mwh',
            '091e': 'elekt_energie_heizen_tag_wh',
            '091f': 'elekt_energie_heizen_tag_kwh',
            '0920': 'elekt_energie_heizen_summe_kwh',
            '0921': 'elekt_energie_heizen_summe_mwh',
            '092a': 'therm_energie_ww_tag_wh',
            '092b': 'therm_energie_ww_tag_kwh',
            '092c': 'therm_energie_ww_summe_kwh',
            '092d': 'therm_energie_ww_summe_mwh',
            '092e': 'therm_energie_heizen_tag_wh',
            '092f': 'therm_energie_heizen_tag_kwh',
            '0930': 'therm_energie_heizen_summe_kwh',
            '0931': 'therm_energie_heizen_summe_mwh',
            'c0ee': 'heiz_kuehlleistung',
            'c355': 'solar_status'
        }
        # CANBUS request frames
        self.data_to_send = [
            b'\x07\xa2\x06\x00\x00\x61\x01\xfa\x00\x04\x00\x00\x00', #HK_temperatur_soll
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x05\x97\x00\x00\x00', #zuluft_ist
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x05\x99\x00\x00\x00', #abluft_ist
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x06\x9e\x00\x00\x00', #motordrehzahl
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x06\xa0\x00\x00\x00', #motorleistung
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x06\x98\x00\x00\x00', #solarpumpe_pwm
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\xc0\xee\x00\x00\x00', #heiz_kuehlleistung
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x00\x14\x00\x00\x00', #verdampfertemperatur
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x05\x9c\x00\x00\x00', #verfluessigertemperatur
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x05\xdd\x00\x00\x00', #lueftungsstufe
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1a\x00\x00\x00', #elekt_energie_ww_tag_wh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1b\x00\x00\x00', #elekt_energie_ww_tag_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1c\x00\x00\x00', #elekt_energie_ww_summe_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1d\x00\x00\x00', #elekt_energie_ww_summe_mwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1e\x00\x00\x00', #elekt_energie_heizen_tag_wh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x1f\x00\x00\x00', #elekt_energie_heizen_tag_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x20\x00\x00\x00', #elekt_energie_heizen_summe_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x21\x00\x00\x00', #elekt_energie_heizen_summe_mwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x2a\x00\x00\x00', #therm_energie_ww_tag_wh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x2b\x00\x00\x00', #therm_energie_ww_tag_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x2e\x00\x00\x00', #therm_energie_heizen_tag_wh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x09\x2f\x00\x00\x00', #therm_energie_heizen_tag_kwh
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\xc3\x55\x00\x00\x00'  #solar_status
        ]

        # Start CANBUS reader
        await self.main()
        # Start continuous CANBUS frame request
        await self.run_every(self.send_multiple_frames, datetime.now(), self.process_interval)
        # Listen for changes in ventilation level
        await self.listen_state(self.ventilation_manual, "input_button.ventilation_manual_trigger")


    async def connect_to_tcp(self):
        try:
            self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.tcp_socket.settimeout(30)
            self.tcp_socket.connect((self.tcp_host, self.tcp_port))
            self.log("Connected to CANBUS source.")
        except Exception as e:
            self.log(f"TCP connection error: {e}")

    async def process_data(self):
        data_buffer = ""
        # Receive data from the TCP connection
        data = self.tcp_socket.recv(1024)
        if data:
            # Append received data to the buffer
            data_buffer += data.hex()
            # Extract 26-character segment from the buffer
            while len(data_buffer) >= 26:
                segment = data_buffer[:26]
                #self.log(f'Extracted CAN frame: {segment[0:2]} {segment[2:6]} {segment[6:10]} {segment[10:14]} {segment[14:16]} {segment[16:20]} {segment[20:24]} {segment[24:26]}')
                # Remove the processed data from the buffer
                data_buffer = data_buffer[26:]

                # Extract data from segment
                source = segment[2:6]
                destination = segment[10:14]
                id_hex = segment[16:20]
                value_hex = segment[20:24]

                if source == "0103" or source == "8001":
                    # Look up the parameter name in the mapping
                    parameter_name = self.parameter_mapping.get(id_hex, "unknown_parameter")

                    # Construct sensor names and set their states
                    sensor_name = f'sensor.{parameter_name}_can_value'

                    if parameter_name == "unknown_parameter":
                        await self.set_state('sensor.unknown_parameter_id', state=id_hex)
                        self.log(f'Set value {value_hex} for {sensor_name}')

                    await self.set_state(sensor_name, state=value_hex)
        else:
            self.tcp_socket.close()
            self.log("No data. Closing TCP socket")

    async def ventilation_manual(self, entity, attribute, old, new, kwargs):
        try:
            # Read the values for ventilation level and duration
            level = int(float(await self.get_state("input_number.ventilation_level_wanted")))
            duration = int(float(await self.get_state("input_number.ventilation_time_wanted")))

            #Get level CAN ID
            level_can_id_map = {
                0: "0571",
                1: "0572",  
                2: "0573",
                3: "0574"
            }
            level_can_id = level_can_id_map.get(level)

            # Convert level and duration to hexadecimal for CAN frame format
            level_hex = f"{level:04x}"  # Convert to 4-digit hex (e.g., '0003' for level 3)
            duration_hex = f"{duration:04x}"  # Convert to 4-digit hex (e.g., '0010' for 10)

            # Construct the CAN frames
            fan_speed_frame = bytes.fromhex(f"07a20600003000fa05dd{level_hex}00")
            fan_duration_frame = bytes.fromhex(f"07a20600003000fa{level_can_id}{duration_hex}00")

            # Send the frames
            await self.send_single_frame(fan_speed_frame)
            await self.send_single_frame(fan_duration_frame)

            self.log(f"Sent fan speed frame: {fan_speed_frame.hex()}")
            self.log(f"Sent fan duration frame: {fan_duration_frame.hex()}")

        except Exception as e:
            self.log(f"Error in on_ventilation_change: {e}")

    async def send_multiple_frames(self,kwargs):
        try:
            # Send the data over the TCP socket
            for frame in self.data_to_send:
                self.tcp_socket.send(frame)
                time.sleep(0.1)
        except Exception as e:
            self.log(f"Error sending data: {e}")
    
    async def send_single_frame(self,frame,kwargs):
        try:
            # Send the frame over the TCP socket
            self.tcp_socket.send(frame)
            time.sleep(0.1)
        except Exception as e:
            self.log(f"Error sending data: {e}")

    async def main(self):
        self.log("Launching CANBUS reader ...")
        while True:
            self.log("Connecting to TCP socket ...")
            await self.connect_to_tcp()
            while self.tcp_socket:
                await self.process_data()
            self.log("TCP socket closed.")

    def terminate(self):
        self.tcp_socket.close()
        self.log("Terminate. TCP socket closed.")

Hi there again,
after making some changes in the code the disconnect error hasn’t reoccured since. The main changes were removing async and adding error handling routines.

Here’s the updated code:

import appdaemon.plugins.hass.hassapi as hass
import socket
from datetime import datetime

class CANBusDataProcessor(hass.Hass):

    def initialize(self):
        self.data_buffer = ""  # Initialize an empty data buffer as a string
        self.tcp_host = "192.168.178.201"  # CANBUS source IP address
        self.tcp_port = 8881  # CANBUS source port
        self.tcp_socket = None
        self.process_interval = 30

        # Define a parameter mapping dictionary
        self.parameter_mapping = {
            '0001': 'fehlermeldung',
            '0004': 'heizkreistemperatur_soll',
           ...
        }
        # CANBUS request frames
        self.data_to_send = [
            b'\x07\xa2\x06\x00\x00\x61\x01\xfa\x00\x04\x00\x00\x00', #HK_temperatur_soll
            b'\x07\xa2\x06\x00\x00\x31\x00\xfa\x05\x97\x00\x00\x00', #zuluft_ist
            ...
        ]

        # Start continuous CANBUS frame request
        self.run_every(self.send_multiple_frames, datetime.now(), self.process_interval)
        # Listen for changes in ventilation level
        self.listen_state(self.ventilation_manual, "input_button.ventilation_manual_trigger")
        # Start CANBUS reader
        self.main()
        
    def connect_to_tcp(self):
        try:
            if hasattr(self, 'tcp_socket') and self.tcp_socket:
                self.tcp_socket.close()
                self.log("TCP socket closed.")
            self.log("Connecting to TCP socket ...")
            self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.tcp_socket.settimeout(30)
            self.tcp_socket.connect((self.tcp_host, self.tcp_port))
            self.log("Connected to CANBUS source.")
        except Exception as e:
            self.log(f"TCP connection error: {e}")

    def process_data(self):
        try:
            data_buffer = ""
            # Receive data from the TCP connection
            data = self.tcp_socket.recv(1024)
            if data:
                # Append received data to the buffer
                data_buffer += data.hex()
                # Extract 26-character segment from the buffer
                while len(data_buffer) >= 26:
                    segment = data_buffer[:26]
                    #self.log(f'Extracted CAN frame: {segment[0:2]} {segment[2:6]} {segment[6:10]} {segment[10:14]} {segment[14:16]} {segment[16:20]} {segment[20:24]} {segment[24:26]}')
                    # Remove the processed data from the buffer
                    data_buffer = data_buffer[26:]

                    # Extract data from segment
                    source = segment[2:6]
                    destination = segment[10:14]
                    id_hex = segment[16:20]
                    value_hex = segment[20:24]

                    if source == "0103" or source == "8001":
                        # Look up the parameter name in the mapping
                        parameter_name = self.parameter_mapping.get(id_hex, "unknown_parameter")

                        # Construct sensor names and set their states
                        sensor_name = f'sensor.{parameter_name}_can_value'

                        if parameter_name == "unknown_parameter":
                            self.set_state('sensor.unknown_parameter_id', state=id_hex)
                            self.log(f'Set value {value_hex} for {sensor_name}')

                        self.set_state(sensor_name, state=value_hex)
            else:
                raise ConnectionResetError("No data received. Connection likely dropped.")

        except ConnectionResetError as e:
            self.log(f"Connection reset: {e}")
            self.tcp_socket.close()
            self.data_buffer = ""  # Reset buffer to prevent partial data issues
            raise  # Propagate to trigger reconnect in the main loop

        except Exception as e:
            self.log(f"Error processing data: {e}")
            raise

    def ventilation_manual(self, entity, attribute, old, new, kwargs):
        try:
            # Read the values for ventilation level and duration
            level = int(float(self.get_state("input_number.ventilation_level_wanted")))
            duration = int(float(self.get_state("input_number.ventilation_time_wanted")))

            #Get level CAN ID
            level_can_id_map = {
                0: "0571",
                1: "0572",  
                2: "0573",
                3: "0574"
            }
            level_can_id = level_can_id_map.get(level)

            # Convert level and duration to hexadecimal for CAN frame format
            level_hex = f"{level:04x}"  # Convert to 4-digit hex (e.g., '0003' for level 3)
            duration_hex = f"{duration:04x}"  # Convert to 4-digit hex (e.g., '0010' for 10)

            # Construct the CAN frames
            fan_speed_frame = bytes.fromhex(f"07a20600003000fa05dd{level_hex}00")
            fan_duration_frame = bytes.fromhex(f"07a20600003000fa{level_can_id}{duration_hex}00")

            # Send the frames
            self.send_single_frame(fan_speed_frame)
            self.send_single_frame(fan_duration_frame)

            self.log(f"Sent fan speed frame: {fan_speed_frame.hex()}")
            self.log(f"Sent fan duration frame: {fan_duration_frame.hex()}")

        except Exception as e:
            self.log(f"Error in on_ventilation_change: {e}")

    def send_multiple_frames(self,kwargs):
        try:
            # Send the data over the TCP socket
            for frame in self.data_to_send:
                self.tcp_socket.send(frame)
        except Exception as e:
            self.log(f"Error sending multiple frames: {e}")
            self.connect_to_tcp()
    
    def send_single_frame(self,frame):
        try:
            # Send the frame over the TCP socket
            self.tcp_socket.send(frame)
        except Exception as e:
            self.log(f"Error sending single frame: {e}")
            self.connect_to_tcp()

    def main(self):
        self.log("Launching CANBUS reader ...")
        while True:
            try:
                self.connect_to_tcp()
                while self.tcp_socket:
                    try:
                        self.process_data()
                    except ConnectionResetError as e:
                        self.log(f"Connection reset error: {e}")
                        self.tcp_socket.close()
                        self.log("Reconnecting to CANBUS source...")
                        break  # Exit the inner loop to reconnect
                    except Exception as e:
                        self.log(f"Error in process_data: {e}")
                        self.tcp_socket.close()
                        break
            except Exception as e:
                self.log(f"Unexpected error in CANBUS main loop: {e}")
                self.tcp_socket = None

    def terminate(self):
        self.tcp_socket.close()
        self.log("Terminate. TCP socket closed.")