import threading import time import logging from snap7.util import * import struct class CacheManager: """PLC数据缓存管理器""" def __init__(self, config, plc_manager, app=None): """ 初始化缓存管理器 Args: config: 配置对象 plc_manager: PLC管理器实例 app: 主应用程序引用(用于配置重载) """ self.plc_manager = plc_manager self.config = config self.app = app self.cache = {} self.refresh_interval = 1 # 1秒刷新一次 self.running = False self.lock = threading.Lock() self.thread = None self.last_update = {} # 区域级最后更新时间 self.plc_last_connected = {} # PLC级最后连接时间 self.plc_connection_status = {} # PLC连接状态 self.logger = logging.getLogger("CacheManager") self.init_cache() def init_cache(self): """初始化缓存结构""" for plc in self.config["plcs"]: plc_name = plc["name"] self.cache[plc_name] = {} self.last_update[plc_name] = {} self.plc_last_connected[plc_name] = 0 # 初始化为0(未连接) self.plc_connection_status[plc_name] = "never_connected" for area in plc["areas"]: name = area["name"] # 确保初始状态为断开 self.cache[plc_name][name] = { "data": bytearray(area["size"]), "db_number": area["db_number"], "offset": area["offset"], "size": area["size"], "type": area["type"], "structure": area.get("structure", []), "status": "disconnected" # 初始状态为断开 } self.last_update[plc_name][name] = 0 def refresh_cache(self): """后台线程:定期刷新缓存""" while self.running: start_time = time.time() try: for plc in self.config["plcs"]: plc_name = plc["name"] refresh_interval = plc.get("refresh_interval", 0.5) client = self.plc_manager.get_plc(plc_name) # 检查PLC连接状态 plc_connected = client.connected # 更新PLC连接状态 with self.lock: if plc_connected: self.plc_last_connected[plc_name] = time.time() self.plc_connection_status[plc_name] = "connected" else: if self.plc_last_connected[plc_name] == 0: self.plc_connection_status[plc_name] = "never_connected" else: self.plc_connection_status[plc_name] = "disconnected" # 刷新所有可读区域 for area in plc["areas"]: if area["type"] in ["read", "read_write"]: name = area["name"] try: data = client.read_db(area["db_number"], area["offset"], area["size"]) # 更新区域状态基于PLC连接状态和读取结果 with self.lock: if plc_connected and data and len(data) == area["size"]: self.cache[plc_name][name]["data"] = bytearray(data) self.cache[plc_name][name]["status"] = "connected" self.last_update[plc_name][name] = time.time() else: self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] # 如果之前有数据,保留旧数据但标记状态 if self.last_update[plc_name][name] > 0: self.logger.info(f"PLC {plc_name} area {name} disconnected but keeping last valid data") except Exception as e: with self.lock: self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] self.logger.warning(f"Error updating status for {plc_name}/{name}: {e}") """计算刷新一个PLC的时间""" # 计算实际执行时间 execution_time = time.time() - start_time #计算需要睡眠的时间,确保总等于refresh_time sleep_time = max(0, refresh_interval - execution_time) time.sleep(sleep_time) # 记录实际刷新间隔 self.logger.debug(f"plc_name: {plc_name}," f"Cache refresh completed.Execution time: {execution_time:.3f}s," f"Sleep time: {sleep_time:.3f}s," f"Total interval: {execution_time + sleep_time:.3f}s") time.sleep(self.refresh_interval) except Exception as e: self.logger.error(f"Error in refresh_cache: {e}") time.sleep(self.refresh_interval) def start(self): """启动缓存刷新线程""" if self.running: return self.running = True self.thread = threading.Thread( target=self.refresh_cache, name="CacheRefreshThread", daemon=True ) self.thread.start() self.logger.info("Cache manager started") def stop(self): """停止缓存刷新线程""" if not self.running: return self.running = False if self.thread: # 等待线程结束,但设置超时防止卡死 self.thread.join(timeout=2.0) if self.thread.is_alive(): self.logger.warning("Cache refresh thread did not terminate gracefully") self.thread = None self.logger.info("Cache manager stopped") def get_plc_connection_status(self, plc_name): """获取PLC连接状态""" with self.lock: return self.plc_connection_status.get(plc_name, "unknown") def get_last_update_time(self, plc_name, area_name): """获取区域数据最后更新时间""" with self.lock: return self.last_update.get(plc_name, {}).get(area_name, 0) def get_summary(self): """获取缓存摘要信息""" summary = {} with self.lock: for plc_name, areas in self.cache.items(): summary[plc_name] = {} for area_name, area in areas.items(): last_update = self.last_update[plc_name][area_name] plc_status = self.plc_connection_status.get(plc_name, "unknown") # 区域状态应与PLC连接状态一致,除非有有效数据 area_status = area["status"] if plc_status == "never_connected": area_status = "never_connected" elif plc_status == "disconnected" and self.last_update[plc_name][area_name] == 0: area_status = "disconnected" summary[plc_name][area_name] = { "status": area_status, "plc_connection_status": plc_status, "last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never", "size": area["size"], "type": area["type"] } return summary def get_area_status(self, plc_name, area_name): """获取区域状态""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return {"status": "not_found", "message": "PLC or area not found"} plc_status = self.plc_connection_status.get(plc_name, "unknown") last_update = self.last_update.get(plc_name, {}).get(area_name, 0) # 区域状态应与PLC连接状态一致,除非有有效数据 area_status = area["status"] if plc_status == "never_connected": area_status = "never_connected" elif plc_status == "disconnected" and last_update == 0: area_status = "disconnected" return { "status": area_status, "plc_connection_status": plc_status, "last_update": last_update, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never", "size": area["size"], "type": area["type"] } def read_area(self, plc_name, area_name, offset, length): """单个区域读取""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) print("read area :",area) if not area: return None, "Area not found", "unknown", 0 if offset + length > area["size"]: return None, "Offset out of bounds", "unknown", 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return None, f"PLC not connected (status: {plc_status})", plc_status, 0 try: data = client.read_db(area["db_number"], area["offset"] + offset, length) # 验证数据有效性 if data and len(data) == length: # 更新缓存中的这部分数据 for i in range(length): area["data"][offset + i] = data[i] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected" return data, None, plc_status, update_time else: area["status"] = plc_status return None, "Invalid data returned", plc_status, 0 except Exception as e: area["status"] = plc_status self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}") return None, f"Read failed: {str(e)}", plc_status, 0 def read_area_bool(self, plc_name, area_name, offset, length): """单个区域读取""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return None, "Area not found", "unknown", 0 if offset + length > area["size"]: return None, "Offset out of bounds", "unknown", 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return None, f"PLC not connected (status: {plc_status})", plc_status, 0 try: data = client.read_db_bool(area["db_number"], area["offset"] + offset, length) # 验证数据有效性 if all(isinstance(val, bool) for val in data.values()): # 按字典键顺序更新多个值 for i, val in data.items(): area["data"][offset + i] = val # 确保offset+i不越界 # area["data"][offset] = data.values update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected" return data, None, plc_status, update_time else: area["status"] = plc_status return None, "Invalid data returned", plc_status, 0 except Exception as e: area["status"] = plc_status self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}") return None, f"Read failed: {str(e)}", plc_status, 0 def write_area(self, plc_name, area_name, offset, data): """单个区域写入""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Area is read-only", plc_status, 0 if offset + len(data) > area["size"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: success = client.write_db(area["db_number"], area["offset"] + offset, data) if success: # 更新缓存中的这部分数据 for i in range(len(data)): area["data"][offset + i] = data[i] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" return True, None, plc_status, update_time else: area["status"] = plc_status return False, "Write failed", plc_status, 0 except Exception as e: area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") return False, f"Write failed: {str(e)}", plc_status, 0 def batch_write_area(self, plc_name, area_name, offset, data): """单个区域写入""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Area is read-only", plc_status, 0 if offset + len(data) > area["size"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: for i, byte in enumerate(data): byte_data = bytes([byte]) current_offset = offset + (i * 2) byte_value = byte_data[0] value = bytearray(2) if isinstance(byte_value, int): set_int(value, 0, byte_value) data = value success = client.batch_write_db(area["db_number"], current_offset, data) if success: # 更新缓存中的这部分数据 for j in range(len(data)): area["data"][offset + j] = data[j] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" else: area["status"] = plc_status return False, "Write failed", plc_status, 0 return True, None, plc_status, update_time except Exception as e: area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") return False, f"Write failed: {str(e)}", plc_status, 0 def batch_write_bool_area(self, plc_name, area_name, offset, data): """单个区域写入""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Area is read-only", plc_status, 0 if offset + len(data) > area["size"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: value = bytearray(offset + 1) for bit, bit_value in enumerate(data): set_bool(value, offset, bit, bit_value) data = value success = client.batch_write_db_bool(area["db_number"], offset, data) if success: # 更新缓存中的这部分数据 for j in range(len(data)): area["data"][offset + j] = data[j] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" else: area["status"] = plc_status return False, "Write failed", plc_status, 0 return True, None, plc_status, update_time except Exception as e: area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") return False, f"Write failed: {str(e)}", plc_status, 0 def write_area_bool(self, plc_name, area_name, offset, data): """单个区域写入""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Area is read-only", plc_status, 0 if offset + len(data) > area["size"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: success = client.write_db_bool(area["db_number"], area["offset"] + offset, data) if success: # 更新缓存中的这部分数据 for i in range(len(data)): area["data"][offset + i] = data[i] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" return True, None, plc_status, update_time else: area["status"] = plc_status return False, "Write failed", plc_status, 0 except Exception as e: area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") return False, f"Write failed: {str(e)}", plc_status, 0 def batch_read(self, requests): """批量读取""" results = [] for req in requests: plc_name = req["plc_name"] area_name = req["area_name"] offset = req.get("offset", 0) length = req.get("length", None) # 获取PLC连接状态 plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": 0, "last_update_formatted": "N/A", "message": f"PLC not connected (status: {plc_status})" }) continue area = self.cache.get(plc_name, {}).get(area_name) if not area: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": 0, "last_update_formatted": "N/A", "message": "Area not found" }) continue # 如果未指定length,读取整个区域 if length is None: length = area["size"] - offset data, error, _, update_time = self.read_area(plc_name, area_name, offset, length) if error: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never", "message": error }) else: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "success", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)), "offset": offset, "length": length, "data": list(data) }) return results def batch_write(self, requests): """批量写入""" results = [] for req in requests: plc_name = req["plc_name"] area_name = req["area_name"] offset = req["offset"] data = bytes(req["data"]) # 获取PLC连接状态 plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": 0, "last_update_formatted": "N/A", "message": f"PLC not connected (status: {plc_status})", "offset": offset }) continue success, error, _, update_time = self.batch_write_area(plc_name, area_name, offset, data) if error: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never", "message": error, "offset": offset }) else: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "success", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)), "offset": offset, "length": len(data) }) return results def batch_write_bool(self, requests): """批量写入""" results = [] for req in requests: plc_name = req["plc_name"] area_name = req["area_name"] offset = req["offset"] data = bytes(req["data"]) # 获取PLC连接状态 plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": 0, "last_update_formatted": "N/A", "message": f"PLC not connected (status: {plc_status})", "offset": offset }) continue success, error, _, update_time = self.batch_write_bool_area(plc_name, area_name, offset, data) if error: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "error", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never", "message": error, "offset": offset }) else: results.append({ "plc_name": plc_name, "area_name": area_name, "status": "success", "plc_connection_status": plc_status, "last_update": update_time, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)), "offset": offset, "length": len(data) }) return results def read_generic(self, plc_name, area_name, offset, data_type, count=1): """通用读取接口""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return None, "Area not found", "unknown", 0 if area["type"] not in ["read", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return None, "Area is read-only", plc_status, 0 array_name = f"{plc_name}_{area_name}" # 计算实际DB偏移 db_offset = area["offset"] + offset # 确保在区域内 if data_type == 'bool': required_size = (offset + count + 7) // 8 elif data_type in ['int', 'word']: required_size = 2 * count elif data_type in ['dint', 'dword', 'real']: required_size = 4 * count else: # byte required_size = count if db_offset + required_size > area["size"] or db_offset < 0: plc_status = self.plc_connection_status.get(plc_name, "unknown") return None, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return None, f"PLC not connected (status: {plc_status})", plc_status, 0 try: # 使用Snap7Client的read_generic方法 result = client.read_generic(area["db_number"], db_offset, data_type, array_name, count) if result is None: return None, "Read failed", plc_status, 0 # 对于bool类型,需要特殊处理缓存 if data_type == 'bool': for i in range(count): byte_offset = offset // 8 + i // 8 bit_offset = (offset % 8) + (i % 8) if bit_offset >= 8: byte_offset += 1 bit_offset -= 8 # 读取当前字节值 current_byte = area["data"][byte_offset] if result[i]: # 设置位为1 new_byte = current_byte | (1 << bit_offset) else: # 设置位为0 new_byte = current_byte & ~(1 << bit_offset) area["data"][byte_offset] = new_byte else: # 对于其他类型,直接更新缓存 if not isinstance(result, list): result = [result] if data_type == 'byte': item_size = 1 elif data_type in ['int', 'word']: item_size = 2 else: # dint, dword, real item_size = 4 for i, val in enumerate(result): item_offset = offset + i * item_size if data_type == 'byte': area["data"][item_offset] = val & 0xFF elif data_type in ['int', 'word']: # 2字节数据 packed = struct.pack(">h" if data_type == "int" else ">H", val) for j in range(2): area["data"][item_offset + j] = packed[j] elif data_type in ['dint', 'dword', 'real']: # 4字节数据 packed = struct.pack( ">l" if data_type == "dint" else ">I" if data_type == "dword" else ">f", val ) for j in range(4): area["data"][item_offset + j] = packed[j] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected" return result, None, plc_status, update_time except Exception as e: area["status"] = plc_status self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}") return None, f"Read failed: {str(e)}", plc_status, 0 def write_generic(self, plc_name, area_name, offset, data_type, value): """通用写入接口""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Area is read-only", plc_status, 0 # 计算实际DB偏移 db_offset = area["offset"] + offset # 确保在区域内 if data_type == 'bool': # 确定存储这些布尔值需要多少字节。 required_size = (offset + (len(value) if isinstance(value, list) else 1) + 7) // 8 elif data_type in ['int', 'word']: required_size = 2 * (len(value) if isinstance(value, list) else 1) elif data_type in ['dint', 'dword', 'real']: required_size = 4 * (len(value) if isinstance(value, list) else 1) else: # byte required_size = len(value) if isinstance(value, list) else 1 if db_offset + required_size > area["size"] or db_offset < 0: plc_status = self.plc_connection_status.get(plc_name, "unknown") return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) plc_status = self.plc_connection_status.get(plc_name, "unknown") # 如果PLC未连接,直接返回错误 if plc_status != "connected": return False, f"PLC not connected (status: {plc_status})", plc_status, 0 try: # 使用Snap7Client的write_generic方法 success = client.write_generic(area["db_number"], db_offset, data_type, value) if success: # 根据数据类型更新缓存 if data_type == 'bool': # 处理bool写入 if not isinstance(value, list): value = [value] for i, val in enumerate(value): byte_offset = offset // 8 + i // 8 bit_offset = (offset % 8) + (i % 8) if bit_offset >= 8: byte_offset += 1 bit_offset -= 8 # 读取当前字节值 current_byte = area["data"][byte_offset] if val: # 设置位为1 new_byte = current_byte | (1 << bit_offset) else: # 设置位为0 new_byte = current_byte & ~(1 << bit_offset) area["data"][byte_offset] = new_byte elif data_type == 'byte': # 处理byte写入 if not isinstance(value, list): value = [value] for i, val in enumerate(value): area["data"][offset + i] = val & 0xFF elif data_type in ['int', 'word']: # 处理int/word写入 if not isinstance(value, list): value = [value] for i, val in enumerate(value): # 2字节数据 packed = struct.pack(">h" if data_type == "int" else ">H", val) for j in range(2): area["data"][offset + i * 2 + j] = packed[j] elif data_type in ['dint', 'dword', 'real']: # 处理dint/dword/real写入 if not isinstance(value, list): value = [value] for i, val in enumerate(value): # 4字节数据 packed = struct.pack( ">l" if data_type == "dint" else ">I" if data_type == "dword" else ">f", val ) for j in range(4): area["data"][offset + i * 4 + j] = packed[j] update_time = time.time() self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" return True, None, plc_status, update_time else: area["status"] = plc_status return False, "Write failed", plc_status, 0 except Exception as e: area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") return False, f"Write failed: {str(e)}", plc_status, 0 def get_parsed_data(self, plc_name, area_name): """获取解析后的数据""" from data_parser import parse_data with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: return {"error": "Area not found"} plc_status = self.plc_connection_status.get(plc_name, "unknown") last_update = self.last_update.get(plc_name, {}).get(area_name, 0) # 区域状态应与PLC连接状态一致,除非有有效数据 area_status = area["status"] if plc_status == "never_connected": area_status = "never_connected" elif plc_status == "disconnected" and last_update == 0: area_status = "disconnected" structure = area.get("structure", []) if structure: parsed = parse_data(area["data"], structure) parsed["status"] = area_status parsed["plc_connection_status"] = plc_status parsed["last_update"] = last_update parsed["last_update_formatted"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never" return parsed else: return { "raw_data": list(area["data"]), "status": area_status, "plc_connection_status": plc_status, "last_update": last_update, "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never" }