goodwe 0.4.0__py3-none-any.whl → 0.4.1__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/et.py CHANGED
@@ -3,10 +3,11 @@ from __future__ import annotations
3
3
  import logging
4
4
  from typing import Tuple
5
5
 
6
- from .exceptions import RequestRejectedException
6
+ from .exceptions import RequestFailedException, RequestRejectedException
7
7
  from .inverter import Inverter
8
8
  from .inverter import OperationMode
9
9
  from .inverter import SensorKind as Kind
10
+ from .modbus import ILLEGAL_DATA_ADDRESS
10
11
  from .model import is_2_battery, is_4_mppt, is_745_platform, is_single_phase
11
12
  from .protocol import ProtocolCommand
12
13
  from .sensor import *
@@ -331,7 +332,7 @@ class ET(Inverter):
331
332
  # Modbus registers of inverter settings, offsets are modbus register addresses
332
333
  __all_settings: Tuple[Sensor, ...] = (
333
334
  Integer("comm_address", 45127, "Communication Address", ""),
334
-
335
+ Integer("modbus_baud_rate", 45132, "Modbus Baud rate", ""),
335
336
  Timestamp("time", 45200, "Inverter time"),
336
337
 
337
338
  Integer("sensitivity_check", 45246, "Sensitivity Check Mode", "", Kind.AC),
@@ -371,6 +372,51 @@ class ET(Inverter):
371
372
  ByteH("eco_mode_3_switch", 47526, "Eco Mode Group 3 Switch"),
372
373
  EcoModeV1("eco_mode_4", 47527, "Eco Mode Group 4"),
373
374
  ByteH("eco_mode_4_switch", 47530, "Eco Mode Group 4 Switch"),
375
+
376
+ # Direct BMS communication for EMS Control
377
+ Integer("bms_version", 47900, "BMS Version"),
378
+ Integer("bms_bat_modules", 47901, "BMS Battery Modules"),
379
+ # Real time read from BMS
380
+ Voltage("bms_bat_charge_v_max", 47902, "BMS Battery Charge Voltage (max)", Kind.BMS),
381
+ Current("bms_bat_charge_i_max", 47903, "BMS Battery Charge Current (max)", Kind.BMS),
382
+ Voltage("bms_bat_discharge_v_min", 47904, "BMS min. Battery Discharge Voltage (min)", Kind.BMS),
383
+ Current("bms_bat_discharge_i_max", 47905, "BMS max. Battery Discharge Current (max)", Kind.BMS),
384
+ Voltage("bms_bat_voltage", 47906, "BMS Battery Voltage", Kind.BMS),
385
+ Current("bms_bat_current", 47907, "BMS Battery Current", Kind.BMS),
386
+ #
387
+ Integer("bms_bat_soc", 47908, "BMS Battery State of Charge", "%", Kind.BMS),
388
+ Integer("bms_bat_soh", 47909, "BMS Battery State of Health", "%", Kind.BMS),
389
+ Temp("bms_bat_temperature", 47910, "BMS Battery Temperature", Kind.BMS),
390
+ Long("bms_bat_warning-code", 47911, "BMS Battery Warning Code"),
391
+ # Reserved
392
+ Long("bms_bat_alarm-code", 47913, "BMS Battery Alarm Code"),
393
+ Integer("bms_status", 47915, "BMS Status"),
394
+ Integer("bms_comm_loss_disable", 47916, "BMS Communication Loss Disable"),
395
+ # RW settings of BMS voltage rate
396
+ Integer("bms_battery_string_rate_v", 47917, "BMS Battery String Rate Voltage"),
397
+
398
+ # Direct BMS communication for EMS Control
399
+ Integer("bms2_version", 47918, "BMS2 Version"),
400
+ Integer("bms2_bat_modules", 47919, "BMS2 Battery Modules"),
401
+ # Real time read from BMS
402
+ Voltage("bms2_bat_charge_v_max", 47920, "BMS2 Battery Charge Voltage (max)", Kind.BMS),
403
+ Current("bms2_bat_charge_i_max", 47921, "BMS2 Battery Charge Current (max)", Kind.BMS),
404
+ Voltage("bms2_bat_discharge_v_min", 47922, "BMS2 min. Battery Discharge Voltage (min)", Kind.BMS),
405
+ Current("bms2_bat_discharge_i_max", 47923, "BMS2 max. Battery Discharge Current (max)", Kind.BMS),
406
+ Voltage("bms2_bat_voltage", 47924, "BMS2 Battery Voltage", Kind.BMS),
407
+ Current("bms2_bat_current", 47925, "BMS2 Battery Current", Kind.BMS),
408
+ #
409
+ Integer("bms2_bat_soc", 47926, "BMS2 Battery State of Charge", "%", Kind.BMS),
410
+ Integer("bms2_bat_soh", 47927, "BMS2 Battery State of Health", "%", Kind.BMS),
411
+ Temp("bms2_bat_temperature", 47928, "BMS2 Battery Temperature", Kind.BMS),
412
+ Long("bms2_bat_warning-code", 47929, "BMS2 Battery Warning Code"),
413
+ # Reserved
414
+ Long("bms2_bat_alarm-code", 47931, "BMS2 Battery Alarm Code"),
415
+ Integer("bms2_status", 47933, "BMS2 Status"),
416
+ Integer("bms2_comm_loss_disable", 47934, "BMS2 Communication Loss Disable"),
417
+ # RW settings of BMS voltage rate
418
+ Integer("bms2_battery_string_rate_v", 47935, "BMS2 Battery String Rate Voltage"),
419
+
374
420
  )
