Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for connecting to Aquarea Demo Enviroment #41

Merged
merged 5 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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