@@ -439,6 +439,7 @@ class LuxFrame:
439439 inverter_serial : str = ""
440440 register : int = 0
441441 values : list [int ] = field (default_factory = list )
442+ write_count : int = 0
442443 is_error : bool = False
443444 error_code : int = 0
444445
@@ -629,6 +630,66 @@ def build_write_request(
629630 return bytes (pkt )
630631
631632
633+ def build_write_multi_request (
634+ datalog_serial : str ,
635+ inverter_serial : str ,
636+ start_register : int ,
637+ values : list [int ],
638+ ) -> bytes :
639+ """
640+ Build a WriteMultipleRegisters (0x10) request packet.
641+
642+ Modbus data frame layout:
643+ addr(1) + func(1) + inv_serial(10) + start_reg(2) + count(2)
644+ + byte_count(1) + values(N*2) + crc(2)
645+
646+ The outer LuxPower packet is variable-length: the data length prefix
647+ (bytes 18-19) and the frame length (bytes 4-5) both grow with the number
648+ of registers being written.
649+
650+ Used by the 7-day scheduling block (registers 500-723), which the
651+ inverter only accepts via multi-register writes (no single writes).
652+ """
653+ if not values :
654+ raise ValueError ("WriteMultipleRegisters requires at least one value" )
655+ if len (values ) > 123 :
656+ raise ValueError (f"Too many registers for one write: { len (values )} (max 123)" )
657+ for v in values :
658+ if not (0 <= v <= 0xFFFF ):
659+ raise ValueError (f"Modbus register value out of range: { v } " )
660+
661+ n = len (values )
662+ # Modbus data frame: addr(1)+func(1)+serial(10)+reg(2)+count(2)+bytecount(1)+values(2n)+crc(2)
663+ data_len = 1 + 1 + 10 + 2 + 2 + 1 + (2 * n ) + 2
664+ # Outer packet: header(18) + datalen(2) + data(data_len)
665+ total = 18 + 2 + data_len
666+ frame_len = total - 6
667+
668+ pkt = bytearray (total )
669+ pkt [0 :2 ] = PREFIX
670+ struct .pack_into ('<H' , pkt , 2 , 1 ) # protocol
671+ struct .pack_into ('<H' , pkt , 4 , frame_len ) # frame length
672+ pkt [6 ] = 0x01
673+ pkt [7 ] = TCP_FUNC_TRANSLATED_DATA
674+ pkt [8 :18 ] = _serial_to_bytes (datalog_serial )
675+ struct .pack_into ('<H' , pkt , 18 , data_len ) # data length
676+
677+ pkt [20 ] = 0x00 # address
678+ pkt [21 ] = MODBUS_WRITE_MULTI # function 0x10
679+ pkt [22 :32 ] = _serial_to_bytes (inverter_serial )
680+ struct .pack_into ('<H' , pkt , 32 , start_register )
681+ struct .pack_into ('<H' , pkt , 34 , n ) # register count
682+ pkt [36 ] = 2 * n # byte count
683+ for i , v in enumerate (values ):
684+ struct .pack_into ('<H' , pkt , 37 + i * 2 , v )
685+
686+ crc_start = 20
687+ crc_end = 37 + 2 * n
688+ crc = crc16_modbus (bytes (pkt [crc_start :crc_end ]))
689+ struct .pack_into ('<H' , pkt , crc_end , crc )
690+ return bytes (pkt )
691+
692+
632693# ── Frame Parser ────────────────────────────────────────────────────
633694
634695def parse_frame (data : bytes ) -> Optional [LuxFrame ]:
@@ -712,6 +773,14 @@ def _parse_data_frame(data: bytes, frame: LuxFrame) -> None:
712773 if offset + 2 <= len (data ) - 2 : # -2 for CRC
713774 val = struct .unpack_from ('<H' , data , offset )[0 ]
714775 frame .values .append (val )
776+ elif frame .device_function == MODBUS_WRITE_MULTI :
777+ # Write-multiple (0x10) echo: addr + func + serial + start_reg(2)
778+ # + count(2) + crc. It echoes the start register and count, NOT the
779+ # values. We store the count in a dedicated field so the caller can
780+ # confirm the write was accepted.
781+ if len (data ) >= reg_start + 4 + 2 : # reg + count + crc
782+ frame .register = struct .unpack_from ('<H' , data , reg_start )[0 ]
783+ frame .write_count = struct .unpack_from ('<H' , data , reg_start + 2 )[0 ]
715784
716785
717786def find_frames (data : bytes ) -> list [LuxFrame ]:
0 commit comments