Skip to content

Commit

Permalink
Add support for connecting to Aquarea Demo Enviroment (#41)
Browse files Browse the repository at this point in the history
* Refactor Aquarea service URLs and add AquareaEnvironment enum

* Refactor Client initialization and add validation for username and password

* Add AquareaEnvironment constant and update Client initialization

* Refactor login method in Client class

* Refactor login_demo method in Client class
  • Loading branch information
cjaliaga authored Jan 9, 2024
1 parent fe012af commit 48ad276
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 45 deletions.
3 changes: 3 additions & 0 deletions aioaquarea/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
)
from .statistics import Consumption, ConsumptionType, DateType

from .const import AquareaEnvironment

__all__: Tuple[str, ...] = (
"Client",
"Device",
Expand All @@ -55,4 +57,5 @@
"ForceHeater",
"HolidayTimer",
"PowerfulTime",
"AquareaEnvironment",
)
16 changes: 12 additions & 4 deletions aioaquarea/const.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
"""Constants definition"""

from enum import IntEnum


AQUAREA_SERVICE_BASE = "https://aquarea-smart.panasonic.com/"
AQUAREA_SERVICE_DEMO_BASE = "https://demo.aquarea-smart.panasonic.com/"
AQUAREA_SERVICE_LOGIN = "remote/v1/api/auth/login"
AQUAREA_SERVICE_DEVICES = "remote/v1/api/devices"
AQUAREA_SERVICE_CONSUMPTION = "remote/v1/api/consumption"
AQUAREA_SERVICE_CONTRACT = "remote/contract"

AQUAREA_SERVICE_A2W_STATUS_DISPLAY = (
"https://aquarea-smart.panasonic.com/remote/a2wStatusDisplay"
)
AQUAREA_SERVICE_A2W_STATUS_DISPLAY = "remote/a2wStatusDisplay"

PANASONIC = "Panasonic"


class AquareaEnvironment(IntEnum):
"""Aquarea environment"""