375
421
 
376
422
  # Settings added in ARM firmware 19
@@ -389,6 +435,7 @@ class ET(Inverter):
389
435
  Integer("load_control_mode", 47595, "Load Control Mode", "", Kind.AC),
390
436
  Integer("load_control_switch", 47596, "Load Control Switch", "", Kind.AC),
391
437
  Integer("load_control_soc", 47597, "Load Control SoC", "", Kind.AC),
438
+ Integer("hardware_feed_power", 47599, "Hardware Feed Power"),
392
439
 
393
440
  Integer("fast_charging_power", 47603, "Fast Charging Power", "%", Kind.BAT),
394
441
  )
@@ -447,19 +494,19 @@ class ET(Inverter):
447
494
  async def read_device_info(self):
448
495
  response = await self._read_from_socket(self._READ_DEVICE_VERSION_INFO)
449
496
  response = response.response_data()
450
- # Modbus registers from offset (35000)
497
+ # Modbus registers from 35000 - 35032
451
498
  self.modbus_version = read_unsigned_int(response, 0)
452
499
  self.rated_power = read_unsigned_int(response, 2)
453
500
  self.ac_output_type = read_unsigned_int(response, 4) # 0: 1-phase, 1: 3-phase (4 wire), 2: 3-phase (3 wire)
454
- self.serial_number = self._decode(response[6:22])
455
- self.model_name = self._decode(response[22:32])
456
- self.dsp1_version = read_unsigned_int(response, 32)
457
- self.dsp2_version = read_unsigned_int(response, 34)
458
- self.dsp_svn_version = read_unsigned_int(response, 36)
459
- self.arm_version = read_unsigned_int(response, 38)
460
- self.arm_svn_version = read_unsigned_int(response, 40)
461
- self.firmware = self._decode(response[42:54])
462
- self.arm_firmware = self._decode(response[54:66])
501
+ self.serial_number = self._decode(response[6:22]) # 35003 - 350010
502
+ self.model_name = self._decode(response[22:32]) # 35011 - 35015
503
+ self.dsp1_version = read_unsigned_int(response, 32) # 35016
504
+ self.dsp2_version = read_unsigned_int(response, 34) # 35017
505
+ self.dsp_svn_version = read_unsigned_int(response, 36) # 35018
506
+ self.arm_version = read_unsigned_int(response, 38) # 35019
507
+ self.arm_svn_version = read_unsigned_int(response, 40) # 35020
508
+ self.firmware = self._decode(response[42:54]) # 35021 - 35027
509
+ self.arm_firmware = self._decode(response[54:66]) # 35027 - 35032
463
510
 
