Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Sun WEG inverter sensor integration."""
2 
3 import datetime
4 import json
5 import logging
6 
7 from sunweg.api import APIHelper
8 from sunweg.plant import Plant
9 
10 from homeassistant import config_entries
11 from homeassistant.config_entries import ConfigEntry
12 from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
13 from homeassistant.core import HomeAssistant
14 from homeassistant.exceptions import ConfigEntryAuthFailed
15 from homeassistant.helpers.typing import StateType, UndefinedType
16 from homeassistant.util import Throttle
17 
18 from .const import CONF_PLANT_ID, DOMAIN, PLATFORMS, DeviceType
19 
20 SCAN_INTERVAL = datetime.timedelta(minutes=5)
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 
26  hass: HomeAssistant, entry: config_entries.ConfigEntry
27 ) -> bool:
28  """Load the saved entities."""
29  api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
30  if not await hass.async_add_executor_job(api.authenticate):
31  raise ConfigEntryAuthFailed("Username or Password may be incorrect!")
32  hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData(
33  api, entry.data[CONF_PLANT_ID]
34  )
35  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
36  return True
37 
38 
39 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
40  """Unload a config entry."""
41  hass.data[DOMAIN].pop(entry.entry_id)
42  if len(hass.data[DOMAIN]) == 0:
43  hass.data.pop(DOMAIN)
44  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
45 
46 
47 class SunWEGData:
48  """The class for handling data retrieval."""
49 
50  def __init__(
51  self,
52  api: APIHelper,
53  plant_id: int,
54  ) -> None:
55  """Initialize the probe."""
56 
57  self.apiapi = api
58  self.plant_idplant_id = plant_id
59  self.datadata: Plant = None
60  self.previous_values: dict = {}
61 
62  @Throttle(SCAN_INTERVAL)
63  def update(self) -> None:
64  """Update probe data."""
65  _LOGGER.debug("Updating data for plant %s", self.plant_idplant_id)
66  try:
67  self.datadata = self.apiapi.plant(self.plant_idplant_id)
68  for inverter in self.datadata.inverters:
69  self.apiapi.complete_inverter(inverter)
70  except json.decoder.JSONDecodeError:
71  _LOGGER.error("Unable to fetch data from SunWEG server")
72  _LOGGER.debug("Finished updating data for plant %s", self.plant_idplant_id)
73 
75  self,
76  variable: str,
77  device_type: DeviceType,
78  inverter_id: int = 0,
79  deep_name: str | None = None,
80  ):
81  """Retrieve from a Plant the desired variable value."""
82  if device_type == DeviceType.TOTAL:
83  return self.datadata.__dict__.get(variable)
84 
85  inverter_list = [i for i in self.datadata.inverters if i.id == inverter_id]
86  if len(inverter_list) == 0:
87  return None
88  inverter = inverter_list[0]
89 
90  if device_type == DeviceType.INVERTER:
91  return inverter.__dict__.get(variable)
92  if device_type == DeviceType.PHASE:
93  for phase in inverter.phases:
94  if phase.name == deep_name:
95  return phase.__dict__.get(variable)
96  elif device_type == DeviceType.STRING:
97  for mppt in inverter.mppts:
98  for string in mppt.strings:
99  if string.name == deep_name:
100  return string.__dict__.get(variable)
101  return None
102 
103  def get_data(
104  self,
105  *,
106  api_variable_key: str,
107  api_variable_unit: str | None,
108  deep_name: str | None,
109  device_type: DeviceType,
110  inverter_id: int,
111  name: str | UndefinedType | None,
112  native_unit_of_measurement: str | None,
113  never_resets: bool,
114  previous_value_drop_threshold: float | None,
115  ) -> tuple[StateType | datetime.datetime, str | None]:
116  """Get the data."""
117  _LOGGER.debug(
118  "Data request for: %s",
119  name,
120  )
121  variable = api_variable_key
122  previous_unit = native_unit_of_measurement
123  api_value = self.get_api_valueget_api_value(variable, device_type, inverter_id, deep_name)
124  previous_value = self.previous_values.get(variable)
125  return_value = api_value
126  if api_variable_unit is not None:
127  native_unit_of_measurement = self.get_api_valueget_api_value(
128  api_variable_unit,
129  device_type,
130  inverter_id,
131  deep_name,
132  )
133 
134  # If we have a 'drop threshold' specified, then check it and correct if needed
135  if (
136  previous_value_drop_threshold is not None
137  and previous_value is not None
138  and api_value is not None
139  and previous_unit == native_unit_of_measurement
140  ):
141  _LOGGER.debug(
142  (
143  "%s - Drop threshold specified (%s), checking for drop... API"
144  " Value: %s, Previous Value: %s"
145  ),
146  name,
147  previous_value_drop_threshold,
148  api_value,
149  previous_value,
150  )
151  diff = float(api_value) - float(previous_value)
152 
153  # Check if the value has dropped (negative value i.e. < 0) and it has only
154  # dropped by a small amount, if so, use the previous value.
155  # Note - The energy dashboard takes care of drops within 10%
156  # of the current value, however if the value is low e.g. 0.2
157  # and drops by 0.1 it classes as a reset.
158  if -(previous_value_drop_threshold) <= diff < 0:
159  _LOGGER.debug(
160  (
161  "Diff is negative, but only by a small amount therefore not a"
162  " nightly reset, using previous value (%s) instead of api value"
163  " (%s)"
164  ),
165  previous_value,
166  api_value,
167  )
168  return_value = previous_value
169  else:
170  _LOGGER.debug("%s - No drop detected, using API value", name)
171 
172  # Lifetime total values should always be increasing, they will never reset,
173  # however the API sometimes returns 0 values when the clock turns to 00:00
174  # local time in that scenario we should just return the previous value
175  # Scenarios:
176  # 1 - System has a genuine 0 value when it it first commissioned:
177  # - will return 0 until a non-zero value is registered
178  # 2 - System has been running fine but temporarily resets to 0 briefly
179  # at midnight:
180  # - will return the previous value
181  # 3 - HA is restarted during the midnight 'outage' - Not handled:
182  # - Previous value will not exist meaning 0 will be returned
183  # - This is an edge case that would be better handled by looking
184  # up the previous value of the entity from the recorder
185  if never_resets and api_value == 0 and previous_value:
186  _LOGGER.debug(
187  (
188  "API value is 0, but this value should never reset, returning"
189  " previous value (%s) instead"
190  ),
191  previous_value,
192  )
193  return_value = previous_value
194 
195  self.previous_values[variable] = return_value
196 
197  return (return_value, native_unit_of_measurement)
tuple[StateType|datetime.datetime, str|None] get_data(self, *str api_variable_key, str|None api_variable_unit, str|None deep_name, DeviceType device_type, int inverter_id, str|UndefinedType|None name, str|None native_unit_of_measurement, bool never_resets, float|None previous_value_drop_threshold)
Definition: __init__.py:115
def get_api_value(self, str variable, DeviceType device_type, int inverter_id=0, str|None deep_name=None)
Definition: __init__.py:80
None __init__(self, APIHelper api, int plant_id)
Definition: __init__.py:54
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:39
bool async_setup_entry(HomeAssistant hass, config_entries.ConfigEntry entry)
Definition: __init__.py:27