PRODUCTION = 0
DEMO = 1
139 changes: 98 additions & 41 deletions aioaquarea/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
AQUAREA_SERVICE_BASE,
AQUAREA_SERVICE_CONSUMPTION,
AQUAREA_SERVICE_CONTRACT,
AQUAREA_SERVICE_DEMO_BASE,
AQUAREA_SERVICE_DEVICES,
AQUAREA_SERVICE_LOGIN,
AquareaEnvironment,
)
from .data import (
Device,
Expand Down Expand Up @@ -98,11 +100,33 @@ class Client:
def __init__(
self,
session: aiohttp.ClientSession,
username: str,
password: str,
username: str | None = None,
password: str | None = None,
refresh_login: bool = True,
logger: Optional[logging.Logger] = None,
environment: AquareaEnvironment = AquareaEnvironment.PRODUCTION,
device_direct: bool = True,
):
"""
Initializes a new instance of the `Core` class.
Args:
session (aiohttp.ClientSession): The aiohttp client session.
username (str, optional): The username for authentication. Defaults to None.
password (str, optional): The password for authentication. Defaults to None.
refresh_login (bool, optional): Whether to refresh the login. Defaults to True.
logger (Optional[logging.Logger], optional): The logger instance. Defaults to None.
environment (AquareaEnvironment, optional): The environment to use. Defaults to AquareaEnvironment.PRODUCTION.
device_direct (bool, optional): Whether to use device direct mode. Defaults to True.
Raises:
ValueError: If the environment is set to PRODUCTION and username or password are not provided.
"""
if environment == AquareaEnvironment.PRODUCTION and (
not username or not password
):
raise ValueError("Username and password must be provided")

self._login_lock = asyncio.Lock()
self._sess = session
self._username = username
Expand All @@ -111,6 +135,15 @@ def __init__(
self._logger = logger or logging.getLogger("aioaquarea")
self._token_expiration: Optional[dt.datetime] = None
self._last_login: dt.datetime = dt.datetime.min
self._environment = environment
self._base_url = (
AQUAREA_SERVICE_BASE
if environment == AquareaEnvironment.PRODUCTION
else AQUAREA_SERVICE_DEMO_BASE
)
self._device_direct = (
device_direct if environment == AquareaEnvironment.PRODUCTION else False
)

@property
def username(self) -> str:
Expand Down Expand Up @@ -164,7 +197,7 @@ async def request(
headers["content-type"] = content_type
kwargs["headers"] = headers

resp = await self._sess.request(method, AQUAREA_SERVICE_BASE + url, **kwargs)
resp = await self._sess.request(method, self._base_url + url, **kwargs)

# Aquarea returns a 200 even if the request failed, we need to check the message property to see if it's an error
# Some errors just require to login again, so we raise a AuthenticationError in those known cases
Expand Down Expand Up @@ -205,36 +238,48 @@ async def login(self) -> None:
if self._last_login > intent:
return

params = {
"var.inputOmit": "false",
"var.loginId": self.username,
"var.password": self.password,
}

response: aiohttp.ClientResponse = await self.request(
"POST",
AQUAREA_SERVICE_LOGIN,
referer=AQUAREA_SERVICE_BASE,
data=urllib.parse.urlencode(params),
)
if self._environment is AquareaEnvironment.DEMO:
await self._login_demo()
else:
await self._login_production()

data = await response.json()
self._last_login = dt.datetime.now()

if not isinstance(data, dict):
raise InvalidData(data)
finally:
self._login_lock.release()

self._token_expiration = dt.datetime.strptime(
data["accessToken"]["expires"], "%Y-%m-%dT%H:%M:%S%z"
)
async def _login_demo(self) -> None:
_ = await self.request("GET", "", referer=self._base_url)
self._token_expiration = dt.datetime.astimezone(
dt.datetime.utcnow(), tz=dt.timezone.utc
) + dt.timedelta(days=1)

async def _login_production(self) -> None:
params = {
"var.inputOmit": "false",
"var.loginId": self.username,
"var.password": self.password,
}

self._logger.info(
f"Login successful for {self.username}. Access Token Expiration: {self._token_expiration}"
)
response: aiohttp.ClientResponse = await self.request(
"POST",
AQUAREA_SERVICE_LOGIN,
referer=self._base_url,
data=urllib.parse.urlencode(params),
)

self._last_login = dt.datetime.now()
data = await response.json()

finally:
self._login_lock.release()
if not isinstance(data, dict):
raise InvalidData(data)

self._token_expiration = dt.datetime.strptime(
data["accessToken"]["expires"], "%Y-%m-%dT%H:%M:%S%z"
)

self._logger.info(
f"Login successful for {self.username}. Access Token Expiration: {self._token_expiration}"
)

@auth_required
async def get_devices(self, include_long_id=False) -> list[DeviceInfo]:
Expand Down Expand Up @@ -286,19 +331,31 @@ async def get_devices(self, include_long_id=False) -> list[DeviceInfo]:
async def get_device_long_id(self, device_id: str) -> str:
"""Retrives device long id to be used to retrive device status."""
cookies = dict(selectedGwid=device_id)

if self._environment is AquareaEnvironment.DEMO:
return (
self._sess.cookie_jar.filter_cookies(self._base_url)
.get("selectedDeviceId")
.value
)

resp = await self.request(
"POST",
AQUAREA_SERVICE_CONTRACT,
referer=AQUAREA_SERVICE_BASE,
referer=self._base_url,
cookies=cookies,
)
return resp.cookies.get("selectedDeviceId").value

@auth_required
async def get_device_status(self, long_id: str) -> DeviceStatus:
"""Retrives device status."""
params = {"var.deviceDirect": "1"} if self._device_direct else {}
response = await self.request(
"GET", f"{AQUAREA_SERVICE_DEVICES}/{long_id}?var.deviceDirect=1"
"GET",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=self._base_url,
data=urllib.parse.urlencode(params),
)
data = await response.json()

Expand Down Expand Up @@ -395,7 +452,7 @@ async def post_device_operation_status(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_device_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand Down Expand Up @@ -423,7 +480,7 @@ async def post_device_tank_temperature(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_device_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand Down Expand Up @@ -453,7 +510,7 @@ async def post_device_tank_operation_status(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_device_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand Down Expand Up @@ -487,7 +544,7 @@ async def post_device_operation_update(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand Down Expand Up @@ -530,7 +587,7 @@ async def _post_device_zone_temperature(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -543,7 +600,7 @@ async def post_device_set_quiet_mode(self, long_id: str, mode: QuietMode) -> Non
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -556,7 +613,7 @@ async def post_device_force_dhw(self, long_id: str, force_dhw: ForceDHW) -> None
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -571,7 +628,7 @@ async def post_device_force_heater(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -588,7 +645,7 @@ async def post_device_holiday_timer(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -601,7 +658,7 @@ async def post_device_request_defrost(self, long_id: str) -> None:
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -623,7 +680,7 @@ async def post_device_set_powerful_time(
response = await self.request(
"POST",
f"{AQUAREA_SERVICE_DEVICES}/{long_id}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
content_type="application/json",
json=data,
)
Expand All @@ -635,7 +692,7 @@ async def get_device_consumption(
response = await self.request(
"GET",
f"{AQUAREA_SERVICE_CONSUMPTION}/{long_id}?{aggregation}={date_input}",
referer=AQUAREA_SERVICE_A2W_STATUS_DISPLAY,
referer=f"{self._base_url}{AQUAREA_SERVICE_A2W_STATUS_DISPLAY}",
)

date_data = await response.json()
Expand Down

0 comments on commit 48ad276

Please sign in to comment.