464
511
  if not is_4_mppt(self) and self.rated_power < 15000:
465
512
  # This inverter does not have 4 MPPTs or PV strings
@@ -485,18 +532,24 @@ class ET(Inverter):
485
532
  await self._read_from_socket(self._read_command(47547, 6))
486
533
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_19})
487
534
  except RequestRejectedException as ex:
488
- if ex.message == 'ILLEGAL DATA ADDRESS':
489
- logger.debug("Cannot read EcoModeV2 settings, using to EcoModeV1.")
535
+ if ex.message == ILLEGAL_DATA_ADDRESS:
536
+ logger.debug("EcoModeV2 settings not supported, switching to EcoModeV1.")
490
537
  self._has_eco_mode_v2 = False
538
+ except RequestFailedException:
539
+ logger.debug("Cannot read EcoModeV2 settings, switching to EcoModeV1.")
540
+ self._has_eco_mode_v2 = False
491
541
 
492
542
  # Check and add Peak Shaving settings added in (ETU fw 22)
493
543
  try:
494
544
  await self._read_from_socket(self._read_command(47589, 6))
495
545
  self._settings.update({s.id_: s for s in self.__settings_arm_fw_22})
496
546
  except RequestRejectedException as ex:
497
- if ex.message == 'ILLEGAL DATA ADDRESS':
498
- logger.debug("Cannot read PeakShaving setting, disabling it.")
547
+ if ex.message == ILLEGAL_DATA_ADDRESS:
548
+ logger.debug("PeakShaving setting not supported, disabling it.")
499
549
  self._has_peak_shaving = False
550
+ except RequestFailedException:
551
+ logger.debug("Cannot read _has_peak_shaving settings, disabling it.")
552
+ self._has_peak_shaving = False
500
553
 
501
554
  async def read_runtime_data(self) -> Dict[str, Any]:
502
555
  response = await self._read_from_socket(self._READ_RUNNING_DATA)
@@ -508,8 +561,8 @@ class ET(Inverter):
508
561
  response = await self._read_from_socket(self._READ_BATTERY_INFO)
509
562
  data.update(self._map_response(response, self._sensors_battery))
510
563
  except RequestRejectedException as ex:
511
- if ex.message == 'ILLEGAL DATA ADDRESS':
512
- logger.warning("Cannot read battery values, disabling further attempts.")
564
+ if ex.message == ILLEGAL_DATA_ADDRESS:
565
+ logger.info("Battery values not supported, disabling further attempts.")
513
566
  self._has_battery = False
514
567
  else:
515
568
  raise ex
@@ -519,8 +572,8 @@ class ET(Inverter):
519
572
  data.update(
520
573
  self._map_response(response, self._sensors_battery2))
521
574
  except RequestRejectedException as ex:
522
- if ex.message == 'ILLEGAL DATA ADDRESS':
523
- logger.warning("Cannot read battery 2 values, disabling further attempts.")
575
+ if ex.message == ILLEGAL_DATA_ADDRESS:
576
+ logger.info("Battery 2 values not supported, disabling further attempts.")
524
577
  self._has_battery2 = False
525
578
  else:
526
579
  raise ex
@@ -530,8 +583,8 @@ class ET(Inverter):
530
583
  response = await self._read_from_socket(self._READ_METER_DATA_EXTENDED)
531
584
  data.update(self._map_response(response, self._sensors_meter))
532
585
  except RequestRejectedException as ex:
533
- if ex.message == 'ILLEGAL DATA ADDRESS':
534
- logger.warning("Cannot read extended meter values, disabling further attempts.")
586
+ if ex.message == ILLEGAL_DATA_ADDRESS:
587
+ logger.info("Extended meter values not supported, disabling further attempts.")
535
588
  self._has_meter_extended = False
