protocol.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. from dataclasses import dataclass
  2. from enum import Enum
  3. import struct
  4. from typing import List, Dict, Callable, Any
  5. from .crc import crc16
  6. type Value = str|int|float
  7. class FunctionCodes(Enum):
  8. # Read
  9. READ_STATUS_REGISTER = 0x02 # Read the switch input status
  10. READ_PARAMETER = 0x03 # Read multiple hold registers
  11. READ_MEMORY = 0x04 # Read input register
  12. # Write
  13. WRITE_STATUS_REGISTER = 0x05 # Write single register
  14. WRITE_MEMORY_SINGLE = 0x06 # Write single hold register
  15. WRITE_MEMORY_RANGE = 0x10 # Write multiple hold registers
  16. @dataclass
  17. class Variable():
  18. address: int
  19. is_32_bit: bool
  20. is_signed: bool
  21. function_codes: List[int]
  22. unit: str
  23. multiplier: int
  24. name: str
  25. friendly_name: str
  26. func: Callable[int, str]
  27. class LumiaxClient:
  28. def _get_functional_status_registers(self, function_codes: list[int], offset: int):
  29. return [
  30. # Controller functional status 1
  31. Variable(offset, False, False, function_codes, "", 0, "maximum_system_voltage_level", "Maximum system voltage level",
  32. lambda x: ["", "12V", "24V", "36V", "48V"][(x >> 12) & 0xF]),
  33. Variable(offset, False, False, function_codes, "", 0, "minimum_system_voltage_level", "Minimum system voltage level",
  34. lambda x: ["", "12V", "24V", "36V", "48V"][(x >> 8) & 0xF]),
  35. Variable(offset, False, False, function_codes, "", 0, "controller_series", "Controller Series",
  36. lambda x: ["MT series", "DC series", "SMR series"][(x >> 4) & 0xF]),
  37. Variable(offset, False, False, function_codes, "", 0, "battery_type", "Battery type",
  38. lambda x: ["Lithium battery", "Non Lithium battery"][(x >> 0) & 0xF]),
  39. # Controller functional status 2
  40. Variable(offset + 1, False, False, function_codes, "", 0, "infrared_function_available", "Is infrared function available",
  41. lambda x: (x >> 15) & 1 == 1),
  42. Variable(offset + 1, False, False, function_codes, "", 0, "automatic_power_reduction_available", "Is automatic power reduction setting available(only in 365 mode)",
  43. lambda x: (x >> 14) & 1 == 1),
  44. Variable(offset + 1, False, False, function_codes, "", 0, "charging_at_zero_celsius_available", "Is 0°C prohibit charging setting available",
  45. lambda x: (x >> 13) & 1 == 1),
  46. Variable(offset + 1, False, False, function_codes, "", 0, "grade_of_rated_voltage_available", "Is grade of rated voltage setting available",
  47. lambda x: (x >> 12) & 1 == 1),
  48. Variable(offset + 1, False, False, function_codes, "", 0, "overcharge_recovery_voltage_available", "Is overcharge recovery voltage setting available (only lithium battery)",
  49. lambda x: (x >> 11) & 1 == 1),
  50. Variable(offset + 1, False, False, function_codes, "", 0, "overcharge_protection_available", "Is overcharge protection setting available (only lithium battery)",
  51. lambda x: (x >> 10) & 1 == 1),
  52. Variable(offset + 1, False, False, function_codes, "", 0, "floating_charge_voltage_available", "Is floating charge voltage setting available",
  53. lambda x: (x >> 9) & 1 == 1),
  54. Variable(offset + 1, False, False, function_codes, "", 0, "equilibrium_charge_voltage_available", "Is equilibrium charge voltage setting available",
  55. lambda x: (x >> 8) & 1 == 1),
  56. Variable(offset + 1, False, False, function_codes, "", 0, "strong_charging_voltage_available", "Is strong charging voltage setting available",
  57. lambda x: (x >> 7) & 1 == 1),
  58. Variable(offset + 1, False, False, function_codes, "", 0, "low_voltage_recovery_voltage_available", "Is low voltage recovery setting available",
  59. lambda x: (x >> 6) & 1 == 1),
  60. Variable(offset + 1, False, False, function_codes, "", 0, "low_voltage_protection_voltage_available", "Is low voltage protection setting available",
  61. lambda x: (x >> 5) & 1 == 1),
  62. Variable(offset + 1, False, False, function_codes, "", 0, "battery_type_available", "Is Battery Type setting available",
  63. lambda x: (x >> 4) & 1 == 1),
  64. Variable(offset + 1, False, False, function_codes, "", 0, "backlight_time_available", "Is Backlight Time setting available",
  65. lambda x: (x >> 3) & 1 == 1),
  66. Variable(offset + 1, False, False, function_codes, "", 0, "device_time_available", "Is Device Time setting available",
  67. lambda x: (x >> 2) & 1 == 1),
  68. Variable(offset + 1, False, False, function_codes, "", 0, "device_id_available", "Is Device ID setting available",
  69. lambda x: (x >> 1) & 1 == 1),
  70. Variable(offset + 1, False, False, function_codes, "", 0, "device_password_available", "Is Device password setting available",
  71. lambda x: (x >> 0) & 1 == 1),
  72. # Controller functional status 3
  73. Variable(offset + 2, False, False, function_codes, "", 0, "six_time_frame_mode_available", "Is Six Time Frame Mode available",
  74. lambda x: (x >> 7) & 1 == 1),
  75. Variable(offset + 2, False, False, function_codes, "", 0, "five_time_frame_mode_available", "Is Five Time Frame Mode available",
  76. lambda x: (x >> 6) & 1 == 1),
  77. Variable(offset + 2, False, False, function_codes, "", 0, "timing_control_mode_available", "Is Timing Control available",
  78. lambda x: (x >> 5) & 1 == 1),
  79. Variable(offset + 2, False, False, function_codes, "", 0, "t0t_mode_available", "Is T0T Mode available",
  80. lambda x: (x >> 4) & 1 == 1),
  81. Variable(offset + 2, False, False, function_codes, "", 0, "fixed_duration_mode_available", "Is Fixed Light Up Duration Mode available",
  82. lambda x: (x >> 3) & 1 == 1),
  83. Variable(offset + 2, False, False, function_codes, "", 0, "d2d_mode_available", "Is D2D Mode available",
  84. lambda x: (x >> 2) & 1 == 1),
  85. Variable(offset + 2, False, False, function_codes, "", 0, "24h_mode_available", "Is 24H Mode available",
  86. lambda x: (x >> 1) & 1 == 1),
  87. Variable(offset + 2, False, False, function_codes, "", 0, "manual_operation_mode_available", "Is Manual Operation Mode available",
  88. lambda x: (x >> 0) & 1 == 1),
  89. # Controller functional status 4 (reserved)
  90. # Variable(offset + 3, False, False, function_codes, "", 0, "controller_functional_status_4", "Controller functional status 4", None),
  91. ]
  92. def _get_status_registers(self, offset: int):
  93. return [
  94. # Battery status
  95. Variable(offset, False, False, [0x04], "", 0, "battery_temperature_protection_status", "Battery temperature protection status",
  96. lambda x: ["Normal", "High temperature protection"][(x >> 4) & 0x1]),
  97. Variable(offset, False, False, [0x04], "", 0, "battery_voltage_protection_status", "Battery voltage protection status",
  98. lambda x: ["Normal", "Over voltage protection", "Voltage is low", "Low voltage protection"][(x >> 0) & 0xF]),
  99. # Charge status
  100. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_charge_disabled", "Is charging manually disabled",
  101. lambda x: (x >> 6) & 0x1 == 1),
  102. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_is_night", "Is solar panel night",
  103. lambda x: (x >> 5) & 0x1 == 1),
  104. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_charge_over_temperature", "Is charge over temperature",
  105. lambda x: (x >> 4) & 0x1 == 1),
  106. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_charge_state", "Solar panel charge status",
  107. lambda x: ["Not charging", "Float charge", "Boost charge", "Equal charge"][(x >> 2) & 0x3]),
  108. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_charge_state", "Is charge fault",
  109. lambda x: (x >> 1) & 0x1 == 1),
  110. Variable(offset + 1, False, False, [0x04], "", 0, "solar_panel_is_charging", "Solar panel is charging",
  111. lambda x: (x >> 0) & 0x1 == 1),
  112. # Discharge status
  113. Variable(offset + 2, False, False, [0x04], "", 0, "load_state", "Load status",
  114. lambda x: ["Light load", "Moderate load", "Rated load", "Overload"][(x >> 12) & 0x3]),
  115. Variable(offset + 2, False, False, [0x04], "", 0, "output_short_circuit", "Is output short circuit",
  116. lambda x: (x >> 11) & 0x1 == 1),
  117. Variable(offset + 2, False, False, [0x04], "", 0, "output_hardware_protection", "Is output hardware protection",
  118. lambda x: (x >> 4) & 0x1 == 1),
  119. Variable(offset + 2, False, False, [0x04], "", 0, "output_open_circuit_protection", "Is output open circuit protection",
  120. lambda x: (x >> 3) & 0x1 == 1),
  121. Variable(offset + 2, False, False, [0x04], "", 0, "output_over_temperature", "Is output over temperature",
  122. lambda x: (x >> 2) & 0x1 == 1),
  123. Variable(offset + 2, False, False, [0x04], "", 0, "output_fault", "Is output fault",
  124. lambda x: (x >> 1) & 0x1 == 1),
  125. Variable(offset + 2, False, False, [0x04], "", 0, "load_is_enabled", "Is load enabled",
  126. lambda x: (x >> 0) & 0x1 == 1),
  127. ]
  128. def __init__(self):
  129. # List of addresses to variable information
  130. self.variables = [
  131. Variable(0x2000, False, False, [0x02], "", 0, "equipment_internal_over_temperature", "Equipment internal over temperature",
  132. lambda x: ["Normal", "Over temperature"][x]),
  133. Variable(0x200C, False, False, [0x02], "", 0, "day_or_night", "Day or night",
  134. lambda x: ["Day", "Night"][x]),
  135. ] + self._get_functional_status_registers([0x04], 0x3011) + [
  136. Variable(0x3015, False, False, [0x04], "V", 100, "lvd_min_setting_value", "Low voltage detect min setting value", None),
  137. Variable(0x3016, False, False, [0x04], "V", 100, "lvd_max_setting_value", "Low voltage detect max setting value", None),
  138. Variable(0x3017, False, False, [0x04], "V", 100, "lvd_default_setting_value", "Low voltage detect default setting value", None),
  139. Variable(0x3018, False, False, [0x04], "V", 100, "lvr_min_setting_value", "Low voltage recovery min setting value", None),
  140. Variable(0x3019, False, False, [0x04], "V", 100, "lvr_max_setting_value", "Low voltage recovery max setting value", None),
  141. Variable(0x301A, False, False, [0x04], "V", 100, "lvr_default_setting_value", "Low voltage recovery default setting value", None),
  142. Variable(0x301B, False, False, [0x04], "V", 100, "cvt_min_setting_value", "Charge target voltage min setting value for Li Series controller", None),
  143. Variable(0x301C, False, False, [0x04], "V", 100, "cvt_max_setting_value", "Charge target voltage max setting value for Li Series controller", None),
  144. Variable(0x301D, False, False, [0x04], "V", 100, "cvt_default_setting_value", "Charge target voltage default setting value Li Series controller", None),
  145. Variable(0x301E, False, False, [0x04], "V", 100, "cvr_min_setting_value", "Charge recovery voltage min setting value Li Series controller", None),
  146. Variable(0x301F, False, False, [0x04], "V", 100, "cvr_max_setting_value", "Charge recovery voltage max setting value Li Series controller", None),
  147. Variable(0x3020, False, False, [0x04], "V", 100, "cvr_default_setting_value", "Charge recovery voltage default setting value Li Series controller", None),
  148. Variable(0x3021, False, False, [0x04], "V", 100, "day_night_threshold_voltage_min", "Day/Night threshold voltage min setting value", None),
  149. Variable(0x3022, False, False, [0x04], "V", 100, "day_night_threshold_voltage_max", "Day/Night threshold voltage max setting value", None),
  150. Variable(0x3023, False, False, [0x04], "V", 100, "day_night_threshold_voltage_default", "Day/Night threshold voltage default setting value", None),
  151. Variable(0x3024, False, False, [0x04], "V", 100, "dimming_voltage_min", "Dimming voltage min setting value", None),
  152. Variable(0x3025, False, False, [0x04], "V", 100, "dimming_voltage_max", "Dimming voltage max setting value", None),
  153. Variable(0x3026, False, False, [0x04], "V", 100, "dimming_voltage_default", "Dimming voltage default setting value", None),
  154. Variable(0x3027, False, False, [0x04], "A", 100, "load_current_min", "Load current min setting value", None),
  155. Variable(0x3028, False, False, [0x04], "A", 100, "load_current_max", "Load current max setting value", None),
  156. Variable(0x3029, False, False, [0x04], "V", 100, "cvt_cvr_max_dropout_voltage", "Charge target and recovery voltage max allow dropout voltage for Li-series controller", None),
  157. Variable(0x302A, False, False, [0x04], "V", 100, "cvt_cvr_min_dropout_voltage", "Charge target and recovery voltage min allow dropout voltage for Li-series controller", None),
  158. Variable(0x302B, False, False, [0x04], "V", 100, "lvd_lvr_min_dropout_voltage", "Low voltage detect and recovery min allow dropout voltage", None),
  159. Variable(0x302C, False, False, [0x04], "V", 100, "min_allow_dropout_voltage", "CVR and LVD & CVT and LVR Min allow dropout voltage", None),
  160. Variable(0x3030, False, False, [0x04], "", 0, "equipment_id", "Equipment ID", None),
  161. Variable(0x3031, False, False, [0x04], "", 0, "run_days", "Number of running days", None),
  162. Variable(0x3032, False, False, [0x04], "V", 100, "battery_voltage_level", "Current battery voltage level", None),
  163. ] + self._get_status_registers(0x3033) + [
  164. Variable(0x3036, False, False, [0x04], "℃", 100, "environment_temperature", "Environment temperature", None),
  165. Variable(0x3037, False, False, [0x04], "℃", 100, "device_built_in_temperature", "Device built-intemperature", None),
  166. Variable(0x3038, False, False, [0x04], "", 0, "battery_empty_times", "Battery empty times", None),
  167. Variable(0x3039, False, False, [0x04], "", 0, "battery_full_times", "Battery full times", None),
  168. Variable(0x303A, False, False, [0x04], "", 0, "over_voltage_protection_times", "Over-voltage protection times", None),
  169. Variable(0x303B, False, False, [0x04], "", 0, "over_current_protection_times", "Over-current protection times", None),
  170. Variable(0x303C, False, False, [0x04], "", 0, "short_circuit_protection_times", "short-circuit protection times", None),
  171. Variable(0x303D, False, False, [0x04], "", 0, "open_circuit_protection_times", "Open-circuit protection times", None),
  172. Variable(0x303E, False, False, [0x04], "", 0, "hardware_protection_times", "Hardware protection times", None),
  173. Variable(0x303F, False, False, [0x04], "", 0, "charge_over_temperature_protection_times", "Charge over-temperature protection times", None),
  174. Variable(0x3040, False, False, [0x04], "", 0, "discharge_over_temperature_protection_times", "Discharge over-temperature protection time", None),
  175. Variable(0x3045, False, False, [0x04], "%", 1, "battery_percentage", "Battery remaining capacity", None),
  176. Variable(0x3046, False, False, [0x04], "V", 100, "battery_voltage", "Battery voltage", None),
  177. Variable(0x3047, False, True, [0x04], "A", 100, "battery_current", "Battery current", None),
  178. Variable(0x3048, True, True, [0x04], "W", 100, "battery_power", "Battery power", None),
  179. Variable(0x304A, False, False, [0x04], "V", 100, "load_voltage", "Load voltage", None),
  180. Variable(0x304B, False, False, [0x04], "A", 100, "load_current", "Load current", None),
  181. Variable(0x304C, True, False, [0x04], "W", 100, "load_power", "Load power", None),
  182. Variable(0x304E, False, False, [0x04], "V", 100, "solar_panel_voltage", "Solar panel voltage", None),
  183. Variable(0x304F, False, False, [0x04], "A", 100, "solar_panel_current", "Solar panel current", None),
  184. Variable(0x3050, True, False, [0x04], "W", 100, "solar_panel_power", "Solar panel power", None),
  185. Variable(0x3052, False, False, [0x04], "kWh", 100, "solar_panel_daily_energy", "The charging capacity of the day", None),
  186. Variable(0x3053, True, False, [0x04], "kWh", 100, "solar_panel_total_energy", "Total charging capacity", None),
  187. Variable(0x3055, True, False, [0x04], "kWh", 100, "load_daily_energy", "The electricity consumption of the day", None),
  188. Variable(0x3056, True, False, [0x04], "kWh", 100, "load_total_energy", "Total electricity consumption", None),
  189. Variable(0x3058, False, False, [0x04], "Min", 0, "total_light_time_during_the_day", "Total light time during the day", None),
  190. Variable(0x309D, False, False, [0x04], "", 0, "run_days", "The number of running days", None),
  191. Variable(0x30A0, False, False, [0x04], "V", 100, "battery_voltage", "Battery voltage", None),
  192. Variable(0x30A1, False, True, [0x04], "A", 100, "battery_current", "Battery current", None),
  193. Variable(0x30A2, False, False, [0x04], "℃", 100, "environment_temperature", "Environment temperature", None),
  194. ] + self._get_status_registers(0x30A3) + [
  195. Variable(0x30A6, False, False, [0x04], "", 0, "battery_empty_times", "Battery empty times", None),
  196. Variable(0x30A7, False, False, [0x04], "", 0, "battery_full_times", "Battery full times", None),
  197. Variable(0x30A8, False, False, [0x04], "V", 100, "battery_daily_voltage_maximum", "The highest battery voltage today", None),
  198. Variable(0x30A9, False, False, [0x04], "V", 100, "battery_daily_voltage_minimum", "The lowest battery voltage today", None),
  199. Variable(0x3125, False, False, [0x04], "V", 100, "load_voltage", "Load voltage", None),
  200. Variable(0x3126, False, False, [0x04], "A", 100, "load_current", "Load current", None),
  201. Variable(0x3127, True, False, [0x04], "W", 100, "load_power", "Load power", None),
  202. Variable(0x3129, False, False, [0x04], "kWh", 100, "load_daily_energy", "The electricity consumption of the day", None),
  203. Variable(0x312E, True, False, [0x04], "kWh", 100, "load_total_energy", "Total electricity consumption", None),
  204. Variable(0x316C, False, False, [0x04], "", 0, "run_days", "The number of running days", None),
  205. # Factory settings
  206. Variable(0x3000, False, False, [0x04], "V", 100, "solar_panel_rated_voltage", "Solar panel rated voltage", None),
  207. Variable(0x3001, False, False, [0x04], "A", 100, "solar_panel_rated_current", "Solar panel rated current", None),
  208. Variable(0x3002, True, False, [0x04], "W", 100, "solar_panel_rated_power", "Solar panel rated power", None),
  209. Variable(0x3004, False, False, [0x04], "V", 100, "battery_rated_voltage", "Battery rated voltage", None),
  210. Variable(0x3005, False, False, [0x04], "A", 100, "battery_rated_current", "Battery rated current", None),
  211. Variable(0x3006, True, False, [0x04], "W", 100, "battery_rated_power", "Battery rated power", None),
  212. Variable(0x3008, False, False, [0x04], "V", 100, "load_rated_voltage", "Load rated voltage", None),
  213. Variable(0x3009, False, False, [0x04], "A", 100, "load_rated_current", "Load rated current", None),
  214. Variable(0x300A, True, False, [0x04], "W", 100, "load_rated_power", "Load rated power", None),
  215. ] + self._get_functional_status_registers([0x03], 0x8FF0) + [
  216. Variable(0x8FF4, False, False, [0x03], "V", 100, "lvd_min_setting_value", "Low voltage detect min setting value", None),
  217. Variable(0x8FF5, False, False, [0x03], "V", 100, "lvd_max_setting_value", "Low voltage detect max setting value", None),
  218. Variable(0x8FF6, False, False, [0x03], "V", 100, "lvd_default_setting_value", "Low voltage detect default setting value", None),
  219. Variable(0x8FF7, False, False, [0x03], "V", 100, "lvr_min_setting_value", "Low voltage recovery min setting value", None),
  220. Variable(0x8FF8, False, False, [0x03], "V", 100, "lvr_max_setting_value", "Low voltage recovery max setting value", None),
  221. Variable(0x8FF9, False, False, [0x03], "V", 100, "lvr_default_setting_value", "Low voltage recovery default setting value", None),
  222. Variable(0x8FFA, False, False, [0x03], "V", 100, "cvt_min_setting_value", "Charge target voltage min setting value for Li Series controller", None),
  223. Variable(0x8FFB, False, False, [0x03], "V", 100, "cvt_max_setting_value", "Charge target voltage max setting value Li Series controller", None),
  224. Variable(0x8FFC, False, False, [0x03], "V", 100, "cvt_default_setting_value", "Charge target voltage default setting value Li Series controller", None),
  225. Variable(0x8FFD, False, False, [0x03], "V", 100, "cvr_min_setting_value", "Charge recovery voltage min setting value Li Series controller", None),
  226. Variable(0x8FFE, False, False, [0x03], "V", 100, "cvr_max_setting_value", "Charge recovery voltage max setting value Li Series controller", None),
  227. Variable(0x8FFF, False, False, [0x03], "V", 100, "cvr_default_setting_value", "Charge recovery voltage default setting value Li Series controller", None),
  228. Variable(0x9000, False, False, [0x03], "V", 100, "day_night_threshold_voltage_min", "Day/Night threshold voltage min setting value", None),
  229. Variable(0x9001, False, False, [0x03], "V", 100, "day_night_threshold_voltage_max", "Day/Night threshold voltage max setting value", None),
  230. Variable(0x9002, False, False, [0x03], "V", 100, "day_night_threshold_voltage_default", "Day/Night threshold voltage default setting value", None),
  231. Variable(0x9003, False, False, [0x03], "V", 100, "dimming_voltage_min", "Dimming voltage min setting value", None),
  232. Variable(0x9004, False, False, [0x03], "V", 100, "dimming_voltage_max", "Dimming voltage max setting value", None),
  233. Variable(0x9005, False, False, [0x03], "V", 100, "dimming_voltage_default", "Dimming voltage default setting value", None),
  234. Variable(0x9006, False, False, [0x03], "A", 100, "load_current_min", "Load current min setting value", None),
  235. Variable(0x9007, False, False, [0x03], "A", 100, "load_current_max", "Load current max setting value", None),
  236. Variable(0x9008, False, False, [0x03], "V", 100, "battery_voltage_level", "Current battery voltage level", None),
  237. Variable(0x9009, False, False, [0x03], "V", 100, "cvt_cvr_max_dropout_voltage", "Charge target and recovery voltage max allow dropout voltage for Li-series controller", None),
  238. Variable(0x900A, False, False, [0x03], "V", 100, "cvt_cvr_min_dropout_voltage", "Charge target and recovery voltage min allow dropout voltage for Li-series controller", None),
  239. Variable(0x900B, False, False, [0x03], "V", 100, "lvd_lvr_min_dropout_voltage", "Low voltage detect and recovery min allow dropout voltage", None),
  240. Variable(0x900C, False, False, [0x03], "V", 100, "min_allow_dropout_voltage", "CVR and LVD & CVT and LVR Min allow dropout voltage", None),
  241. Variable(0x9017, False, False, [0x03, 0x06, 0x10], "ss", 0, "real_time_clock_second", "Real-time clock second", None),
  242. Variable(0x9018, False, False, [0x03, 0x06, 0x10], "mm", 0, "real_time_clock_minute", "Real-time clock minute", None),
  243. Variable(0x9019, False, False, [0x03, 0x06, 0x10], "hh", 0, "real_time_clock_hour", "Real-time clock hour", None),
  244. Variable(0x901A, False, False, [0x03, 0x06, 0x10], "dd", 0, "real_time_clock_day", "Real-time clock day", None),
  245. Variable(0x901B, False, False, [0x03, 0x06, 0x10], "MM", 0, "real_time_clock_month", "Real-time clock month", None),
  246. Variable(0x901C, False, False, [0x03, 0x06, 0x10], "yy", 0, "real_time_clock_year", "Real-time clock year (00-99)", None),
  247. Variable(0x901D, False, False, [0x03, 0x06, 0x10], "", 0, "baud_rate", "Baud rate",
  248. lambda x: [4800, 9600, 19200, 57600, 115200][x & 0xF]),
  249. Variable(0x901E, False, False, [0x03, 0x06, 0x10], "s", 0, "backlight_time", "Backlight time", None),
  250. Variable(0x901F, False, False, [0x03, 0x06, 0x10], "", 0, "device_password", "Device password",
  251. lambda x: str(max((x>>12) & 0xF, 9)) +
  252. str(max((x>> 8) & 0xF, 9)) +
  253. str(max((x>> 4) & 0xF, 9)) +
  254. str(max((x>> 0) & 0xF, 9))),
  255. Variable(0x9020, False, False, [0x03, 0x06, 0x10], "", 0, "slave_id", "Slave ID", None),
  256. Variable(0x9021, False, False, [0x03, 0x06, 0x10], "", 0, "battery_type", "Battery type",
  257. lambda x: ["Lithium", "Liquid", "GEL", "AGM"][(x >> 0) & 0xF]),
  258. Variable(0x9022, False, False, [0x03, 0x06, 0x10], "V", 100, "low_voltage_protection_voltage", "Low voltage protection", None),
  259. Variable(0x9023, False, False, [0x03, 0x06, 0x10], "V", 100, "low_voltage_recovery_voltage", "Low voltage recovery", None),
  260. Variable(0x9024, False, False, [0x03, 0x06, 0x10], "V", 100, "boost_voltage", "Boost voltage", None),
  261. Variable(0x9025, False, False, [0x03, 0x06, 0x10], "V", 100, "equalizing_voltage", "Equalizing voltage", None),
  262. Variable(0x9026, False, False, [0x03, 0x06, 0x10], "V", 100, "float_voltage", "Float voltage", None),
  263. Variable(0x9027, False, False, [0x03, 0x06, 0x10], "", 0, "system_rated_voltage_level", "System rated voltage level",
  264. lambda x: ["Auto", "12V", "24V", "36V", "48V", "60V", "110V", "120V", "220V", "240V"][x]),
  265. Variable(0x9028, False, False, [0x03, 0x06, 0x10], "V", 100, "charge_target_voltage_for_lithium", "Charge target voltage for lithium", None),
  266. Variable(0x9029, False, False, [0x03, 0x06, 0x10], "V", 100, "charge_recovery_voltage_for_lithium", "Charge recovery voltage for lithium", None),
  267. Variable(0x902A, False, False, [0x03, 0x06, 0x10], "", 0, "charging_at_zero_celsius", "0°C charging",
  268. lambda x: ["Normal charging", "No charging", "Slow charging"][x & 0xF]),
  269. Variable(0x902B, False, False, [0x03, 0x06, 0x10], "", 0, "mt_series_load_mode", "Load mode for MT series controller",
  270. lambda x: (["Always on", "Dusk to dawn"] +
  271. [f"Night light on time {n} hours" for n in range(2, 10)] +
  272. ["Manual", "T0T", "Timing switch"])[x]),
  273. Variable(0x902C, False, False, [0x03, 0x06, 0x10], "", 0, "mt_series_manual_control_default", "MT Series manual control mode default setting",
  274. lambda x: ["On", "Off"][x]),
  275. Variable(0x902D, False, False, [0x03, 0x06, 0x10], "Min", 0, "mt_series_timing_period_1", "MT Series timing opening period 1",
  276. lambda x: ((x >> 8) & 0xFF) * 60 + max(x & 0xFF, 59)),
  277. Variable(0x902E, False, False, [0x03, 0x06, 0x10], "", 0, "mt_series_timing_period_2", "MT Series timing opening period 2",
  278. lambda x: ((x >> 8) & 0xFF) * 60 + max(x & 0xFF, 59)),
  279. Variable(0x902F, False, False, [0x03, 0x06, 0x10], "sec", 0, "timed_start_time_1_seconds", "Timed start time 1-seconds", None),
  280. Variable(0x9030, False, False, [0x03, 0x06, 0x10], "Min", 0, "timed_start_time_1_minutes", "Timed start time 1-minute", None),
  281. Variable(0x9031, False, False, [0x03, 0x06, 0x10], "hour", 0, "timed_start_time_1_hours", "Timed start time 1-hour", None),
  282. Variable(0x9032, False, False, [0x03, 0x06, 0x10], "sec", 0, "timed_off_time_1_seconds", "Timed off time 1-seconds", None),
  283. Variable(0x9033, False, False, [0x03, 0x06, 0x10], "Min", 0, "timed_off_time_1_minutes", "Timed off time 1-minute", None),
  284. Variable(0x9034, False, False, [0x03, 0x06, 0x10], "hour", 0, "timed_off_time_1_hours", "Timed off time 1-hour", None),
  285. Variable(0x9035, False, False, [0x03, 0x06, 0x10], "sec", 0, "timed_start_time_2_seconds", "Timed start time 2-seconds", None),
  286. Variable(0x9036, False, False, [0x03, 0x06, 0x10], "Min", 0, "timed_start_time_2_minutes", "Timed start time 2-minute", None),
  287. Variable(0x9037, False, False, [0x03, 0x06, 0x10], "hour", 0, "timed_start_time_2_hours", "Timed start time 2-hour", None),
  288. Variable(0x9038, False, False, [0x03, 0x06, 0x10], "sec", 0, "timed_off_time_2_seconds", "Timed off time 2-seconds", None),
  289. Variable(0x9039, False, False, [0x03, 0x06, 0x10], "Min", 0, "timed_off_time_2_minutes", "Timed off time 2-minute", None),
  290. Variable(0x903A, False, False, [0x03, 0x06, 0x10], "hour", 0, "timed_off_time_2_hours", "Timed off time 2-hour", None),
  291. Variable(0x903B, False, False, [0x03, 0x06, 0x10], "", 0, "time_control_period_selection", "Time control period selection",
  292. lambda x: ["1 period", "2 periods"][x]),
  293. Variable(0x903C, False, False, [0x03, 0x06, 0x10], "V", 100, "light_controlled_dark_voltage", "Light controlled dark voltage", None),
  294. Variable(0x903D, False, False, [0x03, 0x06, 0x10], "Min", 0, "day_night_delay_time", "Day/Night delay time", None),
  295. Variable(0x903E, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_timing_control_time_1_dimming", "DC series timing control time 1 dimming", None),
  296. Variable(0x903F, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_timing_control_time_2_dimming", "DC series timing control time 2 dimming", None),
  297. Variable(0x9040, False, False, [0x03, 0x06, 0x10], "Min", 1.0/30, "dc_series_time_1", "DC Series time 1", None),
  298. Variable(0x9041, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_time_1_dimming", "DC Series the time 1 dimming", None),
  299. Variable(0x9042, False, False, [0x03, 0x06, 0x10], "Min", 1.0/30, "dc_series_time_2", "DC Series time 2", None),
  300. Variable(0x9043, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_time_2_dimming", "DC Series the time 2 dimming", None),
  301. Variable(0x9044, False, False, [0x03, 0x06, 0x10], "Sec", 1.0/30, "dc_series_time_3", "DC Series time 3", None),
  302. Variable(0x9045, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_time_3_dimming", "DC Series the time 3 dimming", None),
  303. Variable(0x9046, False, False, [0x03, 0x06, 0x10], "Sec", 1.0/30, "dc_series_time_4", "DC Series time 4", None),
  304. Variable(0x9047, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_time_4_dimming", "DC Series the time 4 dimming", None),
  305. Variable(0x9048, False, False, [0x03, 0x06, 0x10], "Sec", 1.0/30, "dc_series_time_5", "DC Series time 5", None),
  306. Variable(0x9049, False, False, [0x03, 0x06, 0x10], "%", 0.1, "dc_series_time_5_dimming", "DC Series the time 5 dimming", None),
  307. Variable(0x904A, False, False, [0x03, 0x06, 0x10], "A", 100, "dc_series_load_current_limit", "DC Series load current limit", None),
  308. Variable(0x904B, False, False, [0x03, 0x06, 0x10], "", 0, "dc_series_auto_dimming", "DC Series auto dimming",
  309. lambda x: ["Auto dimming", "365 mode", "No dimming", "No dimming"][x & 0xF]),
  310. Variable(0x904C, False, False, [0x03, 0x06, 0x10], "V", 100, "dc_series_dimming_voltage", "DC Series dimming voltage", None),
  311. Variable(0x904D, False, False, [0x03, 0x06, 0x10], "%", 0, "dc_series_dimming_percentage", "DC Series dimming percentage", None),
  312. Variable(0x904E, False, False, [0x03, 0x06, 0x10], "Sec", 0.1, "sensing_delay_off_time", "Sensing delay off time", None),
  313. Variable(0x904F, False, False, [0x03, 0x06, 0x10], "%", 0.1, "infrared_dimming_when_no_people", "Dimming of Infrared Series controller when no people", None),
  314. Variable(0x9052, False, False, [0x03, 0x06, 0x10], "", 0, "light_controlled_switch", "Light controlled switch",
  315. lambda x: ["Off", "On"][x]),
  316. Variable(0x9053, False, False, [0x03, 0x06, 0x10], "V", 100, "light_controlled_daybreak_voltage", "Light-control led daybreak voltage", None),
  317. Variable(0x9054, False, False, [0x03, 0x06, 0x10], "%", 0, "dimming_percentage", "Dimming percentage for load test", None),
  318. Variable(0x9069, False, False, [0x03, 0x06, 0x10], "A", 100, "maximum_charging_current_setting", "Maximum charging current setting", None),
  319. Variable(0x906A, False, False, [0x03, 0x06, 0x10], "℃", 100, "over_temperature_protection", "Over temperature protection", None),
  320. Variable(0x0000, False, False, [0x05], "", 0, "manual_control_switch", "Manual control switch",
  321. lambda x: ["Off", "On"][x]),
  322. Variable(0x0001, False, False, [0x05], "", 0, "test_key_trigger", "Test key on/off",
  323. lambda x: ["Off", "On"][x]),
  324. Variable(0x0002, False, False, [0x05], "", 0, "dc_series_timing_control_mode_switch", "DC Series timing control mode switch",
  325. lambda x: ["Off", "On"][x]),
  326. Variable(0x0003, False, False, [0x05], "", 0, "manual_control_charging_switch", "Manual control charging switch",
  327. lambda x: ["Off", "On"][x]),
  328. Variable(0x0004, False, False, [0x05], "", 0, "manual_control_switch", "Manual control switch",
  329. lambda x: ["Off", "On"][x]),
  330. Variable(0x0005, False, False, [0x05], "", 0, "restore_system_default_values", "Restore system default values",
  331. lambda x: ["No", "Yes"][x]),
  332. Variable(0x0006, False, False, [0x05], "", 0, "clear_device_statistics", "Clear running days, Power generation or consumption WH and historical minimum/maximum voltage",
  333. lambda x: ["", "Clear"][x]),
  334. Variable(0x0007, False, False, [0x05], "", 0, "clear_counters", "Clear all protection and fully charged times",
  335. lambda x: ["", "Clear"][x]),
  336. Variable(0x0008, False, False, [0x05], "", 0, "Clear_charge_discharge_ah", "Clear charge/discharge AH",
  337. lambda x: ["", "Clear"][x]),
  338. Variable(0x0009, False, False, [0x05], "", 0, "clear_all", "Clear all of the above historical data",
  339. lambda x: ["", "Clear"][x]),
  340. ]
  341. def bytes_to_value(self, variable: Variable, buffer: bytes, offset: int) -> Value:
  342. if variable.is_32_bit and variable.is_signed:
  343. raw_value = struct.unpack_from(">H", buffer, offset)[0] | struct.unpack_from(">h", buffer, offset + 2)[0] << 16
  344. elif variable.is_32_bit:
  345. raw_value = struct.unpack_from(">H", buffer, offset)[0] | struct.unpack_from(">H", buffer, offset + 2)[0] << 16
  346. elif variable.is_signed:
  347. raw_value = struct.unpack_from(">h", buffer, offset)[0]
  348. else:
  349. raw_value = struct.unpack_from(">H", buffer, offset)[0]
  350. if variable.multiplier:
  351. value = raw_value / variable.multiplier
  352. elif variable.func:
  353. try:
  354. value = variable.func(raw_value)
  355. except IndexError as e:
  356. raise Exception(f"unexpected value for {variable.name} ({hex(variable.address)}): '{raw_value}'")
  357. else:
  358. value = raw_value
  359. return value
  360. def value_to_bytes(self, variable: Variable, buffer: bytearray, offset: int, value: Value) -> int:
  361. if variable.multiplier:
  362. raw_value = round(float(value) * variable.multiplier)
  363. elif variable.func:
  364. raw_value = self._find_raw_value_by_brute_force(variable, value)
  365. if raw_value == None:
  366. raise Exception(f"invalid value for {variable.name}: '{value}'")
  367. else:
  368. raw_value = int(value)
  369. if variable.is_32_bit and variable.is_signed:
  370. struct.pack_into(">H", buffer, offset, raw_value & 0xFFFF)
  371. struct.pack_into(">h", buffer, offset + 2, raw_value >> 16)
  372. elif variable.is_32_bit:
  373. struct.pack_into(">H", buffer, offset, raw_value & 0xFFFF)
  374. struct.pack_into(">H", buffer, offset + 2, raw_value >> 16)
  375. elif variable.is_signed:
  376. struct.pack_into(">h", buffer, offset, raw_value)
  377. else:
  378. struct.pack_into(">H", buffer, offset, raw_value)
  379. length = 4 if variable.is_32_bit else 2
  380. return offset + length
  381. def get_read_command(self, device_id: int, start_address: int, count: int) -> bytes:
  382. variables = [v for v in self.variables if v.address >= start_address and v.address < start_address + count]
  383. if not variables:
  384. raise Exception(f"the range {hex(start_address)}-{hex(start_address+count-1)} contains no variables")
  385. function_code = variables[0].function_codes[0]
  386. if not all(function_code in v.function_codes for v in variables):
  387. raise Exception(f"the range {hex(start_address)}-{hex(start_address+count-1)} spans multiple function codes")
  388. result = bytes([
  389. device_id,
  390. function_code,
  391. start_address >> 8,
  392. start_address & 0xFF,
  393. count >> 8,
  394. count & 0xFF
  395. ])
  396. return result + crc16(result)
  397. def get_write_command(self, device_id: int, values: list[(Variable, Any)]) -> bytes:
  398. if not values:
  399. raise Exception(f"values list is empty")
  400. values.sort(key=lambda x: x[0].address)
  401. address = values[0][0].address
  402. for variable, value in values:
  403. if value is None:
  404. raise Exception(f"value of {variable.name} ({hex(variable.address)}) is empty")
  405. if address < variable.address:
  406. raise Exception(f"variables are not continuous at {hex(variable.address)}")
  407. address = variable.address + (2 if variable.is_32_bit else 1)
  408. start_variable = values[0][0]
  409. end_variable = values[-1][0]
  410. start_address = start_variable.address
  411. end_address = end_variable.address + (1 if end_variable.is_32_bit else 0)
  412. count = end_address - start_address + 1
  413. byte_count = count * 2
  414. if byte_count > 255:
  415. raise Exception(f"address range is too large")
  416. if count > 1:
  417. function_code = FunctionCodes.WRITE_MEMORY_RANGE
  418. header = bytes([
  419. device_id,
  420. function_code.value,
  421. start_address >> 8,
  422. start_address & 0xFF,
  423. count >> 8,
  424. count & 0xFF,
  425. byte_count,
  426. ])
  427. else:
  428. if FunctionCodes.WRITE_STATUS_REGISTER.value in values[0][0].function_codes:
  429. function_code = FunctionCodes.WRITE_STATUS_REGISTER
  430. else:
  431. function_code = FunctionCodes.WRITE_MEMORY_SINGLE
  432. header = bytes([
  433. device_id,
  434. function_code.value,
  435. start_address >> 8,
  436. start_address & 0xFF,
  437. ])
  438. if not all(function_code.value in x[0].function_codes for x in values):
  439. raise Exception(f"function code {function_code.name} is not supported for all addresses")
  440. data = bytearray(byte_count)
  441. for variable, value in values:
  442. offset = (variable.address - start_address) * 2
  443. self.value_to_bytes(variable, data, offset, value)
  444. result = header + bytes(data)
  445. return result + crc16(result)
  446. def parse(self, start_address: int, buffer: bytes) -> list[(Variable, Value)]:
  447. device_id = buffer[0]
  448. function_code = FunctionCodes(buffer[1])
  449. if function_code in [FunctionCodes.READ_MEMORY, FunctionCodes.READ_PARAMETER, FunctionCodes.READ_STATUS_REGISTER]:
  450. data_length = buffer[2]
  451. received_crc = buffer[3+data_length:3+data_length+2]
  452. calculated_crc = crc16(buffer[:3+data_length])
  453. if received_crc != calculated_crc:
  454. raise Exception(f"CRC mismatch ({calculated_crc} != {received_crc})")
  455. results = []
  456. address = start_address
  457. cursor = 3
  458. while cursor < data_length + 3:
  459. variables = [v for v in self.variables if address == v.address and function_code.value in v.function_codes]
  460. for variable in variables:
  461. value = self.bytes_to_value(variable, buffer, cursor)
  462. results.append((variable, value))
  463. cursor += 2
  464. address += 1
  465. return results
  466. else:
  467. address = struct.unpack_from('>H', buffer, 2)[0]
  468. if address != start_address:
  469. raise Exception(f"Write result address mismatch ({hex(address)} != {hex(start_address)})")
  470. received_crc = buffer[6:8]
  471. calculated_crc = crc16(buffer[:6])
  472. if received_crc != calculated_crc:
  473. raise Exception(f"CRC mismatch ({calculated_crc} != {received_crc})")
  474. return []
  475. def _find_raw_value_by_brute_force(self, variable: Variable, value):
  476. n_bits = 32 if variable.is_32_bit else 16
  477. if variable.is_signed:
  478. for i in range(0, 2**(n_bits-1) + 1):
  479. try:
  480. if variable.func(i) == value:
  481. return i
  482. except IndexError:
  483. pass
  484. for i in range(0, -2**(n_bits-1) - 2, -1):
  485. try:
  486. if variable.func(i) == value:
  487. return i
  488. except IndexError:
  489. pass
  490. else:
  491. for i in range(0, 2**n_bits + 1):
  492. try:
  493. if variable.func(i) == value:
  494. return i
  495. except IndexError:
  496. pass
  497. return None