api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. """API client for Solar of Things."""
  2. from __future__ import annotations
  3. import logging
  4. from datetime import datetime, timedelta
  5. from typing import Any
  6. import requests
  7. from .const import (
  8. API_BASE_URL,
  9. API_TIME_SERIES,
  10. API_MONTHLY_SUMMARY,
  11. API_SETTINGS_GET,
  12. API_SETTINGS_SET,
  13. )
  14. _LOGGER = logging.getLogger(__name__)
  15. class SolarOfThingsAPI:
  16. """API client for Solar of Things."""
  17. def __init__(
  18. self,
  19. iot_token: str,
  20. station_id: str | None = None,
  21. device_id: str | None = None,
  22. ) -> None:
  23. """Initialize the API client."""
  24. self.iot_token = iot_token
  25. self.station_id = station_id
  26. self.device_id = device_id
  27. self.session = requests.Session()
  28. self.session.headers.update({
  29. "IOT-Token": self.iot_token,
  30. "Content-Type": "application/json",
  31. })
  32. def fetch_latest_data(self) -> dict[str, Any]:
  33. """Fetch the latest time-series data."""
  34. if not self.device_id:
  35. return {}
  36. # Get data for the last hour
  37. end_time = datetime.now()
  38. start_time = end_time - timedelta(hours=1)
  39. payload = {
  40. "deviceId": self.device_id,
  41. "startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
  42. "endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
  43. "keys": [
  44. "pvInputPower",
  45. "acOutputActivePower",
  46. "batteryDischargeCurrent",
  47. "batteryChargingCurrent",
  48. "batteryVoltage",
  49. "feedInPower",
  50. "batteryPower",
  51. "batterySOC",
  52. ],
  53. }
  54. try:
  55. response = self.session.post(
  56. f"{API_BASE_URL}{API_TIME_SERIES}",
  57. json=payload,
  58. timeout=30,
  59. )
  60. response.raise_for_status()
  61. data = response.json()
  62. # Extract latest values from each key
  63. latest_values = {}
  64. if "data" in data:
  65. for key, values in data["data"].items():
  66. if values and len(values) > 0:
  67. # Get the most recent value
  68. latest_values[key] = values[-1]["value"]
  69. # Calculate derived values
  70. if "batteryVoltage" in latest_values:
  71. voltage = float(latest_values.get("batteryVoltage", 0))
  72. discharge = float(latest_values.get("batteryDischargeCurrent", 0))
  73. charge = float(latest_values.get("batteryChargingCurrent", 0))
  74. # Battery power = (discharge - charge) * voltage
  75. latest_values["batteryPower"] = (discharge - charge) * voltage
  76. # Grid power can be calculated if we have all components
  77. pv_power = float(latest_values.get("pvInputPower", 0))
  78. ac_output = float(latest_values.get("acOutputActivePower", 0))
  79. feed_in = float(latest_values.get("feedInPower", 0))
  80. battery_power = latest_values["batteryPower"]
  81. # Grid import = Load - PV - Battery discharge + Feed-in
  82. # Simplified: Grid = AC output - PV - Battery power + Feed-in
  83. latest_values["gridPower"] = max(0, ac_output - pv_power + battery_power + feed_in)
  84. latest_values["loadPower"] = ac_output
  85. return latest_values
  86. except requests.exceptions.RequestException as err:
  87. _LOGGER.error("Error fetching time-series data: %s", err)
  88. return {}
  89. def fetch_monthly_summary(self) -> dict[str, Any]:
  90. """Fetch monthly summary data."""
  91. if not self.station_id:
  92. return {}
  93. # Get current year and month
  94. now = datetime.now()
  95. payload = {
  96. "stationId": self.station_id,
  97. "year": now.year,
  98. }
  99. try:
  100. response = self.session.post(
  101. f"{API_BASE_URL}{API_MONTHLY_SUMMARY}",
  102. json=payload,
  103. timeout=30,
  104. )
  105. response.raise_for_status()
  106. data = response.json()
  107. # Extract current month data
  108. monthly_data = {}
  109. if "data" in data and "summary" in data["data"]:
  110. for month_data in data["data"]["summary"]:
  111. if month_data.get("month") == now.month:
  112. # PV generation in kWh
  113. pv_generated = float(month_data.get("pvGenerated", 0))
  114. monthly_data["monthly_pv_generated"] = pv_generated
  115. # Note: The API doesn't provide grid import directly
  116. # These would need to be calculated from time-series data
  117. # For now, we'll just provide PV generation
  118. break
  119. return monthly_data
  120. except requests.exceptions.RequestException as err:
  121. _LOGGER.error("Error fetching monthly summary: %s", err)
  122. return {}
  123. def test_connection(self) -> bool:
  124. """Test the API connection."""
  125. try:
  126. # Try to fetch a small amount of data
  127. end_time = datetime.now()
  128. start_time = end_time - timedelta(minutes=10)
  129. payload = {
  130. "deviceId": self.device_id,
  131. "startTime": start_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
  132. "endTime": end_time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
  133. "keys": ["pvInputPower"],
  134. }
  135. response = self.session.post(
  136. f"{API_BASE_URL}{API_TIME_SERIES}",
  137. json=payload,
  138. timeout=10,
  139. )
  140. response.raise_for_status()
  141. return True
  142. except requests.exceptions.RequestException:
  143. return False
  144. def fetch_settings(self) -> dict[str, Any]:
  145. """Fetch current device settings."""
  146. if not self.device_id:
  147. return {}
  148. payload = {
  149. "deviceId": self.device_id,
  150. }
  151. try:
  152. response = self.session.post(
  153. f"{API_BASE_URL}{API_SETTINGS_GET}",
  154. json=payload,
  155. timeout=30,
  156. )
  157. response.raise_for_status()
  158. data = response.json()
  159. # Extract settings from response
  160. settings = {}
  161. if "data" in data:
  162. settings_data = data["data"]
  163. settings["batteryChargeLimit"] = settings_data.get("batteryChargeLimit", 100)
  164. settings["batteryDischargeLimit"] = settings_data.get("batteryDischargeLimit", 10)
  165. settings["gridChargeLimit"] = settings_data.get("gridChargeLimit", 0)
  166. settings["operatingMode"] = settings_data.get("operatingMode", "Self-Use")
  167. settings["batteryPriority"] = settings_data.get("batteryPriority", "Solar First")
  168. settings["gridChargingEnabled"] = settings_data.get("gridChargingEnabled", False)
  169. settings["gridFeedInEnabled"] = settings_data.get("gridFeedInEnabled", True)
  170. settings["backupModeEnabled"] = settings_data.get("backupModeEnabled", False)
  171. return settings
  172. except requests.exceptions.RequestException as err:
  173. _LOGGER.error("Error fetching settings: %s", err)
  174. return {}
  175. def set_battery_charge_limit(self, limit: int) -> bool:
  176. """Set battery charge limit (0-100%)."""
  177. return self._update_setting("batteryChargeLimit", limit)
  178. def set_battery_discharge_limit(self, limit: int) -> bool:
  179. """Set battery discharge limit (0-100%)."""
  180. return self._update_setting("batteryDischargeLimit", limit)
  181. def set_grid_charge_limit(self, limit: int) -> bool:
  182. """Set grid charging power limit (W)."""
  183. return self._update_setting("gridChargeLimit", limit)
  184. def set_operating_mode(self, mode: str) -> bool:
  185. """Set system operating mode."""
  186. return self._update_setting("operatingMode", mode)
  187. def set_battery_priority(self, priority: str) -> bool:
  188. """Set battery priority mode."""
  189. return self._update_setting("batteryPriority", priority)
  190. def set_grid_charging(self, enabled: bool) -> bool:
  191. """Enable or disable grid charging."""
  192. return self._update_setting("gridChargingEnabled", enabled)
  193. def set_grid_feed_in(self, enabled: bool) -> bool:
  194. """Enable or disable grid feed-in."""
  195. return self._update_setting("gridFeedInEnabled", enabled)
  196. def set_backup_mode(self, enabled: bool) -> bool:
  197. """Enable or disable backup mode."""
  198. return self._update_setting("backupModeEnabled", enabled)
  199. def _update_setting(self, setting_key: str, value: Any) -> bool:
  200. """Update a specific setting."""
  201. if not self.device_id:
  202. _LOGGER.error("Device ID is required for settings update")
  203. return False
  204. payload = {
  205. "deviceId": self.device_id,
  206. "settings": {
  207. setting_key: value
  208. }
  209. }
  210. try:
  211. response = self.session.post(
  212. f"{API_BASE_URL}{API_SETTINGS_SET}",
  213. json=payload,
  214. timeout=30,
  215. )
  216. response.raise_for_status()
  217. data = response.json()
  218. # Check if update was successful
  219. if "success" in data and data["success"]:
  220. _LOGGER.info("Successfully updated %s to %s", setting_key, value)
  221. return True
  222. else:
  223. _LOGGER.error("Failed to update %s: %s", setting_key, data.get("message", "Unknown error"))
  224. return False
  225. except requests.exceptions.RequestException as err:
  226. _LOGGER.error("Error updating setting %s: %s", setting_key, err)
  227. return False