sensio-lib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sensio_lib/__init__.py +5 -0
- sensio_lib/const.py +18 -0
- sensio_lib/hub.py +85 -0
- sensio_lib/light.py +32 -0
- sensio_lib/light_state.py +6 -0
- sensio_lib/socket_manager.py +25 -0
- sensio_lib-0.1.0.dist-info/METADATA +25 -0
- sensio_lib-0.1.0.dist-info/RECORD +9 -0
- sensio_lib-0.1.0.dist-info/WHEEL +4 -0
sensio_lib/__init__.py
ADDED
sensio_lib/const.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
SENSIO_AGENT_REQUEST = {
|
|
3
|
+
"DeviceName": "sdk_gphone_x86",
|
|
4
|
+
"DeviceId": "97e2cd6e-002d-4cfe-a596-5eb7a34c7582",
|
|
5
|
+
"DeviceOsVersion": "11 (sdk_gphone_x86-userdebug 11 RSR1.210722.013.A2 10067904 dev-keys)",
|
|
6
|
+
"DeviceType": "Android",
|
|
7
|
+
"ClientType": "SensioHaPanelApp",
|
|
8
|
+
"ClientVersion": "6.12.2 (320)"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# URLS
|
|
12
|
+
SENSIO_BASE_URL = 'https://unity.sensio.no/api/v1'
|
|
13
|
+
SENSIO_TOKEN_URL = 'https://unity.sensio.no/api/v1/tokens'
|
|
14
|
+
SENSIO_PROJECTS_URL = 'https://unity.sensio.no/api/v1/users/self/projects?include=controllers&installerProjectsOnly=false&siteIdentifier=self'
|
|
15
|
+
|
|
16
|
+
# Hex
|
|
17
|
+
COMMAND_PREFIX = '\x01'
|
|
18
|
+
COMMAND_POSTFIX = '\x02'
|
sensio_lib/hub.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from sensio_lib.const import SENSIO_AGENT_REQUEST, SENSIO_TOKEN_URL, SENSIO_PROJECTS_URL, SENSIO_BASE_URL
|
|
2
|
+
from sensio_lib.light import Light
|
|
3
|
+
import requests
|
|
4
|
+
import logging
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class Hub:
|
|
10
|
+
def __init__(self, server_address: str, username: str, password: str):
|
|
11
|
+
self.username = username
|
|
12
|
+
self.password = password
|
|
13
|
+
self.server_address = server_address
|
|
14
|
+
|
|
15
|
+
def login(self) -> list:
|
|
16
|
+
''' Authenticate with the Sensio API and retrieve a list of projects '''
|
|
17
|
+
|
|
18
|
+
self.token = self._get_auth_token()
|
|
19
|
+
return self._get_projects()
|
|
20
|
+
|
|
21
|
+
def get_lights(self, project_id) -> list:
|
|
22
|
+
''' Retrieve a list of lights for the specified project '''
|
|
23
|
+
|
|
24
|
+
entities_url = f'{SENSIO_BASE_URL}/projects/{project_id}/functions'
|
|
25
|
+
entities_response = requests.get(entities_url, headers={'Authorization': f'Token {self.token}'})
|
|
26
|
+
if entities_response.status_code != 200:
|
|
27
|
+
raise Exception("Failed to retrieve entities")
|
|
28
|
+
|
|
29
|
+
logger.debug(entities_response.json())
|
|
30
|
+
|
|
31
|
+
switches = self._create_function_list(entities_response.json())
|
|
32
|
+
return [Light(switch['switch']['name'], switch['switch']['command_on'], switch['switch']['command_off'], self.server_address) for switch in switches]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_auth_token(self):
|
|
36
|
+
response = requests.post(SENSIO_TOKEN_URL, auth=(self.username, self.password), json=SENSIO_AGENT_REQUEST, headers={"Content-Type": "application/json"})
|
|
37
|
+
if response.status_code != 200:
|
|
38
|
+
raise Exception("Failed to retrieve token")
|
|
39
|
+
|
|
40
|
+
token_str = f'{response.json()["tokenId"]}:{response.json()["tokenSecret"]}'
|
|
41
|
+
token_bytes = token_str.encode('utf-8')
|
|
42
|
+
return base64.b64encode(token_bytes).decode('utf-8')
|
|
43
|
+
|
|
44
|
+
def _get_projects(self):
|
|
45
|
+
if not self.token:
|
|
46
|
+
raise Exception("Token not set")
|
|
47
|
+
|
|
48
|
+
# Get a list of projects
|
|
49
|
+
project_response = requests.get(SENSIO_PROJECTS_URL, headers={'Authorization': f'Token {self.token}'})
|
|
50
|
+
if project_response.status_code != 200:
|
|
51
|
+
raise Exception("Failed to retrieve projects")
|
|
52
|
+
|
|
53
|
+
# We want to populate projects with the project ID and project name for each project
|
|
54
|
+
return {project['name']: project['projectId'] for project in project_response.json()['projects']}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _create_function_list(self, entity_json) -> list:
|
|
58
|
+
''' Create a list of functions from the entity data. This is.....ugly.'''
|
|
59
|
+
|
|
60
|
+
switch_info = []
|
|
61
|
+
intermediate_switches = {}
|
|
62
|
+
|
|
63
|
+
for switch in entity_json['functions']:
|
|
64
|
+
name = switch['name']
|
|
65
|
+
sub_type = switch['subType']
|
|
66
|
+
# Strip postfix to get the base name
|
|
67
|
+
base_name = name.rsplit('_', 1)[0]
|
|
68
|
+
# Determine command based on postfix
|
|
69
|
+
if sub_type in ['light_on', 'light_off', 'LightOffRoom', 'LightOnRoom']:
|
|
70
|
+
if base_name not in intermediate_switches:
|
|
71
|
+
intermediate_switches[base_name] = {
|
|
72
|
+
'name': base_name,
|
|
73
|
+
'command_on': '',
|
|
74
|
+
'command_off': ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if name.endswith('_OFF'):
|
|
78
|
+
intermediate_switches[base_name]['command_off'] = switch["address"]
|
|
79
|
+
elif name.endswith('_ON'):
|
|
80
|
+
intermediate_switches[base_name]['command_on'] = switch["address"]
|
|
81
|
+
|
|
82
|
+
if intermediate_switches[base_name]['command_on'] and intermediate_switches[base_name]['command_off']:
|
|
83
|
+
switch_info.append({'switch': intermediate_switches.pop(base_name)})
|
|
84
|
+
|
|
85
|
+
return switch_info
|
sensio_lib/light.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from sensio_lib.light_state import LightState
|
|
2
|
+
from sensio_lib.socket_manager import SocketManager
|
|
3
|
+
import uuid
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class Light:
|
|
9
|
+
def __init__(self, name, on_address, off_address, server_address=None):
|
|
10
|
+
self.state = LightState.UNKNOWN
|
|
11
|
+
self.id = uuid.uuid4()
|
|
12
|
+
self.name = name
|
|
13
|
+
self.on_address = on_address
|
|
14
|
+
self.off_address = off_address
|
|
15
|
+
self.server_address = server_address
|
|
16
|
+
self.socket_manager = SocketManager(self.server_address)
|
|
17
|
+
|
|
18
|
+
def turn_on(self):
|
|
19
|
+
logger.debug(f"Turning on light with ID: {self.name}")
|
|
20
|
+
self.socket_manager.send_command(f'new_state {self.on_address} 0')
|
|
21
|
+
self.state = LightState.ON
|
|
22
|
+
|
|
23
|
+
def turn_off(self):
|
|
24
|
+
logger.debug(f"Turning off light with ID: {self.name}")
|
|
25
|
+
self.socket_manager.send_command(f'new_state {self.off_address} 0')
|
|
26
|
+
self.state = LightState.OFF
|
|
27
|
+
|
|
28
|
+
def update(self):
|
|
29
|
+
logger.debug('Not implemented')
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f'LightEntity: {self.name} - {self.state}'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
from sensio_lib.const import COMMAND_PREFIX, COMMAND_POSTFIX
|
|
3
|
+
|
|
4
|
+
class SocketManager:
|
|
5
|
+
_instance = None
|
|
6
|
+
|
|
7
|
+
def __new__(cls, server_address):
|
|
8
|
+
if cls._instance is None:
|
|
9
|
+
cls._instance = super(SocketManager, cls).__new__(cls)
|
|
10
|
+
cls._instance.server_address = server_address
|
|
11
|
+
cls._instance.server_port = 10023
|
|
12
|
+
cls._instance.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
13
|
+
cls._instance.socket.settimeout(15)
|
|
14
|
+
cls._instance.socket.connect((server_address, 10023))
|
|
15
|
+
return cls._instance
|
|
16
|
+
|
|
17
|
+
def send_command(self, command):
|
|
18
|
+
command = f'{COMMAND_PREFIX}{command}{COMMAND_POSTFIX}'
|
|
19
|
+
self.socket.sendall(command.encode('utf-8'))
|
|
20
|
+
#response = self.socket.recv(1024)
|
|
21
|
+
#return response.decode('utf-8')
|
|
22
|
+
|
|
23
|
+
def close(self):
|
|
24
|
+
self.socket.close()
|
|
25
|
+
SocketManager._instance = None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sensio-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: ~=3.10
|
|
5
|
+
Requires-Dist: pytest<9,>=8.3.3
|
|
6
|
+
Requires-Dist: requests<3,>=2.32.3
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# Sensio Library
|
|
10
|
+
|
|
11
|
+
A small library designed to control the Sensio smart house controller. This library was developed by reverse engineering the communication between the Sensio smart home app and the controller. It retrieves all device information from the Sensio cloud, but after the initial setup, all devices can be controlled 100% locally from your LAN.
|
|
12
|
+
|
|
13
|
+
More features could probably be implemented by additional reverse engineering.
|
|
14
|
+
|
|
15
|
+
## Features (v0.1.0)
|
|
16
|
+
|
|
17
|
+
- **Login to Sensio Cloud**
|
|
18
|
+
- Select between multiple projects
|
|
19
|
+
- Get a list of all light devices
|
|
20
|
+
- **Basic Light Control**
|
|
21
|
+
- Turn lights on/off (Note: no actual state of lights is implemented)
|
|
22
|
+
|
|
23
|
+
## Device Support
|
|
24
|
+
|
|
25
|
+
This library has been tested with a Sensio X1 controller. Testing is limited, so your experience may vary.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
sensio_lib/__init__.py,sha256=97fSUXIBY8r-hvLWOyaOOsSjfapwVuZv-chAOFxMs24,153
|
|
2
|
+
sensio_lib/const.py,sha256=uFQ4DYaA3GTY61mDWBJvwlY1NZtMoXs3F0DXjY6HSnY,623
|
|
3
|
+
sensio_lib/hub.py,sha256=TLMqJmz1on-T6Fz86POrAbAdOj1xRuZ2iZiUPDRcD1U,3746
|
|
4
|
+
sensio_lib/light.py,sha256=yoHHGerpZefMpeN4UMdI-wyM8dB19FG8TRYohztD6SQ,1078
|
|
5
|
+
sensio_lib/light_state.py,sha256=ENMnAlSAzvUSxywYsSJgME3ViBp1Er398NGYRiVWPeQ,100
|
|
6
|
+
sensio_lib/socket_manager.py,sha256=8FflL6fTV_d4n7iRA6AaPJf3qzbBoHIYrRzF34Q2H5A,917
|
|
7
|
+
sensio_lib-0.1.0.dist-info/METADATA,sha256=hTnfr7iJ6CpOS6EgoNKFl6RhMou_euEQskd43EEUbpw,971
|
|
8
|
+
sensio_lib-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
sensio_lib-0.1.0.dist-info/RECORD,,
|