536
589
  self._sensors_meter = tuple(filter(self._not_extended_meter, self._sensors_meter))
537
590
  response = await self._read_from_socket(self._READ_METER_DATA)
@@ -548,8 +601,8 @@ class ET(Inverter):
548
601
  response = await self._read_from_socket(self._READ_MPPT_DATA)
549
602
  data.update(self._map_response(response, self._sensors_mppt))
550
603
  except RequestRejectedException as ex:
551
- if ex.message == 'ILLEGAL DATA ADDRESS':
552
- logger.warning("Cannot read MPPT values, disabling further attempts.")
604
+ if ex.message == ILLEGAL_DATA_ADDRESS:
605
+ logger.info("MPPT values not supported, disabling further attempts.")
553
606
  self._has_mppt = False
554
607
  else:
555
608
  raise ex
@@ -560,7 +613,13 @@ class ET(Inverter):
560
613
  setting = self._settings.get(setting_id)
561
614
  if not setting:
562
615
  raise ValueError(f'Unknown setting "{setting_id}"')
563
- return await self._read_setting(setting)
616
+ try:
617
+ return await self._read_setting(setting)
618
+ except RequestRejectedException as ex:
619
+ if ex.message == ILLEGAL_DATA_ADDRESS:
620
+ logger.debug("Unsupported setting %s", setting.id_)
621
+ self._settings.pop(setting_id, None)
622
+ return None
564
623
 
565
624
  async def _read_setting(self, setting: Sensor) -> Any:
566
625
  count = (setting.size_ + (setting.size_ % 2)) // 2
@@ -592,7 +651,7 @@ class ET(Inverter):
592
651
  try:
593
652
  value = await self.read_setting(setting.id_)
594
653
  data[setting.id_] = value
595
- except ValueError:
654
+ except (ValueError, RequestFailedException):
596
655
  logger.exception("Error reading setting %s.", setting.id_)
597
656
  data[setting.id_] = None
598
657
  return data
goodwe/inverter.py CHANGED
@@ -22,6 +22,7 @@ class SensorKind(Enum):
22
22
  UPS - inverter ups/eps/backup output (e.g. ac voltage of backup/off-grid connected output)
23
23
  BAT - battery (e.g. dc voltage of connected battery pack)
24
24
  GRID - power grid/smart meter (e.g. active power exported to grid)
