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 ADDED
@@ -0,0 +1,5 @@
1
+ from sensio_lib.light import Light
2
+ from sensio_lib.hub import Hub
3
+ from sensio_lib.light_state import LightState
4
+
5
+ __all__ = ['Light', 'Hub', 'LightState']
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,6 @@
1
+ from enum import Enum
2
+
3
+ class LightState(Enum):
4
+ ON = "on"
5
+ OFF = "off"
6
+ UNKNOWN = "unknown"
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any