Skip to content

Commit 6a6d5b9

Browse files
committed
added files
1 parent 01e9801 commit 6a6d5b9

File tree

4 files changed

+396
-0
lines changed

4 files changed

+396
-0
lines changed

custom_components/weback/weback_unofficial/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import requests
2+
import datetime
3+
import boto3
4+
import json
5+
from hashlib import md5
6+
7+
8+
class WebackApi(object):
9+
10+
# boto3 session object
11+
aws_session = None
12+
aws_identity_id = None
13+
14+
# Expiration time of session
15+
expiration_time = None
16+
17+
# Creds for WeBack API
18+
__api_login = None
19+
__api_password = None
20+
__api_country_code = None
21+
22+
def __init__(self, login: str = None, password: str = None, country_code: str = None, aws_session: boto3.Session = None, aws_session_expiration: int = None):
23+
self.aws_session = aws_session
24+
self.aws_session_expiration = None
25+
self.__api_login = login
26+
self.__api_password = password
27+
self.__api_country_code = country_code
28+
29+
def auth(self, login: str = None, password: str = None) -> dict:
30+
if login is None:
31+
if self.__api_login is None:
32+
raise Exception(
33+
"Login is not provided via params or class constructor")
34+
if self.__api_country_code is None:
35+
login = self.__api_login
36+
else:
37+
login = f"+{self.__api_country_code}-{self.__api_login}"
38+
if password is None:
39+
if self.__api_password is None:
40+
raise Exception(
41+
"Password is not provided via params or class constructor")
42+
password = self.__api_password
43+
44+
req = {
45+
"App_Version": "android_3.9.3",
46+
"Password": md5(password.encode('utf-8')).hexdigest(),
47+
"User_Account": login
48+
}
49+
r = requests.post(
50+
'https://www.weback-login.com/WeBack/WeBack_Login_Ats_V3', json=req)
51+
resp_content = r.json()
52+
return resp_content
53+
54+
def auth_cognito(self, region: str, identity_id: str, token: str) -> dict:
55+
cl = boto3.client('cognito-identity', region)
56+
aws_creds = cl.get_credentials_for_identity(
57+
IdentityId=identity_id,
58+
Logins={
59+
"cognito-identity.amazonaws.com": token
60+
}
61+
)
62+
return aws_creds
63+
64+
def make_session_from_cognito(self, aws_creds: dict, region: str) -> boto3.Session:
65+
session = boto3.Session(
66+
aws_access_key_id=aws_creds['Credentials']['AccessKeyId'],
67+
aws_secret_access_key=aws_creds['Credentials']['SecretKey'],
68+
aws_session_token=aws_creds['Credentials']['SessionToken'],
69+
region_name=region
70+
)
71+
return session
72+
73+
def device_list(self, session: boto3.Session = None, identity_id: str = None):
74+
if (session == None):
75+
session = self.get_session()
76+
identity_id = self.aws_identity_id
77+
78+
client = session.client('lambda')
79+
resp = client.invoke(
80+
FunctionName='Device_Manager_V2',
81+
InvocationType="RequestResponse",
82+
Payload= json.dumps({
83+
"Device_Manager_Request":"query",
84+
"Identity_Id": self.aws_identity_id,
85+
"Region_Info": session.region_name
86+
})
87+
)
88+
payload = json.loads(resp['Payload'].read())
89+
return payload['Request_Cotent']
90+
91+
def get_device_description(self, device_name, session = None):
92+
if (session == None):
93+
session = self.get_session()
94+
client = session.client('iot')
95+
resp = client.describe_thing(thingName=device_name)
96+
return resp
97+
98+
def get_endpoint(self, session):
99+
iot_client = session.client('iot')
100+
return "https://" + iot_client.describe_endpoint(endpointType="iot:Data-ATS").get("endpointAddress")
101+
102+
def get_device_shadow(self, device_name, session = None, return_full = False):
103+
if (session == None):
104+
session = self.get_session()
105+
client = session.client('iot-data', endpoint_url=self.get_endpoint(session))
106+
resp = client.get_thing_shadow(thingName=device_name)
107+
shadow = json.loads(resp['payload'].read())
108+
if return_full:
109+
return shadow
110+
return shadow['state']['reported']
111+
112+
def publish_device_msg(self, device_name, desired_payload = {}, session = None):
113+
if (session == None):
114+
session = self.get_session()
115+
client = session.client('iot-data', endpoint_url=self.get_endpoint(session))
116+
topic = f"$aws/things/{device_name}/shadow/update"
117+
payload = {
118+
'state': {
119+
'desired': desired_payload
120+
}
121+
}
122+
resp = client.publish(topic=topic, qos = 0, payload = json.dumps(payload))
123+
return resp
124+
125+
def is_renewal_required(self):
126+
return True if (self.expiration_time < datetime.datetime.now(self.expiration_time.tzinfo)) else False
127+
128+
def get_session(self) -> boto3.Session:
129+
if self.aws_session and not self.is_renewal_required():
130+
return self.aws_session
131+
132+
if not self.__api_login or not self.__api_password:
133+
raise Exception("You should provide login and password via constructor to use session management")
134+
135+
weback_data = self.auth()
136+
if weback_data['Request_Result'] != 'success':
137+
raise Exception(f"Could not authenticate. {weback_data['Fail_Reason']}")
138+
139+
region = weback_data['Region_Info']
140+
self.aws_identity_id = weback_data['Identity_Id']
141+
142+
aws_creds = self.auth_cognito(
143+
region, weback_data['Identity_Id'], weback_data['Token'])
144+
self.expiration_time = aws_creds['Credentials'].get('Expiration')
145+
146+
sess = self.make_session_from_cognito(aws_creds, region)
147+
self.aws_session = sess
148+
return sess
149+
150+
class BaseDevice(object):
151+
client: WebackApi = None
152+
name: str = None
153+
shadow: list = {}
154+
_description: dict = None
155+
nickname: str = None
156+
157+
def __init__(self, name: str, client: WebackApi, shadow: list = None, description = None, nickname = None):
158+
super().__init__()
159+
self.client = client
160+
self.name = name
161+
self.description = description
162+
self.nickname = nickname if nickname is not None else self.name
163+
164+
if (shadow):
165+
self.shadow = shadow
166+
167+
def update(self):
168+
"""Update device state"""
169+
shadow = self.client.get_device_shadow(self.name)
170+
self.shadow = shadow
171+
return self
172+
173+
def description(self):
174+
"""Get Amazon IoT device description"""
175+
if (self._description is not None):
176+
return self.description
177+
description = self.client.get_device_description(self.name)
178+
self._description = description
179+
return description
180+
181+
def publish(self, desired_payload):
182+
"""Publish 'desired' payload via MQTT"""
183+
resp = self.client.publish_device_msg(self.name, desired_payload)
184+
return resp
185+
186+
def publish_single(self, attribute, value):
187+
"""Publish single attribute via MQTT."""
188+
return self.publish({attribute: value})
189+
190+
def raise_invalid_value(self, valid):
191+
"""Helper: prevent publish unsupported values."""
192+
raise Exception("Only this set of values supported: %s" % ", ".join(valid))
193+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from weback_unofficial.client import WebackApi, BaseDevice
2+
3+
# uses a weekly plan (Mon, Tues, Wed, Thur, Fri, Sat, Sun)
4+
AUTO = 'auto'
5+
# uses the temperature you set in the set_temp property
6+
MANUAL = 'hand'
7+
8+
WORKING_MODES = {AUTO, MANUAL}
9+
# temperature is always times two (e.g. 25 degrees = 50 degrees in shadow object)
10+
TEMP_MULTIPLIER = 2
11+
12+
# temperature is given like 250, which means 25.0 degrees
13+
TEMP_DISPLAY_DIVIDER = 10
14+
15+
CURRENT_TEMPERATURE = 'air_tem'
16+
GOAL_TEMPERATURE = 'set_tem'
17+
CONNECTED = 'connected'
18+
WORKMODE = 'workmode'
19+
WORKING_STATUS = 'working_status'
20+
21+
22+
class Thermostat(BaseDevice):
23+
def __init__(self, name, client, shadow=None, description=None, nickname=None):
24+
super().__init__(name, client, shadow=shadow, description=description, nickname=nickname)
25+
26+
def setMode(self, mode: str):
27+
if not mode in WORKING_MODES:
28+
self.raise_invalid_value(WORKING_MODES)
29+
self.publish_single('workmode', mode)
30+
31+
def setTemp(self, temp: str):
32+
self.setMode(MANUAL)
33+
self.publish_single('set_tem', temp * TEMP_MULTIPLIER)
34+
35+
36+
@property
37+
def is_available(self):
38+
return True if self.shadow.get(CONNECTED) == 'false' else True
39+
40+
@property
41+
def mode(self):
42+
return self.shadow.get(WORKMODE)
43+
44+
@property
45+
def temperature(self):
46+
return self.shadow.get(CURRENT_TEMPERATURE) / TEMP_DISPLAY_DIVIDER
47+
48+
@property
49+
def goal_temperature(self):
50+
return self.shadow.get(GOAL_TEMPERATURE) / TEMP_MULTIPLIER
51+
52+
@property
53+
def is_heating(self):
54+
return True if self.shadow.get(WORKING_STATUS) == 'on' else False
55+
56+
@property
57+
def autosettings(self):
58+
return {
59+
"Mon": self.format_auto_settings('Mon'),
60+
"Tue": self.format_auto_settings('Tues'),
61+
"Wed": self.format_auto_settings('Wed'),
62+
"Thu": self.format_auto_settings('Thur'),
63+
"Fri": self.format_auto_settings('Fri'),
64+
"Sat": self.format_auto_settings('Sat'),
65+
"Sun": self.format_auto_settings('Sun')
66+
}
67+
68+
# the shadow object includes a string for every day where the actions are concatenated
69+
# this function is used to split them up
70+
# input: "04:50_043C,08:00_030C,17:00_043C,20:00_030C,20:00_030C,20:00_030C"
71+
# output: [
72+
# "04:50_043C",
73+
# "08:00_030C",
74+
# "17:00_043C",
75+
# "20:00_030C",
76+
# "20:00_030C",
77+
# "20:00_030C"
78+
# ]
79+
def format_auto_settings(self, day):
80+
command_string = self.shadow.get(day)
81+
commands = command_string.split(',')
82+
return commands
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from weback_unofficial.client import WebackApi, BaseDevice
2+
3+
CLEAN_MODE_AUTO = 'AutoClean'
4+
CLEAN_MODE_EDGE = 'EdgeClean'
5+
CLEAN_MODE_SPOT = 'SpotClean'
6+
CLEAN_MODE_SINGLE_ROOM = 'RoomClean'
7+
CLEAN_MODE_MOP = 'MopClean'
8+
CLEAN_MODE_STOP = 'Standby'
9+
CLEAN_MODE_SMART = 'SmartClean'
10+
11+
FAN_DISABLED = 'Pause'
12+
FAN_SPEED_QUIET = 'Quiet'
13+
FAN_SPEED_NORMAL = 'Normal'
14+
FAN_SPEED_HIGH = 'Strong'
15+
FAN_SPEEDS = {FAN_SPEED_QUIET, FAN_SPEED_NORMAL, FAN_SPEED_HIGH}
16+
17+
CHARGE_MODE_RETURNING = 'BackCharging'
18+
CHARGE_MODE_CHARGING = 'Charging'
19+
CHARGE_MODE_DOCK_CHARGING = 'PileCharging'
20+
CHARGE_MODE_DIRECT_CHARGING = 'DirCharging'
21+
CHARGE_MODE_IDLE = 'Hibernating'
22+
23+
MOP_DISABLED = 'None'
24+
MOP_SPEED_LOW = 'Low'
25+
MOP_SPEED_NORMAL = 'Default'
26+
MOP_SPEED_HIGH = 'High'
27+
MOP_SPEEDS = {MOP_SPEED_LOW, MOP_SPEED_NORMAL, MOP_SPEED_HIGH}
28+
29+
ROBOT_ERROR = "Malfunction"
30+
31+
CLEANING_STATES = {CLEAN_MODE_AUTO, CLEAN_MODE_EDGE, CLEAN_MODE_SPOT, CLEAN_MODE_SINGLE_ROOM, CLEAN_MODE_MOP, CLEAN_MODE_SMART}
32+
CHARGING_STATES = {CHARGE_MODE_CHARGING, CHARGE_MODE_DOCK_CHARGING, CHARGE_MODE_DIRECT_CHARGING}
33+
DOCKED_STATES = {CHARGE_MODE_IDLE, CHARGE_MODE_CHARGING, CHARGE_MODE_DOCK_CHARGING, CHARGE_MODE_DIRECT_CHARGING}
34+
35+
class CleanRobot(BaseDevice):
36+
def __init__(self, name, client, shadow=None, description=None, nickname=None):
37+
super().__init__(name, client, shadow=shadow, description=description, nickname=nickname)
38+
39+
def turn_on(self):
40+
return self.publish_single('working_status', CLEAN_MODE_AUTO)
41+
42+
def turn_off(self):
43+
return self.publish_single('working_status', CHARGE_MODE_RETURNING)
44+
45+
def return_home(self):
46+
return self.turn_off()
47+
48+
def stop(self):
49+
return self.publish_single('working_status', CLEAN_MODE_STOP)
50+
51+
def setFan(self, mode: str):
52+
if not mode in FAN_SPEEDS:
53+
self.raise_invalid_value(FAN_SPEEDS)
54+
55+
self.publish_single('fan_status', mode)
56+
57+
def setMop(self, mode: str):
58+
if not mode in MOP_SPEEDS:
59+
self.raise_invalid_value(MOP_SPEEDS)
60+
self.publish_single('water_level', mode)
61+
62+
@property
63+
def is_available(self):
64+
return True if self.shadow.get('connected') == 'false' else True
65+
66+
@property
67+
def clean_tine(self) -> int:
68+
return self.shadow.get('clean_time')
69+
70+
@property
71+
def battery_level(self) -> int:
72+
return self.shadow.get('battery_level')
73+
74+
@property
75+
def current_mode(self) -> str:
76+
return self.shadow.get('working_status')
77+
78+
@property
79+
def error(self) -> str:
80+
return self.shadow.get('error_info')
81+
82+
@property
83+
def is_cleaning(self) -> bool:
84+
return True if self.current_mode in CLEANING_STATES else False
85+
86+
@property
87+
def is_docked(self) -> bool:
88+
return True if self.current_mode in DOCKED_STATES else False
89+
90+
@property
91+
def is_paused(self) -> bool:
92+
return True if self.current_mode == CLEAN_MODE_STOP else False
93+
94+
@property
95+
def is_idle(self) -> bool:
96+
return not (self.is_docked or self.is_cleaning or self.is_paused \
97+
or self.is_returning or self.is_error)
98+
99+
@property
100+
def is_returning(self) -> bool:
101+
return True if self.current_mode == CHARGE_MODE_RETURNING else False
102+
103+
@property
104+
def is_error(self) -> bool:
105+
return True if self.current_mode == ROBOT_ERROR else False
106+
107+
@property
108+
def state(self):
109+
if self.current_mode == None:
110+
return 'unknown'
111+
if self.is_error:
112+
return 'error'
113+
if self.is_cleaning:
114+
return 'cleaning'
115+
if self.is_docked:
116+
return 'docked'
117+
if self.is_returning:
118+
return 'returning'
119+
if self.is_paused:
120+
return 'paused'
121+
return 'idle'

0 commit comments

Comments
 (0)