From b00ff99b9817324a81ce9e5740745acbbd547461 Mon Sep 17 00:00:00 2001 From: Aric Cyr <3453844+acyr@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:16:31 -0500 Subject: [PATCH 1/7] Add async_set_ev_charging_state endpoint This endpoint can be used to start and stop a SolarEdge EV charger. Signed-off-by: Aric Cyr <3453844+acyr@users.noreply.github.com> --- src/solaredge_web/solaredge.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index 356fec1..c88c4b2 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -209,6 +209,71 @@ async def async_get_energy_data(self, time_unit: TimeUnit = TimeUnit.WEEK) -> li _LOGGER.debug("Found %s energy data for site: %s", len(energy_data), self.site_id) return energy_data + async def async_set_ev_charging_state(self, device_id: str, level: int) -> bool: + """Set the EV charging state to the specified level (0-100). + + device_id: The reporterId of the EV charger device obtained from async_get_home_automation_devices + level: The charging level to set (0-100) + + Set the EV charging state to the specified level (0-100). + Anything below 100 will disable charging if excess PV is not supported. + + Note: This endpoint requires visiting the Smart Home page first to + establish the proper session state before the API call will succeed. + """ + _LOGGER.debug("Setting EV charging state for device %s at site: %s", device_id, self.site_id) + + # Login and ensure we have the necessary cookies + await self.async_login() + cookie = self._find_cookie("SPRING_SECURITY_REMEMBER_ME_COOKIE") + if cookie is None or not cookie.value: + _LOGGER.error("SPRING_SECURITY_REMEMBER_ME_COOKIE cookie not found") + return False + + # Visit the Smart Home page first to establish session state + # This is required for the API to return device data + if not self._smart_home_visited: + smart_home_url = f"https://monitoring.solaredge.com/solaredge-web/p/site/{self.site_id}/#/smart-home" + _LOGGER.debug("Visiting Smart Home page to establish session: %s", smart_home_url) + try: + resp = await self.session.get(smart_home_url, timeout=self.timeout) + _LOGGER.debug("Smart Home page returned %s", resp.status) + resp.raise_for_status() + self._smart_home_visited = True + except aiohttp.ClientError: + _LOGGER.exception("Error visiting Smart Home page %s", smart_home_url) + raise + + url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" + headers = { + "Cookie": f"SPRING_SECURITY_REMEMBER_ME_COOKIE={cookie.value}", + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Mozilla/5.0", + } + payload = { + "mode": "MANUAL", + "level": level, + "duration": None, + } + + try: + async with self.session.put(url, headers=headers, json=payload, timeout=self.timeout) as response: + if response.status != 200: + _LOGGER.error("Failed to set charging state: %s", response.status) + return False + + result = await response.json() + if result.get("status") == "PASSED": + _LOGGER.debug("Successfully set charging state to level %s", level) + return True + + _LOGGER.error("API returned error: %s", result) + return False + except Exception as err: + _LOGGER.error("Error setting charging state: %s", err) + return False + def _find_cookie(self, name: str) -> Morsel[str] | None: """Find a cookie by name.""" for cookie in self.session.cookie_jar: From a098395e17e0c2a7fabda4d975477156b546a4cc Mon Sep 17 00:00:00 2001 From: Aric Cyr <3453844+acyr@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:20:00 -0500 Subject: [PATCH 2/7] Address review feedback - refactor out common code in _async_ensure_smart_home_session - use exceptions for error handling - clean up comments Signed-off-by: Aric Cyr <3453844+acyr@users.noreply.github.com> --- src/solaredge_web/solaredge.py | 82 ++++++++++++++++------------------ 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index c88c4b2..7b6e266 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -121,13 +121,11 @@ def extract_nested_data(node: dict[Any, Any], data_dict: dict[int, dict[str, Any _LOGGER.debug("Found %s equipment for site: %s", len(self._equipment), self.site_id) return self._equipment - async def async_get_home_automation_devices(self) -> dict[str, Any]: - """Get home automation devices from the SolarEdge Web API. + async def _async_ensure_smart_home_session(self) -> None: + """ Ensure that the smart home session is established. - Returns device information from the home automation API endpoint. - - Note: This endpoint requires visiting the Smart Home page first to - establish the proper session state before the API call will succeed. + This is required before accessing home automation APIs. + Login is performed and the Smart Home page is visited to set up session state if needed. """ _LOGGER.debug("Fetching home automation devices for site: %s", self.site_id) await self.async_login() @@ -146,6 +144,19 @@ async def async_get_home_automation_devices(self) -> dict[str, Any]: _LOGGER.exception("Error visiting Smart Home page %s", smart_home_url) raise + async def async_get_home_automation_devices(self) -> dict[str, Any]: + """Get home automation devices from the SolarEdge Web API. + + Returns device information from the home automation API endpoint. + """ + + # Ensure smart home session is established + try: + await self._async_ensure_smart_home_session() + except Exception as e: + _LOGGER.error("Failed to establish smart home session: %s", e) + raise + url = f"https://monitoring.solaredge.com/services/api/homeautomation/v1.0/sites/{self.site_id}/devices" try: @@ -209,47 +220,30 @@ async def async_get_energy_data(self, time_unit: TimeUnit = TimeUnit.WEEK) -> li _LOGGER.debug("Found %s energy data for site: %s", len(energy_data), self.site_id) return energy_data - async def async_set_ev_charging_state(self, device_id: str, level: int) -> bool: + async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: """Set the EV charging state to the specified level (0-100). device_id: The reporterId of the EV charger device obtained from async_get_home_automation_devices level: The charging level to set (0-100) - Set the EV charging state to the specified level (0-100). Anything below 100 will disable charging if excess PV is not supported. - - Note: This endpoint requires visiting the Smart Home page first to - establish the proper session state before the API call will succeed. """ - _LOGGER.debug("Setting EV charging state for device %s at site: %s", device_id, self.site_id) - # Login and ensure we have the necessary cookies - await self.async_login() + # Ensure smart home session is established + try: + await self._async_ensure_smart_home_session() + except Exception as e: + _LOGGER.error("Failed to establish smart home session: %s", e) + raise + cookie = self._find_cookie("SPRING_SECURITY_REMEMBER_ME_COOKIE") if cookie is None or not cookie.value: _LOGGER.error("SPRING_SECURITY_REMEMBER_ME_COOKIE cookie not found") - return False - - # Visit the Smart Home page first to establish session state - # This is required for the API to return device data - if not self._smart_home_visited: - smart_home_url = f"https://monitoring.solaredge.com/solaredge-web/p/site/{self.site_id}/#/smart-home" - _LOGGER.debug("Visiting Smart Home page to establish session: %s", smart_home_url) - try: - resp = await self.session.get(smart_home_url, timeout=self.timeout) - _LOGGER.debug("Smart Home page returned %s", resp.status) - resp.raise_for_status() - self._smart_home_visited = True - except aiohttp.ClientError: - _LOGGER.exception("Error visiting Smart Home page %s", smart_home_url) - raise + raise aiohttp.ClientError("SPRING_SECURITY_REMEMBER_ME_COOKIE cookie not found") url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" headers = { "Cookie": f"SPRING_SECURITY_REMEMBER_ME_COOKIE={cookie.value}", - "Content-Type": "application/json", - "Accept": "application/json", - "User-Agent": "Mozilla/5.0", } payload = { "mode": "MANUAL", @@ -258,21 +252,21 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> bool: } try: - async with self.session.put(url, headers=headers, json=payload, timeout=self.timeout) as response: - if response.status != 200: - _LOGGER.error("Failed to set charging state: %s", response.status) - return False + async with self.session.put(url, headers=headers, json=payload, timeout=self.timeout) as resp: + if resp.status != 200: + _LOGGER.error("Failed to set charging state: %s", resp.status) + resp.raise_for_status() - result = await response.json() - if result.get("status") == "PASSED": + resp_json = await resp.json() + if resp_json.get("status") == "PASSED": _LOGGER.debug("Successfully set charging state to level %s", level) - return True + return - _LOGGER.error("API returned error: %s", result) - return False - except Exception as err: - _LOGGER.error("Error setting charging state: %s", err) - return False + _LOGGER.error("API failed with status: %s", resp_json.get("status")) + raise aiohttp.ClientError(f"API failed with status: {resp_json.get('status')}") + except aiohttp.ClientError: + _LOGGER.exception("Error setting EV charging state at %s", url) + raise def _find_cookie(self, name: str) -> Morsel[str] | None: """Find a cookie by name.""" From b722d8b4dbb7ab93450f6d9359488c3d28d24d63 Mon Sep 17 00:00:00 2001 From: Aric Cyr <3453844+acyr@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:58:42 -0500 Subject: [PATCH 3/7] Address code review - move debug logs to appropriate functions - remove explicit headers passed to session.put as unneeded - remove excessive exception handling Signed-off-by: Aric Cyr <3453844+acyr@users.noreply.github.com> --- src/solaredge_web/solaredge.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index 7b6e266..a1ad984 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -127,7 +127,6 @@ async def _async_ensure_smart_home_session(self) -> None: This is required before accessing home automation APIs. Login is performed and the Smart Home page is visited to set up session state if needed. """ - _LOGGER.debug("Fetching home automation devices for site: %s", self.site_id) await self.async_login() # Visit the Smart Home page first to establish session state @@ -149,13 +148,10 @@ async def async_get_home_automation_devices(self) -> dict[str, Any]: Returns device information from the home automation API endpoint. """ + _LOGGER.debug("Fetching home automation devices for site: %s", self.site_id) # Ensure smart home session is established - try: - await self._async_ensure_smart_home_session() - except Exception as e: - _LOGGER.error("Failed to establish smart home session: %s", e) - raise + await self._async_ensure_smart_home_session() url = f"https://monitoring.solaredge.com/services/api/homeautomation/v1.0/sites/{self.site_id}/devices" @@ -228,13 +224,10 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: Anything below 100 will disable charging if excess PV is not supported. """ + _LOGGER.debug("Setting EV charging state for device: %s to level: %s", device_id, level) # Ensure smart home session is established - try: - await self._async_ensure_smart_home_session() - except Exception as e: - _LOGGER.error("Failed to establish smart home session: %s", e) - raise + await self._async_ensure_smart_home_session() cookie = self._find_cookie("SPRING_SECURITY_REMEMBER_ME_COOKIE") if cookie is None or not cookie.value: @@ -242,17 +235,10 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: raise aiohttp.ClientError("SPRING_SECURITY_REMEMBER_ME_COOKIE cookie not found") url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" - headers = { - "Cookie": f"SPRING_SECURITY_REMEMBER_ME_COOKIE={cookie.value}", - } - payload = { - "mode": "MANUAL", - "level": level, - "duration": None, - } - try: - async with self.session.put(url, headers=headers, json=payload, timeout=self.timeout) as resp: + async with self.session.put(url, + json={"mode": "MANUAL", "level": level, "duration": None }, + timeout=self.timeout) as resp: if resp.status != 200: _LOGGER.error("Failed to set charging state: %s", resp.status) resp.raise_for_status() From 392d86b7e4fb6064247b584038d54bdb8e9d19ea Mon Sep 17 00:00:00 2001 From: Aric Cyr <3453844+acyr@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:11:20 -0500 Subject: [PATCH 4/7] Remove redundant comments Signed-off-by: Aric Cyr <3453844+acyr@users.noreply.github.com> --- src/solaredge_web/solaredge.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index a1ad984..44ac813 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -150,7 +150,6 @@ async def async_get_home_automation_devices(self) -> dict[str, Any]: """ _LOGGER.debug("Fetching home automation devices for site: %s", self.site_id) - # Ensure smart home session is established await self._async_ensure_smart_home_session() url = f"https://monitoring.solaredge.com/services/api/homeautomation/v1.0/sites/{self.site_id}/devices" @@ -226,7 +225,6 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: """ _LOGGER.debug("Setting EV charging state for device: %s to level: %s", device_id, level) - # Ensure smart home session is established await self._async_ensure_smart_home_session() cookie = self._find_cookie("SPRING_SECURITY_REMEMBER_ME_COOKIE") From 7955c7fa645620cdae218ed31c601cb792cc2830 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:12:11 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/solaredge_web/solaredge.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index 44ac813..607293f 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -122,7 +122,7 @@ def extract_nested_data(node: dict[Any, Any], data_dict: dict[int, dict[str, Any return self._equipment async def _async_ensure_smart_home_session(self) -> None: - """ Ensure that the smart home session is established. + """Ensure that the smart home session is established. This is required before accessing home automation APIs. Login is performed and the Smart Home page is visited to set up session state if needed. @@ -234,9 +234,9 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" try: - async with self.session.put(url, - json={"mode": "MANUAL", "level": level, "duration": None }, - timeout=self.timeout) as resp: + async with self.session.put( + url, json={"mode": "MANUAL", "level": level, "duration": None}, timeout=self.timeout + ) as resp: if resp.status != 200: _LOGGER.error("Failed to set charging state: %s", resp.status) resp.raise_for_status() From 037c8423f03309943f9b389a14aa71ecf3ffbc06 Mon Sep 17 00:00:00 2001 From: Aric Cyr <3453844+acyr@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:43:26 -0500 Subject: [PATCH 6/7] Address CI check Move JSON parsing outside of exception handling to propogate errors upwards to the client Signed-off-by: Aric Cyr <3453844+acyr@users.noreply.github.com> --- src/solaredge_web/solaredge.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index 607293f..9500c8b 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -234,24 +234,26 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" try: - async with self.session.put( - url, json={"mode": "MANUAL", "level": level, "duration": None}, timeout=self.timeout - ) as resp: - if resp.status != 200: + resp = await self.session.put( + url, + json={"mode": "MANUAL", "level": level, "duration": None}, + timeout=self.timeout + ) + if resp.status != 200: _LOGGER.error("Failed to set charging state: %s", resp.status) resp.raise_for_status() - - resp_json = await resp.json() - if resp_json.get("status") == "PASSED": - _LOGGER.debug("Successfully set charging state to level %s", level) - return - - _LOGGER.error("API failed with status: %s", resp_json.get("status")) - raise aiohttp.ClientError(f"API failed with status: {resp_json.get('status')}") except aiohttp.ClientError: _LOGGER.exception("Error setting EV charging state at %s", url) raise + resp_json = await resp.json() + if resp_json.get("status") == "PASSED": + _LOGGER.debug("Successfully set charging state to level %s", level) + return + + _LOGGER.error("API failed with status: %s", resp_json.get("status")) + raise aiohttp.ClientError(f"API failed with status: {resp_json.get('status')}") + def _find_cookie(self, name: str) -> Morsel[str] | None: """Find a cookie by name.""" for cookie in self.session.cookie_jar: From ca497500a144045e9792f9be244b06539af733c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:44:05 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/solaredge_web/solaredge.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/solaredge_web/solaredge.py b/src/solaredge_web/solaredge.py index 9500c8b..53f1fd5 100644 --- a/src/solaredge_web/solaredge.py +++ b/src/solaredge_web/solaredge.py @@ -234,14 +234,10 @@ async def async_set_ev_charging_state(self, device_id: str, level: int) -> None: url = f"https://monitoring.solaredge.com/services/m/api/homeautomation/v1.0/{self.site_id}/devices/{device_id}/activationState" try: - resp = await self.session.put( - url, - json={"mode": "MANUAL", "level": level, "duration": None}, - timeout=self.timeout - ) + resp = await self.session.put(url, json={"mode": "MANUAL", "level": level, "duration": None}, timeout=self.timeout) if resp.status != 200: - _LOGGER.error("Failed to set charging state: %s", resp.status) - resp.raise_for_status() + _LOGGER.error("Failed to set charging state: %s", resp.status) + resp.raise_for_status() except aiohttp.ClientError: _LOGGER.exception("Error setting EV charging state at %s", url) raise