sensio-lib 0.1.0__tar.gz
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-0.1.0/.gitignore +125 -0
- sensio_lib-0.1.0/PKG-INFO +25 -0
- sensio_lib-0.1.0/README.md +17 -0
- sensio_lib-0.1.0/example.py +57 -0
- sensio_lib-0.1.0/pyproject.toml +14 -0
- sensio_lib-0.1.0/sensio_lib/__init__.py +5 -0
- sensio_lib-0.1.0/sensio_lib/const.py +18 -0
- sensio_lib-0.1.0/sensio_lib/hub.py +85 -0
- sensio_lib-0.1.0/sensio_lib/light.py +32 -0
- sensio_lib-0.1.0/sensio_lib/light_state.py +6 -0
- sensio_lib-0.1.0/sensio_lib/socket_manager.py +25 -0
- sensio_lib-0.1.0/tests/__init__.py +0 -0
- sensio_lib-0.1.0/tests/unit/test_hub.py +70 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
env/
|
|
12
|
+
venv/
|
|
13
|
+
ENV/
|
|
14
|
+
env.bak/
|
|
15
|
+
venv.bak/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.installed.cfg
|
|
18
|
+
*.egg
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
*.whl
|
|
22
|
+
|
|
23
|
+
# Poetry / UV
|
|
24
|
+
poetry.lock
|
|
25
|
+
uv.lock
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
# Usually these files are written by a python script from a template
|
|
29
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
cover/
|
|
51
|
+
|
|
52
|
+
# Translations
|
|
53
|
+
*.mo
|
|
54
|
+
*.pot
|
|
55
|
+
|
|
56
|
+
# Django stuff:
|
|
57
|
+
*.log
|
|
58
|
+
local_settings.py
|
|
59
|
+
db.sqlite3
|
|
60
|
+
db.sqlite3-journal
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
target/
|
|
74
|
+
|
|
75
|
+
# Jupyter Notebook
|
|
76
|
+
.ipynb_checkpoints
|
|
77
|
+
|
|
78
|
+
# IPython
|
|
79
|
+
profile_default/
|
|
80
|
+
ipython_config.py
|
|
81
|
+
|
|
82
|
+
# pyenv
|
|
83
|
+
.python-version
|
|
84
|
+
|
|
85
|
+
# celery beat schedule file
|
|
86
|
+
celerybeat-schedule
|
|
87
|
+
|
|
88
|
+
# SageMath parsed files
|
|
89
|
+
*.sage.py
|
|
90
|
+
|
|
91
|
+
# Environments
|
|
92
|
+
.env
|
|
93
|
+
.env.*
|
|
94
|
+
.venv
|
|
95
|
+
.venv.*
|
|
96
|
+
|
|
97
|
+
# Spyder project settings
|
|
98
|
+
.spyderproject
|
|
99
|
+
.spyderworkspace
|
|
100
|
+
|
|
101
|
+
# Rope project settings
|
|
102
|
+
.ropeproject
|
|
103
|
+
|
|
104
|
+
# mkdocs documentation
|
|
105
|
+
/site
|
|
106
|
+
|
|
107
|
+
# mypy
|
|
108
|
+
.mypy_cache/
|
|
109
|
+
.dmypy.json
|
|
110
|
+
dmypy.json
|
|
111
|
+
|
|
112
|
+
# Pyre type checker
|
|
113
|
+
.pyre/
|
|
114
|
+
|
|
115
|
+
# pytype static type analyzer
|
|
116
|
+
.pytype/
|
|
117
|
+
|
|
118
|
+
# Cython debug symbols
|
|
119
|
+
cython_debug/
|
|
120
|
+
|
|
121
|
+
# Tests
|
|
122
|
+
tests/integration/
|
|
123
|
+
|
|
124
|
+
# Ignore vscode files
|
|
125
|
+
.vscode/
|
|
@@ -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,17 @@
|
|
|
1
|
+
# Sensio Library
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
More features could probably be implemented by additional reverse engineering.
|
|
6
|
+
|
|
7
|
+
## Features (v0.1.0)
|
|
8
|
+
|
|
9
|
+
- **Login to Sensio Cloud**
|
|
10
|
+
- Select between multiple projects
|
|
11
|
+
- Get a list of all light devices
|
|
12
|
+
- **Basic Light Control**
|
|
13
|
+
- Turn lights on/off (Note: no actual state of lights is implemented)
|
|
14
|
+
|
|
15
|
+
## Device Support
|
|
16
|
+
|
|
17
|
+
This library has been tested with a Sensio X1 controller. Testing is limited, so your experience may vary.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from sensio_lib import Light
|
|
2
|
+
from sensio_lib.light_state import LightState
|
|
3
|
+
from sensio_lib.hub import Hub
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
# Configure the root logger
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
level=logging.INFO,
|
|
9
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
10
|
+
handlers=[logging.StreamHandler()]
|
|
11
|
+
)
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
logger.info("Starting Sensio integration")
|
|
16
|
+
|
|
17
|
+
# Configure the system. Replace the IP address with the IP address of your Sensio hub on your LAN.
|
|
18
|
+
# Replace SENSIO_USERNAME and SENSIO_PASSWORD with your Sensio credentials (same username and password you use to log in to the Sensio app)
|
|
19
|
+
hub_adress = '192.168.xx.xx'
|
|
20
|
+
username = 'SENSIO_USERNAME'
|
|
21
|
+
password = 'SENSIO_PASSWORD'
|
|
22
|
+
hub = Hub(hub_adress, username, password)
|
|
23
|
+
|
|
24
|
+
# Login to Sensio cloud
|
|
25
|
+
hub.login()
|
|
26
|
+
|
|
27
|
+
# Retrieve all projects
|
|
28
|
+
if not hub.projects or len(hub.projects) == 0:
|
|
29
|
+
logger.error("No projects found")
|
|
30
|
+
return
|
|
31
|
+
else:
|
|
32
|
+
logger.info(hub.projects)
|
|
33
|
+
|
|
34
|
+
# Set the first project as the active project. Note: If you have multiple projects, you can select the project you want to control instead by using hub.projects['my_project_name'].
|
|
35
|
+
# All available projects should be listed by the log command above when the script is run
|
|
36
|
+
project = hub.projects.popitem()[1]
|
|
37
|
+
|
|
38
|
+
# By setting the project in the hub the hub will add all lights to the hub.lights list
|
|
39
|
+
hub.set_project(project)
|
|
40
|
+
|
|
41
|
+
[logger.info(f'Light {i}: {light}') for i, light in enumerate(hub.lights)]
|
|
42
|
+
while True:
|
|
43
|
+
light = input("Enter light number: ")
|
|
44
|
+
if light.isdigit():
|
|
45
|
+
light = int(light)
|
|
46
|
+
if light < len(hub.lights):
|
|
47
|
+
if hub.lights[light].get_light_state() == LightState.ON or hub.lights[light].get_light_state() == LightState.UNKNOWN:
|
|
48
|
+
logger.info(f'Light state is {hub.lights[light].get_light_state()}, turning off')
|
|
49
|
+
hub.lights[light].turn_off()
|
|
50
|
+
else:
|
|
51
|
+
logger.info(f'Light state is {hub.lights[light].get_light_state()}, turning on')
|
|
52
|
+
hub.lights[light].turn_on()
|
|
53
|
+
else:
|
|
54
|
+
logger.error('Invalid light number to toggle')
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sensio-lib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = ""
|
|
9
|
+
requires-python = "~=3.10"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"requests>=2.32.3,<3",
|
|
13
|
+
"pytest>=8.3.3,<9",
|
|
14
|
+
]
|
|
@@ -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'
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import requests
|
|
3
|
+
from unittest.mock import patch, Mock
|
|
4
|
+
from sensio_lib.hub import Hub
|
|
5
|
+
|
|
6
|
+
# Mock constants
|
|
7
|
+
SENSIO_PROJECTS_URL = "https://example.com/projects"
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def hub():
|
|
11
|
+
with patch('sensio_lib.hub.SENSIO_PROJECTS_URL', SENSIO_PROJECTS_URL):
|
|
12
|
+
with patch.object(Hub, '_get_auth_token', return_value="mocked_token"):
|
|
13
|
+
with patch.object(Hub, '_get_projects', return_value={}):
|
|
14
|
+
return Hub("1.1.1.1", "username", "password")
|
|
15
|
+
|
|
16
|
+
def test_get_projects_success(hub):
|
|
17
|
+
mock_response = Mock()
|
|
18
|
+
mock_response.status_code = 200
|
|
19
|
+
mock_response.json.return_value = {
|
|
20
|
+
'projects': [
|
|
21
|
+
{'name': 'Project1', 'projectId': '123'},
|
|
22
|
+
{'name': 'Project2', 'projectId': '456'}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
with patch('requests.get', return_value=mock_response):
|
|
27
|
+
projects = hub._get_projects()
|
|
28
|
+
assert projects == {'Project1': '123', 'Project2': '456'}
|
|
29
|
+
|
|
30
|
+
def test_get_auth_token_success(hub):
|
|
31
|
+
mock_response = Mock()
|
|
32
|
+
mock_response.status_code = 200
|
|
33
|
+
mock_response.json.return_value = {
|
|
34
|
+
'tokenId': 'mocked_token_id',
|
|
35
|
+
'tokenSecret': 'mocked_token_secret'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
with patch('requests.post', return_value=mock_response):
|
|
39
|
+
token = hub._get_auth_token()
|
|
40
|
+
assert token == "bW9ja2VkX3Rva2VuX2lkOm1vY2tlZF90b2tlbl9zZWNyZXQ="
|
|
41
|
+
|
|
42
|
+
def test_get_projects_no_token(hub):
|
|
43
|
+
hub.token = None
|
|
44
|
+
with pytest.raises(Exception, match="Token not set"):
|
|
45
|
+
hub._get_projects()
|
|
46
|
+
|
|
47
|
+
def test_get_projects_failure(hub):
|
|
48
|
+
mock_response = Mock()
|
|
49
|
+
mock_response.status_code = 500
|
|
50
|
+
|
|
51
|
+
with patch('requests.get', return_value=mock_response):
|
|
52
|
+
with pytest.raises(Exception, match="Failed to retrieve projects"):
|
|
53
|
+
hub._get_projects()
|
|
54
|
+
|
|
55
|
+
def test_create_function_list(hub):
|
|
56
|
+
mock_entity_json = {
|
|
57
|
+
'functions': [
|
|
58
|
+
{'name': 'light_1_OFF', 'subType': 'light_off', 'address': '0'},
|
|
59
|
+
{'name': 'light_1_ON', 'subType': 'light_on', 'address': '1'},
|
|
60
|
+
{'name': 'light_2_OFF', 'subType': 'light_off', 'address': '2'},
|
|
61
|
+
{'name': 'light_2_ON', 'subType': 'light_on', 'address': '3'},
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
light_list = hub._create_function_list(mock_entity_json)
|
|
66
|
+
assert light_list == [
|
|
67
|
+
{ 'switch': {'name': 'light_1', 'command_on': '1', 'command_off': '0'}},
|
|
68
|
+
{ 'switch': {'name': 'light_2', 'command_on': '3', 'command_off': '2'}}
|
|
69
|
+
]
|
|
70
|
+
|