upgrade homeassistant
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,233 +29,280 @@ _DefaultHeaders = {
|
||||
class SemsApi:
|
||||
"""Interface to the SEMS API."""
|
||||
|
||||
def __init__(self, hass, username, password):
|
||||
def __init__(self, hass: HomeAssistant, username: str, password: str) -> None:
|
||||
"""Init dummy hub."""
|
||||
self._hass = hass
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._token = None
|
||||
self._token: dict[str, Any] | None = None
|
||||
|
||||
def test_authentication(self) -> bool:
|
||||
"""Test if we can authenticate with the host."""
|
||||
try:
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
return self._token is not None
|
||||
except Exception as exception:
|
||||
_LOGGER.exception("SEMS Authentication exception: %s", exception)
|
||||
return False
|
||||
else:
|
||||
return self._token is not None
|
||||
|
||||
def getLoginToken(self, userName, password):
|
||||
def _make_http_request(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
data: str | None = None,
|
||||
json_data: dict[str, Any] | None = None,
|
||||
operation_name: str = "HTTP request",
|
||||
validate_code: bool = True,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Make a generic HTTP request with error handling and optional code validation."""
|
||||
try:
|
||||
_LOGGER.debug("SEMS - Making %s to %s", operation_name, url)
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
json=json_data,
|
||||
timeout=_RequestTimeout,
|
||||
)
|
||||
|
||||
_LOGGER.debug("%s Response: %s", operation_name, response)
|
||||
# _LOGGER.debug("%s Response text: %s", operation_name, response.text)
|
||||
|
||||
response.raise_for_status()
|
||||
jsonResponse: dict[str, Any] = response.json()
|
||||
|
||||
# Validate response code if requested
|
||||
if validate_code:
|
||||
if jsonResponse.get("code") not in (0, "0"):
|
||||
_LOGGER.error(
|
||||
"%s failed with code: %s, message: %s",
|
||||
operation_name,
|
||||
jsonResponse.get("code"),
|
||||
jsonResponse.get("msg", "Unknown error"),
|
||||
)
|
||||
return None
|
||||
|
||||
if jsonResponse.get("data") is None:
|
||||
_LOGGER.error("%s response missing data field", operation_name)
|
||||
return None
|
||||
|
||||
return jsonResponse
|
||||
|
||||
except (requests.RequestException, ValueError, KeyError) as exception:
|
||||
_LOGGER.error("Unable to complete %s: %s", operation_name, exception)
|
||||
raise
|
||||
|
||||
def getLoginToken(self, userName: str, password: str) -> dict[str, Any] | None:
|
||||
"""Get the login token for the SEMS API."""
|
||||
try:
|
||||
# Get our Authentication Token from SEMS Portal API
|
||||
_LOGGER.debug("SEMS - Getting API token")
|
||||
|
||||
# Prepare Login Data to retrieve Authentication Token
|
||||
# Dict won't work here somehow, so this magic string creation must do.
|
||||
login_data = '{"account":"' + userName + '","pwd":"' + password + '"}'
|
||||
# login_data = {"account": userName, "pwd": password}
|
||||
|
||||
# Make POST request to retrieve Authentication Token from SEMS API
|
||||
login_response = requests.post(
|
||||
jsonResponse = self._make_http_request(
|
||||
_LoginURL,
|
||||
headers=_DefaultHeaders,
|
||||
_DefaultHeaders,
|
||||
data=login_data,
|
||||
timeout=_RequestTimeout,
|
||||
operation_name="login API call",
|
||||
validate_code=True,
|
||||
)
|
||||
_LOGGER.debug("Login Response: %s", login_response)
|
||||
# _LOGGER.debug("Login Response text: %s", login_response.text)
|
||||
|
||||
login_response.raise_for_status()
|
||||
if jsonResponse is None:
|
||||
return None
|
||||
|
||||
# Process response as JSON
|
||||
jsonResponse = login_response.json() # json.loads(login_response.text)
|
||||
# _LOGGER.debug("Login JSON response %s", jsonResponse)
|
||||
# Get all the details from our response, needed to make the next POST request (the one that really fetches the data)
|
||||
# Also store the api url send with the authentication request for later use
|
||||
tokenDict = jsonResponse["data"]
|
||||
if not isinstance(tokenDict, dict):
|
||||
_LOGGER.error("Login response data was not a dict")
|
||||
return None
|
||||
tokenDict["api"] = jsonResponse["api"]
|
||||
|
||||
_LOGGER.debug("SEMS - API Token received: %s", tokenDict)
|
||||
return tokenDict
|
||||
except Exception as exception:
|
||||
_LOGGER.error("Unable to fetch login token from SEMS API. %s", exception)
|
||||
|
||||
except (requests.RequestException, ValueError, KeyError) as exception:
|
||||
_LOGGER.error("Unable to fetch login token from SEMS API: %s", exception)
|
||||
return None
|
||||
|
||||
def getPowerStationIds(self, renewToken=False, maxTokenRetries=2) -> str:
|
||||
def _make_api_call(
|
||||
self,
|
||||
url_part: str,
|
||||
data: str | None = None,
|
||||
renewToken: bool = False,
|
||||
maxTokenRetries: int = 2,
|
||||
operation_name: str = "API call",
|
||||
) -> dict[str, Any] | None:
|
||||
"""Make a generic API call with token management and retry logic."""
|
||||
_LOGGER.debug("SEMS - Making %s", operation_name)
|
||||
if maxTokenRetries <= 0:
|
||||
_LOGGER.info("SEMS - Maximum token fetch tries reached, aborting for now")
|
||||
raise OutOfRetries
|
||||
|
||||
if self._token is None or renewToken:
|
||||
_LOGGER.debug(
|
||||
"API token not set (%s) or new token requested (%s), fetching",
|
||||
self._token,
|
||||
renewToken,
|
||||
)
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
|
||||
if self._token is None:
|
||||
_LOGGER.error("Failed to obtain API token")
|
||||
return None
|
||||
|
||||
# Prepare headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"token": json.dumps(self._token),
|
||||
}
|
||||
|
||||
api_url = self._token["api"] + url_part
|
||||
|
||||
try:
|
||||
jsonResponse: dict[str, Any] | None = self._make_http_request(
|
||||
api_url,
|
||||
headers,
|
||||
data=data,
|
||||
operation_name=operation_name,
|
||||
validate_code=True,
|
||||
)
|
||||
|
||||
# _make_http_request already validated the response, so if we get here, it's successful
|
||||
if jsonResponse is None:
|
||||
# Response validation failed in _make_http_request
|
||||
_LOGGER.debug(
|
||||
"%s not successful, retrying with new token, %s retries remaining",
|
||||
operation_name,
|
||||
maxTokenRetries,
|
||||
)
|
||||
return self._make_api_call(
|
||||
url_part, data, True, maxTokenRetries - 1, operation_name
|
||||
)
|
||||
|
||||
# Response is valid, return the data
|
||||
return jsonResponse["data"]
|
||||
|
||||
except (requests.RequestException, ValueError, KeyError) as exception:
|
||||
_LOGGER.error("Unable to complete %s: %s", operation_name, exception)
|
||||
return None
|
||||
|
||||
def getPowerStationIds(
|
||||
self, renewToken: bool = False, maxTokenRetries: int = 2
|
||||
) -> dict[str, Any] | None:
|
||||
"""Get the power station ids from the SEMS API."""
|
||||
try:
|
||||
# Get the status of our SEMS Power Station
|
||||
_LOGGER.debug(
|
||||
"SEMS - getPowerStationIds Making Power Station Status API Call"
|
||||
)
|
||||
if maxTokenRetries <= 0:
|
||||
_LOGGER.info(
|
||||
"SEMS - Maximum token fetch tries reached, aborting for now"
|
||||
)
|
||||
raise OutOfRetries
|
||||
if self._token is None or renewToken:
|
||||
_LOGGER.debug(
|
||||
"API token not set (%s) or new token requested (%s), fetching",
|
||||
self._token,
|
||||
renewToken,
|
||||
)
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
return self._make_api_call(
|
||||
_GetPowerStationIdByOwnerURLPart,
|
||||
data=None,
|
||||
renewToken=renewToken,
|
||||
maxTokenRetries=maxTokenRetries,
|
||||
operation_name="getPowerStationIds API call",
|
||||
)
|
||||
|
||||
# Prepare Power Station status Headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"token": json.dumps(self._token),
|
||||
}
|
||||
|
||||
getPowerStationIdUrl = self._token["api"] + _GetPowerStationIdByOwnerURLPart
|
||||
_LOGGER.debug(
|
||||
"Querying SEMS API (%s) for power station ids by owner",
|
||||
getPowerStationIdUrl,
|
||||
)
|
||||
|
||||
response = requests.post(
|
||||
getPowerStationIdUrl,
|
||||
headers=headers,
|
||||
# data=data,
|
||||
timeout=_RequestTimeout,
|
||||
)
|
||||
jsonResponse = response.json()
|
||||
_LOGGER.debug("Response: %s", jsonResponse)
|
||||
# try again and renew token is unsuccessful
|
||||
if (
|
||||
jsonResponse["msg"] not in ["Successful", "操作成功"]
|
||||
or jsonResponse["data"] is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"GetPowerStationIdByOwner Query not successful (%s), retrying with new token, %s retries remaining",
|
||||
jsonResponse["msg"],
|
||||
maxTokenRetries,
|
||||
)
|
||||
return self.getPowerStationIds(
|
||||
True, maxTokenRetries=maxTokenRetries - 1
|
||||
)
|
||||
|
||||
return jsonResponse["data"]
|
||||
except Exception as exception:
|
||||
_LOGGER.error(
|
||||
"Unable to fetch power station Ids from SEMS Api. %s", exception
|
||||
)
|
||||
|
||||
def getData(self, powerStationId, renewToken=False, maxTokenRetries=2) -> dict:
|
||||
def getData(
|
||||
self, powerStationId: str, renewToken: bool = False, maxTokenRetries: int = 2
|
||||
) -> dict[str, Any]:
|
||||
"""Get the latest data from the SEMS API and updates the state."""
|
||||
try:
|
||||
# Get the status of our SEMS Power Station
|
||||
_LOGGER.debug("SEMS - Making Power Station Status API Call")
|
||||
if maxTokenRetries <= 0:
|
||||
_LOGGER.info(
|
||||
"SEMS - Maximum token fetch tries reached, aborting for now"
|
||||
)
|
||||
raise OutOfRetries
|
||||
if self._token is None or renewToken:
|
||||
_LOGGER.debug(
|
||||
"API token not set (%s) or new token requested (%s), fetching",
|
||||
self._token,
|
||||
renewToken,
|
||||
)
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
data = '{"powerStationId":"' + powerStationId + '"}'
|
||||
result = self._make_api_call(
|
||||
_PowerStationURLPart,
|
||||
data=data,
|
||||
renewToken=renewToken,
|
||||
maxTokenRetries=maxTokenRetries,
|
||||
operation_name="getData API call",
|
||||
)
|
||||
return result if isinstance(result, dict) else {}
|
||||
|
||||
# Prepare Power Station status Headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"token": json.dumps(self._token),
|
||||
}
|
||||
def _make_control_api_call(
|
||||
self,
|
||||
data: dict[str, Any],
|
||||
renewToken: bool = False,
|
||||
maxTokenRetries: int = 2,
|
||||
operation_name: str = "Control API call",
|
||||
) -> bool:
|
||||
"""Make a control API call with different response handling."""
|
||||
_LOGGER.debug("SEMS - Making %s", operation_name)
|
||||
if maxTokenRetries <= 0:
|
||||
_LOGGER.info("SEMS - Maximum token fetch tries reached, aborting for now")
|
||||
raise OutOfRetries
|
||||
|
||||
powerStationURL = self._token["api"] + _PowerStationURLPart
|
||||
if self._token is None or renewToken:
|
||||
_LOGGER.debug(
|
||||
"Querying SEMS API (%s) for power station id: %s",
|
||||
powerStationURL,
|
||||
powerStationId,
|
||||
"API token not set (%s) or new token requested (%s), fetching",
|
||||
self._token,
|
||||
renewToken,
|
||||
)
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
|
||||
data = '{"powerStationId":"' + powerStationId + '"}'
|
||||
if self._token is None:
|
||||
_LOGGER.error("Failed to obtain API token")
|
||||
return False
|
||||
|
||||
response = requests.post(
|
||||
powerStationURL, headers=headers, data=data, timeout=_RequestTimeout
|
||||
)
|
||||
jsonResponse = response.json()
|
||||
_LOGGER.debug("Response: %s", jsonResponse)
|
||||
# try again and renew token is unsuccessful
|
||||
if (
|
||||
jsonResponse["msg"] not in ["success", "操作成功"]
|
||||
or jsonResponse["data"] is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Query not successful (%s), retrying with new token, %s retries remaining",
|
||||
jsonResponse["msg"],
|
||||
maxTokenRetries,
|
||||
)
|
||||
return self.getData(
|
||||
powerStationId, True, maxTokenRetries=maxTokenRetries - 1
|
||||
)
|
||||
# Prepare headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"token": json.dumps(self._token),
|
||||
}
|
||||
|
||||
return jsonResponse["data"]
|
||||
except Exception as exception:
|
||||
_LOGGER.error("Unable to fetch data from SEMS. %s", exception)
|
||||
return {}
|
||||
api_url = self._token["api"] + _PowerControlURLPart
|
||||
|
||||
def change_status(self, inverterSn, status, renewToken=False, maxTokenRetries=2):
|
||||
"""Schedule the downtime of the station"""
|
||||
try:
|
||||
# Get the status of our SEMS Power Station
|
||||
_LOGGER.debug("SEMS - Making Power Station Status API Call")
|
||||
if maxTokenRetries <= 0:
|
||||
_LOGGER.info(
|
||||
"SEMS - Maximum token fetch tries reached, aborting for now"
|
||||
)
|
||||
raise OutOfRetries
|
||||
if self._token is None or renewToken:
|
||||
_LOGGER.debug(
|
||||
"API token not set (%s) or new token requested (%s), fetching",
|
||||
self._token,
|
||||
renewToken,
|
||||
)
|
||||
self._token = self.getLoginToken(self._username, self._password)
|
||||
|
||||
# Prepare Power Station status Headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"token": json.dumps(self._token),
|
||||
}
|
||||
|
||||
powerControlURL = self._token["api"] + _PowerControlURLPart
|
||||
# powerControlURL = _PowerControlURL
|
||||
_LOGGER.debug(
|
||||
"Sending power control command (%s) for power station id: %s",
|
||||
powerControlURL,
|
||||
inverterSn,
|
||||
# Control API uses different validation (HTTP status code), so don't validate JSON response code
|
||||
self._make_http_request(
|
||||
api_url,
|
||||
headers,
|
||||
json_data=data,
|
||||
operation_name=operation_name,
|
||||
validate_code=False,
|
||||
)
|
||||
|
||||
data = {
|
||||
"InverterSN": inverterSn,
|
||||
"InverterStatusSettingMark": "1",
|
||||
"InverterStatus": str(status),
|
||||
}
|
||||
# For control API, any successful HTTP response (status 200) means success
|
||||
# The _make_http_request already validated HTTP status via raise_for_status()
|
||||
return True
|
||||
|
||||
response = requests.post(
|
||||
powerControlURL, headers=headers, json=data, timeout=_RequestTimeout
|
||||
)
|
||||
if response.status_code != 200:
|
||||
# try again and renew token is unsuccessful
|
||||
except requests.HTTPError as e:
|
||||
if hasattr(e.response, "status_code") and e.response.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
"Power control command not successful, retrying with new token, %s retries remaining",
|
||||
"%s not successful, retrying with new token, %s retries remaining",
|
||||
operation_name,
|
||||
maxTokenRetries,
|
||||
)
|
||||
return self.change_status(
|
||||
inverterSn, status, True, maxTokenRetries=maxTokenRetries - 1
|
||||
return self._make_control_api_call(
|
||||
data, True, maxTokenRetries - 1, operation_name
|
||||
)
|
||||
_LOGGER.error("Unable to execute %s: %s", operation_name, e)
|
||||
return False
|
||||
except (requests.RequestException, ValueError, KeyError) as exception:
|
||||
_LOGGER.error("Unable to execute %s: %s", operation_name, exception)
|
||||
return False
|
||||
|
||||
return
|
||||
except Exception as exception:
|
||||
_LOGGER.error("Unable to execute Power control command. %s", exception)
|
||||
def change_status(
|
||||
self,
|
||||
inverterSn: str,
|
||||
status: str | int,
|
||||
renewToken: bool = False,
|
||||
maxTokenRetries: int = 2,
|
||||
) -> None:
|
||||
"""Schedule the downtime of the station."""
|
||||
data = {
|
||||
"InverterSN": inverterSn,
|
||||
"InverterStatusSettingMark": "1",
|
||||
"InverterStatus": str(status),
|
||||
}
|
||||
|
||||
success = self._make_control_api_call(
|
||||
data,
|
||||
renewToken=renewToken,
|
||||
maxTokenRetries=maxTokenRetries,
|
||||
operation_name=f"power control command for inverter {inverterSn}",
|
||||
)
|
||||
|
||||
if not success:
|
||||
_LOGGER.error("Power control command failed after all retries")
|
||||
|
||||
|
||||
class OutOfRetries(exceptions.HomeAssistantError):
|
||||
|
||||
Reference in New Issue
Block a user