25
+ BMS - BMS direct data (e.g. dc voltage of)
25
26
  """
26
27
 
27
28
  PV = 1
@@ -29,6 +30,7 @@ class SensorKind(Enum):
29
30
  UPS = 3
30
31
  BAT = 4
31
32
  GRID = 5
33
+ BMS = 6
32
34
 
33
35
 
34
36
  @dataclass
goodwe/modbus.py CHANGED
@@ -9,9 +9,11 @@ MODBUS_READ_CMD: int = 0x3
9
9
  MODBUS_WRITE_CMD: int = 0x6
10
10
  MODBUS_WRITE_MULTI_CMD: int = 0x10
11
11
 
12
+ ILLEGAL_DATA_ADDRESS = 'ILLEGAL DATA ADDRESS'
13
+
12
14
  FAILURE_CODES = {
13
15
  1: "ILLEGAL FUNCTION",
14
- 2: "ILLEGAL DATA ADDRESS",
16
+ 2: ILLEGAL_DATA_ADDRESS,
15
17
  3: "ILLEGAL DATA VALUE",
16
18
  4: "SLAVE DEVICE FAILURE",
17
19
  5: "ACKNOWLEDGE",
goodwe/protocol.py CHANGED
@@ -241,7 +241,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
241
241
  """Send message via transport"""
242
242
  await self._ensure_lock().acquire()
243
243
  try:
244
- await self._connect()
244
+ await asyncio.wait_for(self._connect(), timeout=5)
245
245
  response_future = asyncio.get_running_loop().create_future()
246
246
  self._send_request(command, response_future)
247
247
  await response_future
@@ -249,7 +249,7 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
249
249
  except asyncio.CancelledError:
250
250
  if self._retry < self.retries:
251
251
  if self._timer:
252
- logger.debug("Connection broken error")
252
+ logger.debug("Connection broken error.")
253
253
  self._retry += 1
254
254
  if self._lock and self._lock.locked():
255
255
  self._lock.release()
@@ -257,9 +257,9 @@ class TcpInverterProtocol(InverterProtocol, asyncio.Protocol):
257
257
  return await self.send_request(command)
258
258
  else:
259
259
  return self._max_retries_reached()
260
- except (ConnectionRefusedError, TimeoutError) as exc:
260
+ except (ConnectionRefusedError, TimeoutError, OSError, asyncio.TimeoutError) as exc:
261
261
  if self._retry < self.retries:
262
- logger.debug("Connection refused error: %s", exc)
262
+ logger.debug("Connection refused error.")
263
263
  self._retry += 1
264
264
  if self._lock and self._lock.locked():
265
265
  self._lock.release()
goodwe/sensor.py CHANGED
@@ -515,6 +515,14 @@ class EcoMode(ABC):
515
515
  def set_schedule_type(self, schedule_type: ScheduleType, is745: bool):
516
516
  """Set the schedule type"""
517
517
 
518
+ @abstractmethod
519
+ def get_power(self) -> int:
520
+ """Answer the power value"""
521
+
522
+ @abstractmethod
523
+ def get_power_unit(self) -> str:
524
+ """Answer the power unit"""
525
+
518
526
 
519
527
  class EcoModeV1(Sensor, EcoMode):
520
528
  """Sensor representing Eco Mode Battery Power Group encoded in 8 bytes"""
@@ -606,6 +614,14 @@ class EcoModeV1(Sensor, EcoMode):
606
614
  """Set the schedule type"""
607
615
  pass
608
616
 
617
+ def get_power(self) -> int:
618
+ """Answer the power value"""
619
+ return self.power
620
+
621
+ def get_power_unit(self) -> str:
622
+ """Answer the power unit"""
623
+ return "%"
624
+
609
625
  def as_eco_mode_v2(self) -> EcoModeV2:
610
626
  """Convert V1 to V2 EcoMode"""
611
627
  result = EcoModeV2(self.id_, self.offset, self.name)
@@ -642,7 +658,7 @@ class Schedule(Sensor, EcoMode):
642
658
  def __str__(self):
643
659
  return f"{self.start_h}:{self.start_m}-{self.end_h}:{self.end_m} {self.days} " \
644
660
  f"{self.months + ' ' if self.months else ''}" \
645
- f"{self.schedule_type.decode_power(self.power)}{self.schedule_type.power_unit()} (SoC {self.soc}%) " \
661
+ f"{self.get_power()}{self.get_power_unit()} (SoC {self.soc}%) " \
646
662
  f"{'On' if -10 < self.on_off < 0 else 'Off' if 10 > self.on_off >= 0 else 'Unset'}"
647
663
 
648
664
  def read_value(self, data: ProtocolResponse):
@@ -736,6 +752,14 @@ class Schedule(Sensor, EcoMode):
736
752
  else:
737
753
  self.schedule_type = schedule_type
738
754
 
755
+ def get_power(self) -> int:
756
+ """Answer the power value"""
757
+ return self.schedule_type.decode_power(self.power)
758
+
759
+ def get_power_unit(self) -> str:
760
+ """Answer the power unit"""
761
+ return self.schedule_type.power_unit()
762
+
739
763
  def as_eco_mode_v1(self) -> EcoModeV1:
740
764
  """Convert V2 to V1 EcoMode"""
741
765
  result = EcoModeV1(self.id_, self.offset, self.name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: goodwe
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Read data from GoodWe inverter via local network
5
5
  Home-page: https://github.com/marcelblijleven/goodwe
6
6
  Author: Martin Letenay, Marcel Blijleven
@@ -0,0 +1,16 @@
1
+ goodwe/__init__.py,sha256=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
+ goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
+ goodwe/dt.py,sha256=q8PRs0nVqN4mEhH8243NTbkkBtrGx-n8icwE-BkTN5Q,10460
4
+ goodwe/es.py,sha256=gnSla5SGXK3cJag45o9Z2Wd7rwLkjm3xmS-JN1lf5Ck,22545
5
+ goodwe/et.py,sha256=qqC-1r_Q2gmWSYEHhqdrXRCyBLjihCoWbTmbtSGtLJs,43517
6
+ goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
+ goodwe/inverter.py,sha256=3whUY_ZG7A8aDH1HDSgAzgtFqLOEJBvaxkDwSqKYeuM,10395
8
+ goodwe/modbus.py,sha256=NUlG_d3usiJFjTRNpp61u23CLVSx8NfRJLWP4DmxpMU,8196
9
+ goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
+ goodwe/protocol.py,sha256=vVDLDlnjOEMkfdz8_h1bupU4K8VUxyPbrzfNytUK5us,24926
11
+ goodwe/sensor.py,sha256=9Oo74Qp9vHmV8trfTn3PlGJrp0Ql1vk0U81oCmod-1c,37640
12
+ goodwe-0.4.1.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
+ goodwe-0.4.1.dist-info/METADATA,sha256=QpFz1_icH-w7_ZLAxUT3Ee927v_9tUJqbuNqnbsYiPQ,3376
14
+ goodwe-0.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
+ goodwe-0.4.1.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
+ goodwe-0.4.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- goodwe/__init__.py,sha256=0Zwuri1cbJ2Qe24R2rEjDMTZeVtsh21YIx3KlRaXgWg,5742
2
- goodwe/const.py,sha256=yhWk56YV7k7-MbgfmWEMYNlqeRNLOfOpfTqEfRj6Hp8,7934
3
- goodwe/dt.py,sha256=q8PRs0nVqN4mEhH8243NTbkkBtrGx-n8icwE-BkTN5Q,10460
4
- goodwe/es.py,sha256=gnSla5SGXK3cJag45o9Z2Wd7rwLkjm3xmS-JN1lf5Ck,22545
5
- goodwe/et.py,sha256=VJxCl54DILBRFQTmm-9K2yqS_QbBVMDvPFNv8dr0z7Y,39676
6
- goodwe/exceptions.py,sha256=I6PHG0GTWgxNrDVZwJZBnyzItRq5eiM6ci23-EEsn1I,1012
7
- goodwe/inverter.py,sha256=JIKYcOLihxCG1_m7HGMoFgVR1dyO8F0OXP5q1ClQJ-w,10336
8
- goodwe/modbus.py,sha256=sFmkBgylwJkZd64a52fOUst6Rde5Vm3JsAm3Nh3s6e8,8151
9
- goodwe/model.py,sha256=dWBjMFJMnhZoUdDd9fGT54DERDANz4TirK0Wy8kWMbk,2068
10
- goodwe/protocol.py,sha256=_jPwIlKE5ou2X3_3PDTUzsgBLLD1dzAdyt5DJOsWTWA,24873
11
- goodwe/sensor.py,sha256=fWMYyr3Vw02axfGvL7y7YUH2LmfE4A_lsIulX0Zpy5c,37054
12
- goodwe-0.4.0.dist-info/LICENSE,sha256=aZAhk3lRdYT1YZV-IKRHISEcc_KNUmgfuNO3QhRamNM,1073
13
- goodwe-0.4.0.dist-info/METADATA,sha256=CRS4h7iSlxExGN2ZjLungFGwuI7d85g-TlVRbnPx6vU,3376
14
- goodwe-0.4.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
15
- goodwe-0.4.0.dist-info/top_level.txt,sha256=kKoiqiVvAxDaDJYMZZQLgHQj9cuWT1MXLfXElTDuf8s,7
16
- goodwe-0.4.0.dist-info/RECORD,,
File without changes