diff --git a/custom_components/bambu_lab/coordinator.py b/custom_components/bambu_lab/coordinator.py index 515757f7..8479822f 100644 --- a/custom_components/bambu_lab/coordinator.py +++ b/custom_components/bambu_lab/coordinator.py @@ -32,6 +32,7 @@ class BambuDataUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass, *, entry: ConfigEntry) -> None: self._hass = hass + self._entry = entry LOGGER.debug(f"ConfigEntry.Id: {entry.entry_id}") self.latest_usage_hours = float(entry.options.get('usage_hours', 0)) @@ -49,6 +50,7 @@ def __init__(self, hass, *, entry: ConfigEntry) -> None: self._updatedDevice = False self.data = self.get_model() + self._eventloop = asyncio.get_running_loop() super().__init__( hass, LOGGER, @@ -56,66 +58,78 @@ def __init__(self, hass, *, entry: ConfigEntry) -> None: update_interval=SCAN_INTERVAL ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) + @callback - async def start_mqtt(self) -> None: - """Use MQTT for updates.""" - LOGGER.debug("Starting MQTT") + def _async_shutdown(self, event: Event) -> None: + """Call when Home Assistant is stopping.""" + LOGGER.debug(f"HOME ASSISTANT IS SHUTTING DOWN") + self.shutdown() - def event_handler(event): - if event == "event_printer_info_update": - self._update_device_info() - if self.get_model().supports_feature(Features.EXTERNAL_SPOOL): - self._update_external_spool_info() + def event_handler(self, event): + # The callback comes in on the MQTT thread. Need to jump to the HA main thread to guarantee thread safety. + self._eventloop.call_soon_threadsafe(self.event_handler_internal, event) - elif event == "event_ams_info_update": - self._update_ams_info() + def event_handler_internal(self, event): + LOGGER.debug(f"EVENT: {event}") + if event == "event_printer_info_update": + self._update_device_info() + if self.get_model().supports_feature(Features.EXTERNAL_SPOOL): + self._update_external_spool_info() - elif event == "event_light_update": - self._update_data() + elif event == "event_ams_info_update": + self._update_ams_info() - elif event == "event_speed_update": - self._update_data() + elif event == "event_light_update": + self._update_data() - elif event == "event_printer_data_update": - self._update_data() + elif event == "event_speed_update": + self._update_data() - # Check is usage hours change and persist to config entry if it did. - if self.latest_usage_hours != self.get_model().info.usage_hours: - self.latest_usage_hours = self.get_model().info.usage_hours - LOGGER.debug(f"OVERWRITING USAGE_HOURS WITH : {self.latest_usage_hours}") - options = dict(self.config_entry.options) - options['usage_hours'] = self.latest_usage_hours - self._hass.config_entries.async_update_entry( - entry=self.config_entry, - title=self.get_model().info.serial, - data=self.config_entry.data, - options=options) + elif event == "event_printer_data_update": + self._update_data() - elif event == "event_hms_errors": - self._update_hms() + # Check is usage hours change and persist to config entry if it did. + if self.latest_usage_hours != self.get_model().info.usage_hours: + self.latest_usage_hours = self.get_model().info.usage_hours + LOGGER.debug(f"OVERWRITING USAGE_HOURS WITH : {self.latest_usage_hours}") + options = dict(self.config_entry.options) + options['usage_hours'] = self.latest_usage_hours + self._hass.config_entries.async_update_entry( + entry=self.config_entry, + title=self.get_model().info.serial, + data=self.config_entry.data, + options=options) - elif event == "event_print_canceled": - self.PublishDeviceTriggerEvent(event) + elif event == "event_hms_errors": + self._update_hms() - elif event == "event_print_failed": - self.PublishDeviceTriggerEvent(event) + elif event == "event_print_canceled": + self.PublishDeviceTriggerEvent(event) - elif event == "event_print_finished": - self.PublishDeviceTriggerEvent(event) + elif event == "event_print_failed": + self.PublishDeviceTriggerEvent(event) - elif event == "event_print_started": - self.PublishDeviceTriggerEvent(event) + elif event == "event_print_finished": + self.PublishDeviceTriggerEvent(event) - elif event == "event_printer_chamber_image_update": - self._update_data() + elif event == "event_print_started": + self.PublishDeviceTriggerEvent(event) - elif event == "event_printer_cover_image_update": - self._update_data() + elif event == "event_printer_chamber_image_update": + self._update_data() - async def listen(): - self.client.connect(callback=event_handler) + elif event == "event_printer_cover_image_update": + self._update_data() - asyncio.create_task(listen()) + async def listen(self): + LOGGER.debug("Starting listen()") + self.client.connect(callback=self.event_handler) + + async def start_mqtt(self) -> None: + """Use MQTT for updates.""" + LOGGER.debug("Starting MQTT") + asyncio.create_task(self.listen()) def shutdown(self) -> None: """ Halt the MQTT listener thread """ @@ -143,7 +157,7 @@ def _update_hms(self): hadevice = dev_reg.async_get_device(identifiers={(DOMAIN, self.get_model().info.serial)}) device = self.get_model() - if device.hms.count == 0: + if device.hms.error_count == 0: event_data = { "device_id": hadevice.id, "type": "event_printer_error_cleared", @@ -151,7 +165,7 @@ def _update_hms(self): LOGGER.debug(f"EVENT: HMS errors cleared: {event_data}") self._hass.bus.async_fire(f"{DOMAIN}_event", event_data) else: - for index in range (device.hms.count): + for index in range (device.hms.error_count): event_data = { "device_id": hadevice.id, "type": "event_printer_error", diff --git a/custom_components/bambu_lab/definitions.py b/custom_components/bambu_lab/definitions.py index 5eb058f5..67f4ed5f 100644 --- a/custom_components/bambu_lab/definitions.py +++ b/custom_components/bambu_lab/definitions.py @@ -77,7 +77,7 @@ class BambuLabBinarySensorEntityDescription(BinarySensorEntityDescription, Bambu translation_key="hms_errors", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, - is_on_fn=lambda self: self.coordinator.get_model().hms.count != 0, + is_on_fn=lambda self: self.coordinator.get_model().hms.error_count != 0, extra_attributes=lambda self: self.coordinator.get_model().hms.errors ), BambuLabBinarySensorEntityDescription( diff --git a/custom_components/bambu_lab/manifest.json b/custom_components/bambu_lab/manifest.json index d55a05ec..6b3e9572 100644 --- a/custom_components/bambu_lab/manifest.json +++ b/custom_components/bambu_lab/manifest.json @@ -18,5 +18,5 @@ "st": "urn:bambulab-com:device:3dprinter:1" } ], - "version": "2.0.15" + "version": "2.0.16" } \ No newline at end of file diff --git a/custom_components/bambu_lab/pybambu/bambu_client.py b/custom_components/bambu_lab/pybambu/bambu_client.py index a3378907..633d1498 100644 --- a/custom_components/bambu_lab/pybambu/bambu_client.py +++ b/custom_components/bambu_lab/pybambu/bambu_client.py @@ -36,6 +36,7 @@ def __init__(self, client): self._stop_event = threading.Event() self._last_received_data = time.time() super().__init__() + self.setName(f"WatchdogThread-{self._client._device.info.device_type}") def stop(self): self._stop_event.set() @@ -55,7 +56,7 @@ def run(self): break interval = time.time() - self._last_received_data if not self._watchdog_fired and (interval > WATCHDOG_TIMER): - LOGGER.debug(f"Watchdog fired. No data received for {math.floor(interval)} seconds for {self._client._device.info.device_type}/{self._client._serial}.") + LOGGER.debug(f"Watchdog fired. No data received for {math.floor(interval)} seconds for {self._client._serial}.") self._watchdog_fired = True self._client._on_watchdog_fired() elif interval < WATCHDOG_TIMER: @@ -69,12 +70,13 @@ def __init__(self, client): self._client = client self._stop_event = threading.Event() super().__init__() + self.setName(f"ChamberImageThread-{self._client._device.info.device_type}") def stop(self): self._stop_event.set() def run(self): - LOGGER.debug("{self._client._device.info.device_type}: Chamber image thread started.") + LOGGER.debug("Chamber image thread started.") auth_data = bytearray() @@ -131,11 +133,11 @@ def run(self): payload_size = 0 status = sslSock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) - LOGGER.debug(f"{self._client._device.info.device_type}: SOCKET STATUS: {status}") + LOGGER.debug(f"SOCKET STATUS: {status}") if status != 0: - LOGGER.error(f"{self._client._device.info.device_type}: Socket error: {status}") + LOGGER.error(f"Socket error: {status}") except socket.error as e: - LOGGER.error(f"{self._client._device.info.device_type}: Socket error: {e}") + LOGGER.error(f"Socket error: {e}") # Sleep to allow printer to stabilize during boot when it may fail these connection attempts repeatedly. time.sleep(1) continue @@ -144,16 +146,16 @@ def run(self): while not self._stop_event.is_set(): try: dr = sslSock.recv(read_chunk_size) - #LOGGER.debug(f"{self._client._device.info.device_type}: Received {len(dr)} bytes.") + #LOGGER.debug(f"Received {len(dr)} bytes.") except ssl.SSLWantReadError: - #LOGGER.debug(f"{self._client._device.info.device_type}: SSLWantReadError") + #LOGGER.debug("SSLWantReadError") time.sleep(1) continue except Exception as e: - LOGGER.error(f"{self._client._device.info.device_type}: A Chamber Image thread inner exception occurred:") - LOGGER.error(f"{self._client._device.info.device_type}: Exception. Type: {type(e)} Args: {e}") + LOGGER.error("A Chamber Image thread inner exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") time.sleep(1) continue @@ -189,77 +191,87 @@ def run(self): elif len(dr) == 0: # This occurs if the wrong access code was provided. - LOGGER.error(f"{self._client._device.info.device_type}: Chamber image connection rejected by the printer. Check provided access code and IP address.") + LOGGER.error("Chamber image connection rejected by the printer. Check provided access code and IP address.") # Sleep for a short while and then re-attempt the connection. time.sleep(5) break else: - LOGGER.error(f"{self._client._device.info.device_type}: UNEXPECTED DATA RECEIVED: {len(dr)}") + LOGGER.error(f"UNEXPECTED DATA RECEIVED: {len(dr)}") time.sleep(1) except OSError as e: if e.errno == 113: - LOGGER.debug(f"{self._client._device.info.device_type}: Host is unreachable") + LOGGER.debug("Host is unreachable") else: - LOGGER.error(f"{self._client._device.info.device_type}: A Chamber Image thread outer exception occurred:") - LOGGER.error(f"{self._client._device.info.device_type}: Exception. Type: {type(e)} Args: {e}") + LOGGER.error("A Chamber Image thread outer exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") if not self._stop_event.is_set(): time.sleep(1) # Avoid a tight loop if this is a persistent error. except Exception as e: - LOGGER.error(f"{self._client._device.info.device_type}: A Chamber Image thread outer exception occurred:") - LOGGER.error(f"{self._client._device.info.device_type}: Exception. Type: {type(e)} Args: {e}") + LOGGER.error(f"A Chamber Image thread outer exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") if not self._stop_event.is_set(): time.sleep(1) # Avoid a tight loop if this is a persistent error. - LOGGER.info(f"{self._client._device.info.device_type}: Chamber image thread exited.") + LOGGER.info("Chamber image thread exited.") -def mqtt_listen_thread(self): - LOGGER.info("MQTT listener thread started.") - exceptionSeen = "" - while True: - try: - host = self.host if self._local_mqtt else self.bambu_cloud.cloud_mqtt_host - LOGGER.debug(f"Connect: Attempting Connection to {host}") - self.client.connect(host, self._port, keepalive=5) - - LOGGER.debug("Starting listen loop") - self.client.loop_forever() - LOGGER.debug("Ended listen loop.") - break - except TimeoutError as e: - if exceptionSeen != "TimeoutError": - LOGGER.debug(f"TimeoutError: {e}.") - exceptionSeen = "TimeoutError" - time.sleep(5) - except ConnectionError as e: - if exceptionSeen != "ConnectionError": - LOGGER.debug(f"ConnectionError: {e}.") - exceptionSeen = "ConnectionError" - time.sleep(5) - except OSError as e: - if e.errno == 113: - if exceptionSeen != "OSError113": - LOGGER.debug(f"OSError: {e}.") - exceptionSeen = "OSError113" +class MqttThread(threading.Thread): + def __init__(self, client): + self._client = client + self._stop_event = threading.Event() + super().__init__() + self.setName(f"MqttThread-{self._client._device.info.device_type}") + + def stop(self): + self._stop_event.set() + + def run(self): + LOGGER.info("MQTT listener thread started.") + exceptionSeen = "" + while True: + try: + host = self._client.host if self._client._local_mqtt else self._client.bambu_cloud.cloud_mqtt_host + LOGGER.debug(f"Connect: Attempting Connection to {host}") + self._client.client.connect(host, self._client._port, keepalive=5) + + LOGGER.debug("Starting listen loop") + self._client.client.loop_forever() + LOGGER.debug("Ended listen loop.") + break + except TimeoutError as e: + if exceptionSeen != "TimeoutError": + LOGGER.debug(f"TimeoutError: {e}.") + exceptionSeen = "TimeoutError" time.sleep(5) - else: + except ConnectionError as e: + if exceptionSeen != "ConnectionError": + LOGGER.debug(f"ConnectionError: {e}.") + exceptionSeen = "ConnectionError" + time.sleep(5) + except OSError as e: + if e.errno == 113: + if exceptionSeen != "OSError113": + LOGGER.debug(f"OSError: {e}.") + exceptionSeen = "OSError113" + time.sleep(5) + else: + LOGGER.error("A listener loop thread exception occurred:") + LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") + time.sleep(1) # Avoid a tight loop if this is a persistent error. + except Exception as e: LOGGER.error("A listener loop thread exception occurred:") LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") time.sleep(1) # Avoid a tight loop if this is a persistent error. - except Exception as e: - LOGGER.error("A listener loop thread exception occurred:") - LOGGER.error(f"Exception. Type: {type(e)} Args: {e}") - time.sleep(1) # Avoid a tight loop if this is a persistent error. - if self.client is None: - break + if self._client.client is None: + break - self.client.disconnect() + self._client.client.disconnect() - LOGGER.info("MQTT listener thread exited.") + LOGGER.info("MQTT listener thread exited.") @dataclass @@ -325,8 +337,8 @@ def connect(self, callback): self.client.username_pw_set(self._username, password=self._auth_token) LOGGER.debug("Starting MQTT listener thread") - thread = threading.Thread(target=mqtt_listen_thread, args=(self,)) - thread.start() + self._mqtt = MqttThread(self) + self._mqtt.start() def subscribe_and_request_info(self): LOGGER.debug("Now subscribing...") @@ -383,12 +395,15 @@ def on_disconnect(self, self._on_disconnect() def _on_disconnect(self): + LOGGER.warn("_on_disconnect") self._connected = False self._device.info.set_online(False) if self._watchdog is not None: + LOGGER.warn("Stopping watchdog thread") self._watchdog.stop() self._watchdog.join() if self._camera is not None: + LOGGER.warn("Stopping camera thread") self._camera.stop() self._camera.join() @@ -406,9 +421,7 @@ def on_message(self, client, userdata, message): # X1 mqtt payload is inconsistent. Adjust it for consistent logging. clean_msg = re.sub(r"\\n *", "", str(message.payload)) if self._refreshed: - LOGGER.debug(f"Received data from: {self._device.info.device_type}: {clean_msg}") - else: - LOGGER.debug(f"Received data from: {self._device.info.device_type}") + LOGGER.debug(f"Received data: {clean_msg}") json_data = json.loads(message.payload) if json_data.get("event"): @@ -474,7 +487,7 @@ def get_device(self): def disconnect(self): """Disconnect the Bambu Client from server""" - LOGGER.debug("Disconnect: Client Disconnecting") + LOGGER.debug(" Disconnect: Client Disconnecting") if self.client is not None: self.client.disconnect() self.client = None diff --git a/custom_components/bambu_lab/pybambu/models.py b/custom_components/bambu_lab/pybambu/models.py index 91a8de79..bde0922e 100644 --- a/custom_components/bambu_lab/pybambu/models.py +++ b/custom_components/bambu_lab/pybambu/models.py @@ -59,8 +59,6 @@ def __init__(self, client): self.cover_image = CoverImage(client = client) def print_update(self, data) -> bool: - """Update from dict""" - send_event = False send_event = send_event | self.info.print_update(data = data) send_event = send_event | self.print_job.print_update(data = data) @@ -76,13 +74,13 @@ def print_update(self, data) -> bool: send_event = send_event | self.home_flag.print_update(data = data) if send_event and self._client.callback is not None: + LOGGER.debug("event_printer_data_update") self._client.callback("event_printer_data_update") if data.get("msg", 0) == 0: self.push_all_data = data def info_update(self, data): - """Update from dict""" self.info.info_update(data = data) self.home_flag.info_update(data = data) self.ams.info_update(data = data) @@ -151,7 +149,6 @@ def __init__(self, client): self.chamber_light_override = "" def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" # "lights_report": [ @@ -209,7 +206,6 @@ def __init__(self): self.timelapse = '' def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" # "ipcam": { @@ -246,7 +242,6 @@ def __init__(self): self.target_nozzle_temp = 0 def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" self.bed_temp = round(data.get("bed_temper", self.bed_temp)) @@ -293,7 +288,6 @@ def __init__(self, client): self._heatbreak_fan_speed = 0 def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" self._aux_fan_speed = data.get("big_fan1_speed", self._aux_fan_speed) @@ -418,7 +412,6 @@ def __init__(self, client): self.print_type = "" def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" # Example payload: @@ -463,7 +456,7 @@ def print_update(self, data) -> bool: # Calculate start / end time after we update task data so we don't stomp on prepopulated values while idle on integration start. if data.get("gcode_start_time") is not None: if self.start_time != get_start_time(int(data.get("gcode_start_time"))): - LOGGER.debug(f"GCODE START TIME: {self._client._device.info.device_type} {self.start_time}") + LOGGER.debug(f"GCODE START TIME: {self.start_time}") self.start_time = get_start_time(int(data.get("gcode_start_time"))) # Generate the end_time from the remaining_time mqtt payload value if present. @@ -472,11 +465,11 @@ def print_update(self, data) -> bool: self.remaining_time = data.get("mc_remaining_time") if self.start_time is None: if self.start_time is not None: - LOGGER.debug(f"END TIME1: {self._client._device.info.device_type} None") + LOGGER.debug("END TIME1: None") self.end_time = None elif existing_remaining_time != self.remaining_time: self.end_time = get_end_time(self.remaining_time) - LOGGER.debug(f"END TIME2: {self._client._device.info.device_type} {self.end_time}") + LOGGER.debug(f"END TIME2: {self.end_time}") # Handle print start previously_idle = previous_gcode_state == "IDLE" or previous_gcode_state == "FAILED" or previous_gcode_state == "FINISH" @@ -493,7 +486,7 @@ def print_update(self, data) -> bool: self.start_time = get_end_time(0) # Make sure we don't keep using a stale end time. self.end_time = None - LOGGER.debug(f"GENERATED START TIME: {self._client._device.info.device_type} {self.start_time}") + LOGGER.debug(f"GENERATED START TIME: {self.start_time}") # Update task data if bambu cloud connected self._update_task_data() @@ -531,7 +524,7 @@ def print_update(self, data) -> bool: duration = datetime.now() - self.start_time # Round usage hours to 2 decimal places (about 1/2 a minute accuracy) new_hours = round((duration.seconds / 60 / 60) * 100) / 100 - LOGGER.debug(f"NEW USAGE HOURS: {self._client._device.info.device_type} {new_hours}") + LOGGER.debug(f"NEW USAGE HOURS: {new_hours}") self._client._device.info.usage_hours += new_hours return (old_data != f"{self.__dict__}") @@ -619,23 +612,23 @@ def _update_task_data(self): # "startTime": "2023-12-21T19:02:16Z" cloud_time_str = self._task_data.get('startTime', "") - LOGGER.debug(f"CLOUD START TIME1: {self._client._device.info.device_type} {self.start_time}") + LOGGER.debug(f"CLOUD START TIME1: {self.start_time}") if cloud_time_str != "": local_dt = parser.parse(cloud_time_str).astimezone(tz.tzlocal()) # Convert it to timestamp and back to get rid of timezone in printed output to match datetime objects created from mqtt timestamps. local_dt = datetime.fromtimestamp(local_dt.timestamp()) self.start_time = local_dt - LOGGER.debug(f"CLOUD START TIME2: {self._client._device.info.device_type} {self.start_time}") + LOGGER.debug(f"CLOUD START TIME2: {self.start_time}") # "endTime": "2023-12-21T19:02:35Z" cloud_time_str = self._task_data.get('endTime', "") - LOGGER.debug(f"CLOUD END TIME1: {self._client._device.info.device_type} {self.end_time}") + LOGGER.debug(f"CLOUD END TIME1: {self.end_time}") if cloud_time_str != "": local_dt = parser.parse(cloud_time_str).astimezone(tz.tzlocal()) # Convert it to timestamp and back to get rid of timezone in printed output to match datetime objects created from mqtt timestamps. local_dt = datetime.fromtimestamp(local_dt.timestamp()) self.end_time = local_dt - LOGGER.debug(f"CLOUD END TIME2: {self._client._device.info.device_type} {self.end_time}") + LOGGER.debug(f"CLOUD END TIME2: {self.end_time}") @dataclass @@ -678,7 +671,6 @@ def set_online(self, online): self._client.callback("event_printer_data_update") def info_update(self, data): - """Update from dict""" # Example payload: # { @@ -708,7 +700,6 @@ def info_update(self, data): self._client.callback("event_printer_info_update") def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" # Example payload: @@ -822,7 +813,7 @@ def __init__(self, client): self.data = [None] * 4 def info_update(self, data): - """Update from dict""" + old_data = f"{self.__dict__}" # First determine if this the version info data or the json payload data. We use the version info to determine # what devices to add to humidity_index assistant and add all the sensors as entities. And then then json payload data @@ -877,12 +868,14 @@ def info_update(self, data): received_ams_info = True self.data[index].hw_version = module['hw_ver'] + data_changed = old_data != f"{self.__dict__}" + LOGGER.debug(f"UPDATED1: {received_ams_info} {data_changed}") + if received_ams_info: if self._client.callback is not None: self._client.callback("event_ams_info_update") def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" # AMS json payload is of the form: @@ -943,7 +936,6 @@ def print_update(self, data) -> bool: # "power_on_flag": false # }, - received_ams_data = False ams_data = data.get("ams", []) if len(ams_data) != 0: self.tray_now = int(ams_data.get('tray_now', self.tray_now)) @@ -956,18 +948,17 @@ def print_update(self, data) -> bool: self.data[index] = AMSInstance() if self.data[index].humidity_index != int(ams['humidity']): - received_ams_data = True self.data[index].humidity_index = int(ams['humidity']) if self.data[index].temperature != float(ams['temp']): - received_ams_data = True self.data[index].temperature = float(ams['temp']) tray_list = ams['tray'] for tray in tray_list: tray_id = int(tray['id']) - received_ams_data = received_ams_data | self.data[index].tray[tray_id].print_update(tray) + self.data[index].tray[tray_id].print_update(tray) - return received_ams_data + data_changed = (old_data != f"{self.__dict__}") + return data_changed @dataclass class AMSTray: @@ -987,7 +978,6 @@ def __init__(self): self.tag_uid = "0000000000000000" def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" if len(data) == 1: @@ -1028,7 +1018,6 @@ def __init__(self, client): self._client = client def print_update(self, data) -> bool: - """Update from dict""" # P1P virtual tray example # "vt_tray": { @@ -1074,14 +1063,12 @@ class Speed: modifier: int def __init__(self, client): - """Load from dict""" self._client = client self._id = 2 self.name = get_speed_name(2) self.modifier = 100 def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" self._id = int(data.get("spd_lvl", self._id)) @@ -1110,13 +1097,11 @@ class StageAction: description: str def __init__(self): - """Load from dict""" self._id = 255 self._print_type = "" self.description = get_current_stage(self._id) def print_update(self, data) -> bool: - """Update from dict""" old_data = f"{self.__dict__}" self._print_type = data.get("print_type", self._print_type) @@ -1131,16 +1116,16 @@ def print_update(self, data) -> bool: @dataclass class HMSList: """Return all HMS related info""" + _count: int + _errors: dict def __init__(self, client): self._client = client - self.count = 0 - self.errors = {} - self.errors["Count"] = 0 + self._count = 0 + self._errors = {} + self._errors["Count"] = 0 def print_update(self, data) -> bool: - """Update from dict""" - # Example payload: # "hms": [ # { @@ -1154,9 +1139,9 @@ def print_update(self, data) -> bool: if 'hms' in data.keys(): hmsList = data.get('hms', []) - self.count = len(hmsList) + self._count = len(hmsList) errors = {} - errors["Count"] = self.count + errors["Count"] = self._count index: int = 0 for hms in hmsList: @@ -1170,15 +1155,25 @@ def print_update(self, data) -> bool: #LOGGER.debug(f"HMS error for '{hms_notif.module}' and severity '{hms_notif.severity}': HMS_{hms_notif.hms_code}") #errors[f"{index}-Module"] = hms_notif.module # commented out to avoid bloat with current structure - if self.errors != errors: - self.errors = errors - if self.count != 0: + if self._errors != errors: + LOGGER.debug("Updating HMS error list.") + self._errors = errors + if self._count != 0: LOGGER.warning(f"HMS ERRORS: {errors}") if self._client.callback is not None: self._client.callback("event_hms_errors") return True return False + + @property + def errors(self) -> dict: + #LOGGER.debug(f"PROPERTYCALL: get_hms_errors") + return self._errors + + @property + def error_count(self) -> int: + return self._count @dataclass @@ -1219,14 +1214,18 @@ def __init__(self, client): self._image_last_updated = datetime.now() def set_jpeg(self, bytes): - #LOGGER.debug(f"JPEG RECEIVED: {self._client._device.info.device_type}") + #LOGGER.debug("JPEG RECEIVED") self._bytes = bytes self._image_last_updated = datetime.now() if self._client.callback is not None: self._client.callback("event_printer_chamber_image_update") + #LOGGER.debug("JPEG RECIEVED DONE") def get_jpeg(self) -> bytearray: - return self._bytes + #LOGGER.debug("JPEG RETRIEVED") + value = self._bytes.copy() + #LOGGER.debug("JPEG RETRIEVED DONE") + return value def get_last_update_time(self) -> datetime: return self._image_last_updated diff --git a/release_notes.md b/release_notes.md index 559611e0..41a605e3 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,7 @@ +### V2.0.16 +- Handle home assistant shutdown more gracefully +- Fix threading bug causing severe instability for some folk on newer HA versions. + ### V2.0.15 - Add lost fix for usage hours from prototype branch - Fix missed usage hours paused at print end if the AMS fails to retract