goodwe 0.4.8__py3-none-any.whl → 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
goodwe/dt.py CHANGED
@@ -1,12 +1,12 @@
1
+ """Grid-only inverter support - models DT/MS/D-NS/XS or GE's GEP(PSB/PSC)"""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import logging
4
- from typing import Tuple
5
6
 
7
+ from .const import *
6
8
  from .exceptions import InverterError, RequestFailedException, RequestRejectedException
7
- from .inverter import Inverter
8
- from .inverter import OperationMode
9
- from .inverter import SensorKind as Kind
9
+ from .inverter import EMSMode, Inverter, OperationMode, SensorKind as Kind
10
10
  from .modbus import ILLEGAL_DATA_ADDRESS
11
11
  from .model import is_3_mppt, is_single_phase
12
12
  from .protocol import ProtocolCommand
@@ -18,35 +18,51 @@ logger = logging.getLogger(__name__)
18
18
  class DT(Inverter):
19
19
  """Class representing inverter of DT/MS/D-NS/XS or GE's GEP(PSB/PSC) families"""
20
20
 
21
- __all_sensors: Tuple[Sensor, ...] = (
21
+ __all_sensors: tuple[Sensor, ...] = (
22
22
  Timestamp("timestamp", 30100, "Timestamp"),
23
23
  Voltage("vpv1", 30103, "PV1 Voltage", Kind.PV),
24
24
  Current("ipv1", 30104, "PV1 Current", Kind.PV),
25
- Calculated("ppv1",
26
- lambda data: round(read_voltage(data, 30103) * read_current(data, 30104)),
27
- "PV1 Power", "W", Kind.PV),
25
+ Calculated(
26
+ "ppv1",
27
+ lambda data: round(read_voltage(data, 30103) * read_current(data, 30104)),
28
+ "PV1 Power",
29
+ "W",
30
+ Kind.PV,
31
+ ),
28
32
  Voltage("vpv2", 30105, "PV2 Voltage", Kind.PV),
29
33
  Current("ipv2", 30106, "PV2 Current", Kind.PV),
30
- Calculated("ppv2",
31
- lambda data: round(read_voltage(data, 30105) * read_current(data, 30106)),
32
- "PV2 Power", "W", Kind.PV),
34
+ Calculated(
35
+ "ppv2",
36
+ lambda data: round(read_voltage(data, 30105) * read_current(data, 30106)),
37
+ "PV2 Power",
38
+ "W",
39
+ Kind.PV,
40
+ ),
33
41
  Voltage("vpv3", 30107, "PV3 Voltage", Kind.PV),
34
42
  Current("ipv3", 30108, "PV3 Current", Kind.PV),
35
- Calculated("ppv3",
36
- lambda data: round(read_voltage(data, 30107) * read_current(data, 30108)),
37
- "PV3 Power", "W", Kind.PV),
43
+ Calculated(
44
+ "ppv3",
45
+ lambda data: round(read_voltage(data, 30107) * read_current(data, 30108)),
46
+ "PV3 Power",
47
+ "W",
48
+ Kind.PV,
49
+ ),
38
50
  # ppv1 + ppv2 + ppv3
39
- Calculated("ppv",
40
- lambda data: (round(read_voltage(data, 30103) * read_current(data, 30104))) + (round(
41
- read_voltage(data, 30105) * read_current(data, 30106))) + (round(
42
- read_voltage(data, 30107) * read_current(data, 30108))),
43
- "PV Power", "W", Kind.PV),
44
- # Voltage("vpv4", 14, "PV4 Voltage", Kind.PV),
45
- # Current("ipv4", 16, "PV4 Current", Kind.PV),
46
- # Voltage("vpv5", 14, "PV5 Voltage", Kind.PV),
47
- # Current("ipv5", 16, "PV5 Current", Kind.PV),
48
- # Voltage("vpv6", 14, "PV6 Voltage", Kind.PV),
49
- # Current("ipv6", 16, "PV7 Current", Kind.PV),
51
+ Calculated(
52
+ "ppv",
53
+ lambda data: (round(read_voltage(data, 30103) * read_current(data, 30104)))
54
+ + (round(read_voltage(data, 30105) * read_current(data, 30106)))
55
+ + (round(read_voltage(data, 30107) * read_current(data, 30108))),
56
+ "PV Power",
57
+ "W",
58
+ Kind.PV,
59
+ ),
60
+ # Voltage("vpv4", 30109, "PV4 Voltage", Kind.PV),
61
+ # Current("ipv4", 30110, "PV4 Current", Kind.PV),
62
+ # Voltage("vpv5", 30111, "PV5 Voltage", Kind.PV),
63
+ # Current("ipv5", 30112, "PV5 Current", Kind.PV),
64
+ # Voltage("vpv6", 30113, "PV6 Voltage", Kind.PV),
65
+ # Current("ipv6", 30114, "PV7 Current", Kind.PV),
50
66
  Voltage("vline1", 30115, "On-grid L1-L2 Voltage", Kind.AC),
51
67
  Voltage("vline2", 30116, "On-grid L2-L3 Voltage", Kind.AC),
52
68
  Voltage("vline3", 30117, "On-grid L3-L1 Voltage", Kind.AC),
@@ -59,116 +75,164 @@ class DT(Inverter):
59
75
  Frequency("fgrid1", 30124, "On-grid L1 Frequency", Kind.AC),
60
76
  Frequency("fgrid2", 30125, "On-grid L2 Frequency", Kind.AC),
61
77
  Frequency("fgrid3", 30126, "On-grid L3 Frequency", Kind.AC),
62
- Calculated("pgrid1",
63
- lambda data: round(read_voltage(data, 30118) * read_current(data, 30121)),
64
- "On-grid L1 Power", "W", Kind.AC),
65
- Calculated("pgrid2",
66
- lambda data: round(read_voltage(data, 30119) * read_current(data, 30122)),
67
- "On-grid L2 Power", "W", Kind.AC),
68
- Calculated("pgrid3",
69
- lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
70
- "On-grid L3 Power", "W", Kind.AC),
71
- # 30127 reserved
72
- PowerS("total_inverter_power", 30128, "Total Power", Kind.AC),
78
+ Calculated(
79
+ "pgrid1",
80
+ lambda data: round(read_voltage(data, 30118) * read_current(data, 30121)),
81
+ "On-grid L1 Power",
82
+ "W",
83
+ Kind.AC,
84
+ ),
85
+ Calculated(
86
+ "pgrid2",
87
+ lambda data: round(read_voltage(data, 30119) * read_current(data, 30122)),
88
+ "On-grid L2 Power",
89
+ "W",
90
+ Kind.AC,
91
+ ),
92
+ Calculated(
93
+ "pgrid3",
94
+ lambda data: round(read_voltage(data, 30120) * read_current(data, 30123)),
95
+ "On-grid L3 Power",
96
+ "W",
97
+ Kind.AC,
98
+ ),
99
+ Power4("total_inverter_power", 30127, "Total Power", Kind.AC),
73
100
  Integer("work_mode", 30129, "Work Mode code"),
74
101
  Enum2("work_mode_label", 30129, WORK_MODES, "Work Mode"),
75
102
  Long("error_codes", 30130, "Error Codes"),
76
103
  Integer("warning_code", 30132, "Warning code"),
77
104
  Apparent4("apparent_power", 30133, "Apparent Power", Kind.AC),
78
105
  Reactive4("reactive_power", 30135, "Reactive Power", Kind.AC),
79
- # 30137 reserved
80
- # 30138 reserved
106
+ PowerS("total_input_power", 30137, "Total Input Power", Kind.PV),
81
107
  Decimal("power_factor", 30139, 1000, "Power Factor", "", Kind.GRID),
82
- # 30140 reserved
108
+ # 30140 inverter efficiency
83
109
  Temp("temperature", 30141, "Inverter Temperature", Kind.AC),
84
- # 30142 reserved
85
- # 30143 reserved
110
+ Temp("temperature_heatsink", 30142, "Heatsink Temperature", Kind.AC),
111
+ # Temp("temperature_module", 30143, "Module Temperature", Kind.AC),
86
112
  Energy("e_day", 30144, "Today's PV Generation", Kind.PV),
87
113
  Energy4("e_total", 30145, "Total PV Generation", Kind.PV),
88
114
  Long("h_total", 30147, "Hours Total", "h", Kind.PV),
89
115
  Integer("safety_country", 30149, "Safety Country code", "", Kind.AC),
90
- Enum2("safety_country_label", 30149, SAFETY_COUNTRIES, "Safety Country", Kind.AC),
91
- # 30150 reserved
92
- # 30151 reserved
93
- # 30152 reserved
94
- # 30153 reserved
95
- # 30154 reserved
96
- # 30155 reserved
97
- # 30156 reserved
98
- # 30157 reserved
99
- # 30158 reserved
100
- # 30159 reserved
101
- # 30160 reserved
102
- # 30161 reserved
103
- Integer("funbit", 30162, "FunBit", "", Kind.PV),
116
+ Enum2(
117
+ "safety_country_label", 30149, SAFETY_COUNTRIES, "Safety Country", Kind.AC
118
+ ),
119
+ # 30150 PV1 input power kW
120
+ # 30152 PV2 input power kW
121
+ # 30154 PV3 input power kW
122
+ # 30156 PV4 input power kW
123
+ # 30158 PV5 input power kW
124
+ # 30160 PV6 input power kW
125
+ Integer("funbit", 30162, "FunctionBit", "", Kind.PV),
104
126
  Voltage("vbus", 30163, "Bus Voltage", Kind.PV),
105
127
  Voltage("vnbus", 30164, "NBus Voltage", Kind.PV),
106
128
  Long("derating_mode", 30165, "Derating Mode code"),
107
129
  EnumBitmap4("derating_mode_label", 30165, DERATING_MODE_CODES, "Derating Mode"),
108
- # 30167 reserved
109
- # 30168 reserved
110
- # 30169 reserved
111
- # 30170 reserved
112
- # 30171 reserved
113
- # 30172 reserved
130
+ # 30167 PV2 fault value
131
+ # 30168 Line2 fault value
132
+ # 30169 Line3 fault value
133
+ # 30170 Line3 fault value (duplicate commentary in modbus protocol document)
134
+ # 30171 Manufacture ID
135
+ Integer("rssi", 30172, "RSSI"),
136
+ # 30173 ISO test value
137
+ # 30174 PID and Wietap status
138
+ # 30175 String 1 Current
139
+ # 30176 String 2 Current
140
+ # 30177 String 3 Current
141
+ # 30178 - 30194 listed String 4 Current through to String 20 Current
114
142
  )
115
143
 
116
144
  # Inverter's meter data
117
- # Modbus registers from offset 0x75f4 (30196)
118
- __all_sensors_meter: Tuple[Sensor, ...] = (
119
- PowerS("active_power", 30196, "Active Power", Kind.GRID),
145
+ # Modbus registers from offset 0x75f3 (30195)
146
+ __all_sensors_meter: tuple[Sensor, ...] = (
147
+ Power4S("meter_active_power", 30195, "Meter Active Power", Kind.GRID),
148
+ Energy4W("meter_e_total_exp", 30197, "Meter Total Energy (export)", Kind.GRID),
149
+ Energy4W("meter_e_total_imp", 30199, "Meter Total Energy (import)", Kind.GRID),
150
+ # 30201 GPRS Burn Mode
151
+ # 30202 Cabinet Humidity %
152
+ # 30203 ARM Error Message
153
+ # 30205 Warning Code 2
154
+ # 30207 AFCI Status
155
+ # 30208 Output control status - Japanese models only
156
+ Integer("meter_comm_status", 30209, "Meter Communication Status code"), # 1 Normal, 2 Disconnected
157
+ Enum2("meter_comm_label", 30209, METER_COMMUNICATION_STATUS, "Meter Communication Status"),
158
+ Calculated("house_consumption", lambda data: None, "House Consumption", "W", Kind.AC), # calculated and patched in read_runtime_data as we are unable to calculate it from seperate modbus offsets in all_sensors and all_sensors_meter
159
+ CurrentSmA("leakage_current", 30210, "Leakage Current", Kind.PV),
160
+ # 30211 repeat of 30197 in U64 instead of U32
161
+ # 30215 repeat of 30199 in U64 instead of U32
162
+ # 30219 Wireless Module AT Instruction Status Log
163
+ # 30220 Disable Inverter Flag - 0 for normal operation, 1 for inverter disabled until safety regulations changed
164
+ # 30221 ARM Internal Firmware Version
165
+ # 30227 G100 CLS State (UK anti-reflux status)
166
+ # 30228 Grid power monitored by DSPs CT (S32)
167
+ # 30230 String Current Detection Flag - 0 No string current detected, 1 String current detected
120
168
  )
121
169
 
122
170
  # Modbus registers of inverter settings, offsets are modbus register addresses
123
- __all_settings: Tuple[Sensor, ...] = (
171
+ __all_settings: tuple[Sensor, ...] = (
124
172
  Timestamp("time", 40313, "Inverter time"),
125
-
126
- Integer("shadow_scan", 40326, "Shadow Scan", "", Kind.PV),
127
- Integer("grid_export", 40327, "Grid Export Enabled", "", Kind.GRID),
173
+ Integer("shadow_scan_pv1", 40326, "Shadow Scan Status PV1", "", Kind.PV),
174
+ Integer("shadow_scan_pv2", 40352, "Shadow Scan Status PV2", "", Kind.PV),
175
+ Integer("shadow_scan_pv3", 40362, "Shadow Scan Status PV3", "", Kind.PV),
176
+ Integer("shadow_scan_time", 40347, "Shadow Scan Time", "", Kind.PV),
177
+ # Integer("shadow_scan_pv2_time", 40353, "Shadow Scan PV2 Time", "", Kind.PV), - documentation suggests a duplicate of 40347 as the value is global for all 3 strings
178
+ Integer("grid_export", 40327, "Grid Export Limit Enabled", "", Kind.GRID),
128
179
  Integer("grid_export_limit", 40328, "Grid Export Limit", "%", Kind.GRID),
129
180
  Integer("start", 40330, "Start / Power On", "", Kind.GRID),
130
181
  Integer("stop", 40331, "Stop / Power Off", "", Kind.GRID),
131
182
  Integer("restart", 40332, "Restart", "", Kind.GRID),
132
- Integer("grid_export_hw", 40345, "Grid Export Enabled (HW)", "", Kind.GRID),
183
+ Integer(
184
+ "grid_export_hw", 40345, "Grid Export Limit Enabled (HW)", "", Kind.GRID
185
+ ),
133
186
  )
134
187
 
135
188
  # Settings for single phase inverters
136
- __settings_single_phase: Tuple[Sensor, ...] = (
189
+ __settings_single_phase: tuple[Sensor, ...] = (
137
190
  Long("grid_export_limit", 40328, "Grid Export Limit", "W", Kind.GRID),
138
191
  )
139
192
 
140
193
  # Settings for three phase inverters
141
- __settings_three_phase: Tuple[Sensor, ...] = (
194
+ __settings_three_phase: tuple[Sensor, ...] = (
142
195
  Integer("grid_export_limit", 40336, "Grid Export Limit", "%", Kind.GRID),
143
196
  )
144
197
 
145
- def __init__(self, host: str, port: int, comm_addr: int = 0, timeout: int = 1, retries: int = 3):
146
- super().__init__(host, port, comm_addr if comm_addr else 0x7f, timeout, retries)
147
- self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(0x7531, 0x0028)
198
+ def __init__(
199
+ self,
200
+ host: str,
201
+ port: int,
202
+ comm_addr: int = 0,
203
+ timeout: int = 1,
204
+ retries: int = 3,
205
+ ):
206
+ super().__init__(host, port, comm_addr if comm_addr else 0x7F, timeout, retries)
207
+ self._READ_DEVICE_VERSION_INFO: ProtocolCommand = self._read_command(
208
+ 0x7531, 0x0028
209
+ )
210
+ self._READ_METER_VERSION_INFO: ProtocolCommand = self._read_command(
211
+ 0x756F, 0x0014
212
+ )
213
+ self._READ_DEVICE_MODEL: ProtocolCommand = self._read_command(0x9CED, 0x0008)
148
214
  self._READ_RUNNING_DATA: ProtocolCommand = self._read_command(0x7594, 0x0049)
149
- self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75f4, 0x01)
215
+ self._READ_METER_DATA: ProtocolCommand = self._read_command(0x75F3, 0xF)
150
216
  self._sensors = self.__all_sensors
151
217
  self._sensors_meter = self.__all_sensors_meter
152
218
  self._settings: dict[str, Sensor] = {s.id_: s for s in self.__all_settings}
219
+ self._sensors_map: dict[str, Sensor] | None = None
153
220
  self._has_meter: bool = True
154
221
 
155
222
  @staticmethod
156
223
  def _single_phase_only(s: Sensor) -> bool:
157
224
  """Filter to exclude phase2/3 sensors on single phase inverters"""
158
- return not ((s.id_.endswith('2') or s.id_.endswith('3')) and 'pv' not in s.id_)
225
+ return not ((s.id_.endswith("2") or s.id_.endswith("3")) and "pv" not in s.id_)
159
226
 
160
227
  @staticmethod
161
228
  def _pv1_pv2_only(s: Sensor) -> bool:
162
229
  """Filter to exclude sensors on < 3 PV inverters"""
163
- return not s.id_.endswith('pv3')
230
+ return not s.id_.endswith("pv3")
164
231
 
165
232
  async def read_device_info(self):
166
233
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
167
234
  response = response.response_data()
168
- try:
169
- self.model_name = response[22:32].decode("ascii").rstrip()
170
- except:
171
- print("No model name sent from the inverter.")
235
+
172
236
  # Modbus registers from 30001 - 30040
173
237
  self.serial_number = self._decode(response[6:22]) # 30004 - 30012
174
238
  self.dsp1_version = read_unsigned_int(response, 66) # 30034
@@ -176,24 +240,40 @@ class DT(Inverter):
176
240
  self.arm_version = read_unsigned_int(response, 70) # 30036
177
241
  self.dsp_svn_version = read_unsigned_int(response, 72) # 35037
178
242
  self.arm_svn_version = read_unsigned_int(response, 74) # 35038
179
- self.firmware = "{}.{}.{:02x}".format(self.dsp1_version, self.dsp2_version, self.arm_version)
243
+ self.firmware = (
244
+ f"{self.dsp1_version}.{self.dsp2_version}.{self.arm_version:02x}"
245
+ )
246
+
247
+ try:
248
+ self.model_name = response[22:32].decode("ascii").rstrip()
249
+ except:
250
+ try:
251
+ response = await self._read_from_socket(self._READ_DEVICE_MODEL)
252
+ response = response.response_data()
253
+ self.model_name = response[0:16].decode("ascii").rstrip("\x00").strip()
254
+ except InverterError as e:
255
+ logger.debug("No model name sent from the inverter.")
180
256
 
181
257
  if is_single_phase(self):
182
- # this is single phase inverter, filter out all L2 and L3 sensors
183
258
  self._sensors = tuple(filter(self._single_phase_only, self.__all_sensors))
184
259
  self._settings.update({s.id_: s for s in self.__settings_single_phase})
185
260
  else:
186
261
  self._settings.update({s.id_: s for s in self.__settings_three_phase})
187
262
 
188
263
  if is_3_mppt(self):
189
- # this is 3 PV strings inverter, keep all sensors
190
264
  pass
191
265
  else:
192
- # this is only 2 PV strings inverter
193
266
  self._sensors = tuple(filter(self._pv1_pv2_only, self._sensors))
194
- pass
195
267
 
196
- async def read_runtime_data(self) -> Dict[str, Any]:
268
+ try:
269
+ response = await self._read_from_socket(self._READ_METER_VERSION_INFO)
270
+ response = response.response_data()
271
+ self.meter_software_version = read_unsigned_int(response, 0) # 30063
272
+ self.meter_serial_number = self._decode(response[24:38]) # 30075 - 30082
273
+ except InverterError as e:
274
+ logger.debug("Could not read meter version info.")
275
+
276
+ async def read_runtime_data(self) -> dict[str, Any]:
197
277
  response = await self._read_from_socket(self._READ_RUNNING_DATA)
198
278
  data = self._map_response(response, self._sensors)
199
279
 
@@ -201,33 +281,48 @@ class DT(Inverter):
201
281
  try:
202
282
  response = await self._read_from_socket(self._READ_METER_DATA)
203
283
  data.update(self._map_response(response, self._sensors_meter))
284
+ #patch house_consumption int from all_sensors_meter now that we have values available
285
+ data["house_consumption"] = abs(data.get("ppv", 0) - data.get("meter_active_power", 0))
204
286
  except (RequestRejectedException, RequestFailedException):
205
287
  logger.info("Meter values not supported, disabling further attempts.")
206
288
  self._has_meter = False
207
289
 
208
290
  return data
209
291
 
292
+ async def read_sensor(self, sensor_id: str) -> Any:
293
+ sensor: Sensor = self._get_sensor(sensor_id)
294
+ if sensor:
295
+ return await self._read_sensor(sensor)
296
+ if sensor_id.startswith("modbus"):
297
+ response = await self._read_from_socket(
298
+ self._read_command(int(sensor_id[7:]), 1)
299
+ )
300
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
301
+ raise ValueError(f'Unknown sensor "{sensor_id}"')
302
+
210
303
  async def read_setting(self, setting_id: str) -> Any:
211
304
  setting = self._settings.get(setting_id)
212
305
  if setting:
213
- return await self._read_setting(setting)
214
- else:
215
- if setting_id.startswith("modbus"):
216
- response = await self._read_from_socket(self._read_command(int(setting_id[7:]), 1))
217
- return int.from_bytes(response.read(2), byteorder="big", signed=True)
218
- else:
219
- raise ValueError(f'Unknown setting "{setting_id}"')
220
-
221
- async def _read_setting(self, setting: Sensor) -> Any:
306
+ return await self._read_sensor(setting)
307
+ if setting_id.startswith("modbus"):
308
+ response = await self._read_from_socket(
309
+ self._read_command(int(setting_id[7:]), 1)
310
+ )
311
+ return int.from_bytes(response.read(2), byteorder="big", signed=True)
312
+ raise ValueError(f'Unknown setting "{setting_id}"')
313
+
314
+ async def _read_sensor(self, setting: Sensor) -> Any:
222
315
  try:
223
316
  count = (setting.size_ + (setting.size_ % 2)) // 2
224
- response = await self._read_from_socket(self._read_command(setting.offset, count))
317
+ response = await self._read_from_socket(
318
+ self._read_command(setting.offset, count)
319
+ )
225
320
  return setting.read_value(response)
226
321
  except RequestRejectedException as ex:
227
322
  if ex.message == ILLEGAL_DATA_ADDRESS:
228
- logger.debug("Unsupported setting %s", setting.id_)
323
+ logger.debug("Unsupported sensor/setting %s", setting.id_)
229
324
  self._settings.pop(setting.id_, None)
230
- raise ValueError(f'Unknown setting "{setting.id_}"')
325
+ raise ValueError(f'Unknown sensor/setting "{setting.id_}"')
231
326
  return None
232
327
 
233
328
  async def write_setting(self, setting_id: str, value: Any):
@@ -236,14 +331,18 @@ class DT(Inverter):
236
331
  await self._write_setting(setting, value)
237
332
  else:
238
333
  if setting_id.startswith("modbus"):
239
- await self._read_from_socket(self._write_command(int(setting_id[7:]), int(value)))
334
+ await self._read_from_socket(
335
+ self._write_command(int(setting_id[7:]), int(value))
336
+ )
240
337
  else:
241
338
  raise ValueError(f'Unknown setting "{setting_id}"')
242
339
 
243
340
  async def _write_setting(self, setting: Sensor, value: Any):
244
341
  if setting.size_ == 1:
245
342
  # modbus can address/store only 16 bit values, read the other 8 bytes
246
- response = await self._read_from_socket(self._read_command(setting.offset, 1))
343
+ response = await self._read_from_socket(
344
+ self._read_command(setting.offset, 1)
345
+ )
247
346
  raw_value = setting.encode_value(value, response.response_data()[0:2])
248
347
  else:
249
348
  raw_value = setting.encode_value(value)
@@ -251,9 +350,11 @@ class DT(Inverter):
251
350
  value = int.from_bytes(raw_value, byteorder="big", signed=True)
252
351
  await self._read_from_socket(self._write_command(setting.offset, value))
253
352
  else:
254
- await self._read_from_socket(self._write_multi_command(setting.offset, raw_value))
353
+ await self._read_from_socket(
354
+ self._write_multi_command(setting.offset, raw_value)
355
+ )
255
356
 
256
- async def read_settings_data(self) -> Dict[str, Any]:
357
+ async def read_settings_data(self) -> dict[str, Any]:
257
358
  data = {}
258
359
  for setting in self.settings():
259
360
  value = await self.read_setting(setting.id_)
@@ -261,20 +362,34 @@ class DT(Inverter):
261
362
  return data
262
363
 
263
364
  async def get_grid_export_limit(self) -> int:
264
- return await self.read_setting('grid_export_limit')
365
+ return await self.read_setting("grid_export_limit")
265
366
 
266
367
  async def set_grid_export_limit(self, export_limit: int) -> None:
267
368
  if export_limit >= 0:
268
- return await self.write_setting('grid_export_limit', export_limit)
369
+ return await self.write_setting("grid_export_limit", export_limit)
269
370
 
270
- async def get_operation_modes(self, include_emulated: bool) -> Tuple[OperationMode, ...]:
371
+ async def get_operation_modes(
372
+ self, include_emulated: bool
373
+ ) -> tuple[OperationMode, ...]:
271
374
  return ()
272
375
 
273
376
  async def get_operation_mode(self) -> OperationMode:
274
377
  raise InverterError("Operation not supported.")
275
378
 
276
- async def set_operation_mode(self, operation_mode: OperationMode, eco_mode_power: int = 100,
277
- eco_mode_soc: int = 100) -> None:
379
+ async def set_operation_mode(
380
+ self,
381
+ operation_mode: OperationMode,
382
+ eco_mode_power: int = 100,
383
+ eco_mode_soc: int = 100,
384
+ ) -> None:
385
+ raise InverterError("Operation not supported.")
386
+
387
+ async def get_ems_mode(self) -> EMSMode:
388
+ raise InverterError("Operation not supported.")
389
+
390
+ async def set_ems_mode(
391
+ self, ems_mode: EMSMode, ems_power_limit: int | None = None
392
+ ) -> None:
278
393
  raise InverterError("Operation not supported.")
279
394
 
280
395
  async def get_ongrid_battery_dod(self) -> int:
@@ -283,11 +398,16 @@ class DT(Inverter):
283
398
  async def set_ongrid_battery_dod(self, dod: int) -> None:
284
399
  raise InverterError("Operation not supported, inverter has no batteries.")
285
400
 
286
- def sensors(self) -> Tuple[Sensor, ...]:
401
+ def _get_sensor(self, sensor_id: str) -> Sensor | None:
402
+ if self._sensors_map is None:
403
+ self._sensors_map = {s.id_: s for s in self.sensors()}
404
+ return self._sensors_map.get(sensor_id)
405
+
406
+ def sensors(self) -> tuple[Sensor, ...]:
287
407
  result = self._sensors
288
408
  if self._has_meter:
289
409
  result = result + self._sensors_meter
290
410
  return result
291
411
 
292
- def settings(self) -> Tuple[Sensor, ...]:
412
+ def settings(self) -> tuple[Sensor, ...]:
293
413
  return tuple(self._settings.values())