Skip to content

Commit 7c32458

Browse files
committed
Add timeout and ssl verify setup options & simplify requests command handler
1 parent 89c3422 commit 7c32458

File tree

8 files changed

+366
-206
lines changed

8 files changed

+366
-206
lines changed

CHANGELOG.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
## [0.2-beta] - 2024-06-26
11+
12+
### Breaking changes
13+
- This integration now uses a configuration file to store the setup state and advanced settings (see below). Existing users therefore need to run the integration setup again to create this file. Just open the integration settings and click on "Start integration setup". You don't need to enter the advanced settings.
14+
1015
### Added
1116

1217
- Added a more granular http status code response handling
1318
- Added optional parameter to send form data in the request body as key/value pairs (see README)
19+
- Added optional custom global entity-independent timeout and ssl verify option in the integration setup. For self signed ssl certificates to work the ssl verify option needs to be deactivated.
1420

1521
### Changed
1622
- Only return an error response to the remote if the http response code is in the 400 or 500 range. Otherwise display the status code in the integration log if it's not 200/Ok
1723

18-
## [0.1-beta] - 2024-03-27
24+
## [0.1-beta] - 2024-04-27
1925

2026
### Added
2127

README.md

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
1-
# HTTP Get/Post/Put/Patch & Wake on LAN Integration for Unfolded Circle Remote Two
1+
# HTTP Get/Post/Put/Patch & Wake on LAN Integration for Unfolded Circle Remote Two and Remote 3
22

33
## ⚠️ WARNING ⚠️
44

55
### Disclaimer: This software is at an early stage of development and may contain serious bugs that could affect system stability. Please use it at your own risk!
66

77
##
88

9-
Integration for [Unfolded Circle Remote Two](https://unfoldedcircle.com) to send network requests to a specified url or mac address.
9+
Integration for Unfolded Circle [Remote Two](https://www.unfoldedcircle.com/remote-two) and [Remote 3](https://www.unfoldedcircle.com) running [Unfolded OS](https://www.unfoldedcircle.com/unfolded-os) to send network requests to a specified url or mac address.
1010

1111
Using [uc-integration-api](https://github.com/aitatoi/integration-python-library), [requests](https://github.com/psf/requests) and [pywakeonlan](https://github.com/remcohaszing/pywakeonlan).
1212

1313

1414
### Supported features
1515

1616
- Send http(s) get, post, patch & put requests to a specified url
17+
- Set a custom timeout and deactivate ssl certificate verification
1718
- Send Wake on LAN magic packets to a specified mac address
1819

1920

2021
### Planned features
2122

2223
- Send Wake on LAN magic packets by entering the ip address
2324
- Support for sending json and xml data in request body
24-
- Configurable timeout in integration setup
25-
- Support for self signed SSL certificates / deactivate SSL verification in integration setup
2625

2726
*Planned improvements are labeled with #TODO in the code*
2827

2928

29+
## Configuration
30+
31+
During the integration setup you can change the default http request timeout of 2 seconds to a custom value. You also can deactivate the ssl certificate verification. This is needed for self signed certificates. You can run the setup process again in the integration settings after adding entities to the remote.
32+
3033
## Usage
3134

32-
The integration exposes a media player entity for each supported request command. These entities only support the source feature. Just enter the desired url (including http(s)://) or mac address in the source field when you configure your activity/macro sequences or activity ui. The default timeout is 2 seconds.
35+
The integration exposes a media player entity for each supported request command. These entities only support the source feature. Just enter the desired url (including http(s)://) or mac address in the source field when you configure your activity/macro sequences or activity ui.
3336
<br>
34-
For http requests your server needs to respond with a 200 OK or any other informational or redirection http status codes (100s, 200s or 300s). In case of a client or server error (400s or 500s) the command will fail on the remote and the error and status code will be shown in the log of the integration.
37+
For http requests your server needs to respond with a *200 OK* status or any other informational or redirection http status codes (100s, 200s or 300s). In case of a client or server error (400s or 500s) the command will fail on the remote and the error message and status code will be shown in the integration log.
3538
<br>
3639
<br>
37-
Optional form data in the request body as key/value pairs can be added with a paragraph as a separator like this:
40+
Optional form data in the request body as key/value pairs can be added with a paragraph character (§) as a separator like this:
3841
- https://httpbin.org/post§key1=value1,key2=value2
3942

40-
Note that if your url contains a paragraph you need to url-encode it first (%C2%A7, see https://www.urlencoder.io)
43+
Note that if your url contains a paragraph character you need to url-encode it first (%C2%A7, see https://www.urlencoder.io)
4144

4245

4346
### Setup

intg-requests/config.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"rq_timeout": 5, "rq_ssl_verify": true, "setup_complete": true}

intg-requests/config.py

+92-12
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
import json
2+
import os
13
import logging
24

35
_LOG = logging.getLogger(__name__)
46

7+
CFG_FILENAME = "config.json"
58

69

710
class setup:
8-
cmds = ["get", "post", "patch", "put", "wol"]
9-
1011
__conf = {
1112
"standby": False,
12-
"rq-timeout": 2,
13+
"setup_complete": False,
14+
"setup_reconfigure": False,
15+
"rq_timeout": 2,
16+
"rq_ssl_verify": True,
1317
"id-get": "http-get",
1418
"name-get": "HTTP Get",
1519
"id-post": "http-post",
@@ -21,19 +25,95 @@ class setup:
2125
"id-wol": "wol",
2226
"name-wol": "Wake on LAN"
2327
}
24-
__setters = ["standby"]
25-
28+
__setters = ["standby", "setup_complete", "setup_reconfigure", "rq_timeout", "rq_ssl_verify"]
29+
__storers = ["setup_complete", "rq_timeout", "rq_ssl_verify"] #Skip runtime only related values in config file
30+
all_cmds = ["get", "post", "patch", "put", "wol"]
31+
rq_ids = [__conf["id-get"], __conf["id-post"], __conf["id-patch"], __conf["id-put"]]
32+
rq_names = [__conf["name-get"], __conf["name-post"], __conf["name-patch"], __conf["name-put"]]
33+
2634
@staticmethod
27-
def get(value):
28-
if setup.__conf[value] != "":
29-
return setup.__conf[value]
35+
def get(key):
36+
if setup.__conf[key] != "":
37+
return setup.__conf[key]
3038
else:
31-
_LOG.error("Got empty value from config storage")
39+
_LOG.error("Got empty value for " + key + " from config storage")
3240

3341
@staticmethod
3442
def set(key, value):
43+
3544
if key in setup.__setters:
36-
setup.__conf[key] = value
37-
_LOG.debug("Stored " + key + ": " + str(value) + " into runtime storage")
45+
if setup.__conf["setup_reconfigure"] == True and key == "setup_complete":
46+
_LOG.debug("Ignore setting and storing setup_complete flag during reconfiguration")
47+
else:
48+
setup.__conf[key] = value
49+
_LOG.debug("Stored " + key + ": " + str(value) + " into runtime storage")
50+
51+
#Store key/value pair in config file
52+
if key in setup.__storers:
53+
54+
jsondata = {key: value}
55+
if os.path.isfile(CFG_FILENAME):
56+
try:
57+
with open(CFG_FILENAME, "r+") as f:
58+
l = json.load(f)
59+
l.update(jsondata)
60+
f.seek(0)
61+
f.truncate() #Needed when the new value has less characters than the old value (e.g. false to true or 11 to 5 seconds timeout)
62+
json.dump(l, f)
63+
f.close
64+
_LOG.debug("Stored " + key + ": " + str(value) + " into " + CFG_FILENAME)
65+
except OSError as o:
66+
raise Exception(o)
67+
except:
68+
raise Exception("Error while storing " + key + ": " + str(value) + " into " + CFG_FILENAME)
69+
70+
#Create config file first if it doesn't exists yet
71+
else:
72+
try:
73+
with open(CFG_FILENAME, "w") as f:
74+
json.dump(jsondata, f)
75+
f.close
76+
_LOG.debug("Created " + CFG_FILENAME + " and stored " + key + ": " + str(value) + " in it")
77+
except OSError as o:
78+
raise Exception(o)
79+
except:
80+
raise Exception("Error while storing " + key + ": " + str(value) + " into " + CFG_FILENAME)
81+
82+
else:
83+
_LOG.debug(key + " not found in __storers because it should not be stored in the config file")
84+
85+
else:
86+
raise NameError(key + " not found in __setters because it should not be changed")
87+
88+
@staticmethod
89+
def load():
90+
if os.path.isfile(CFG_FILENAME):
91+
92+
try:
93+
with open(CFG_FILENAME, "r") as f:
94+
configfile = json.load(f)
95+
except:
96+
raise OSError("Error while reading " + CFG_FILENAME)
97+
if configfile == "":
98+
raise OSError("Error in " + CFG_FILENAME + ". No data")
99+
100+
setup.__conf["setup_complete"] = configfile["setup_complete"]
101+
_LOG.debug("Loaded setup_complete: " + str(configfile["setup_complete"]) + " into runtime storage from " + CFG_FILENAME)
102+
103+
if not setup.__conf["setup_complete"]:
104+
_LOG.warning("The setup was not completed the last time. Please restart the setup process")
105+
else:
106+
if "rq_timeout" in configfile:
107+
setup.__conf["rq_timeout"] = configfile["rq_timeout"]
108+
_LOG.info("Loaded requests timeout of " + str(configfile["rq_timeout"]) + " seconds into runtime storage from " + CFG_FILENAME)
109+
else:
110+
_LOG.info("Skip loading custom requests timeout as it has not been changed during setup. Default value of " + str(setup.get("rq_timeout")) + " seconds will be used")
111+
112+
if "rq_ssl_verify" in configfile:
113+
setup.__conf["rq_ssl_verify"] = configfile["rq_ssl_verify"]
114+
_LOG.info("Loaded http ssl verification: " + str(configfile["rq_ssl_verify"]) + " into runtime storage from " + CFG_FILENAME)
115+
else:
116+
_LOG.info("Skip loading http ssl verification flag as it has not been changed during setup. Default value " + str(setup.get("rq_ssl_verify")) + " will be used")
117+
38118
else:
39-
raise NameError("Name not accepted in set() method")
119+
_LOG.info(CFG_FILENAME + " does not exist (yet). Please start the setup process")

intg-requests/driver.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import config
1111
import media_player
12+
import setup
1213

1314
_LOG = logging.getLogger("driver") # avoid having __main__ in log messages
1415

@@ -19,23 +20,22 @@
1920

2021
async def startcheck():
2122
"""
22-
Called at the start of the integration driver to add a media player entity for all configured cmds list entries in config.py
23+
Called at the start of the integration driver to load the config file into the runtime storage and add a media player entity for all configured cmds
2324
"""
25+
try:
26+
config.setup.load()
27+
except OSError as o:
28+
_LOG.critical(o)
29+
_LOG.critical("Stopping integration driver")
30+
raise SystemExit(0)
2431

25-
for cmd in config.setup.cmds:
26-
id = config.setup.get("id-"+cmd)
27-
name = config.setup.get("name-"+cmd)
28-
29-
if api.available_entities.contains(id):
30-
_LOG.debug("Entity with id " + id + " is already in storage as available entity")
31-
else:
32-
_LOG.info("Add entity with id " + id + " and name " + name + " as available entity")
33-
await add_mp(id, name)
32+
if config.setup.get("setup_complete"):
33+
await setup.add_mp_all()
3434

3535

3636

3737
async def add_mp(id: str, name: str):
38-
#TODO Only works when in driver.py. When in media_player.py the response to get_available entities is an empty list
38+
# Only works when in driver.py. When in media_player.py the response to get_available entities is an empty list
3939
"""
4040
Creates the media player entity definition and adds the entity to the remote via the api
4141
@@ -72,7 +72,7 @@ async def mp_cmd_handler(entity: ucapi.MediaPlayer, cmd_id: str, _params: dict[s
7272
if _params == None:
7373
_LOG.info(f"Received {cmd_id} command for {entity.id}")
7474
else:
75-
_LOG.info(f"Received {cmd_id} command with parameter {_params} for {entity.id}")
75+
_LOG.info(f"Received {cmd_id} command with parameter {_params} for entity id {entity.id}")
7676

7777
return media_player.mp_cmd_assigner(entity.id, cmd_id, _params)
7878

@@ -91,7 +91,6 @@ async def on_r2_connect() -> None:
9191

9292

9393
@api.listens_to(ucapi.Events.DISCONNECT)
94-
#TODO Find out how to prevent the remote from constantly reconnecting when the integration is not running without deleting the integration configuration on the remote every time
9594
async def on_r2_disconnect() -> None:
9695
"""
9796
Disconnect notification from the remote Two.
@@ -174,7 +173,7 @@ async def main():
174173

175174
_LOG.debug("Starting driver")
176175

177-
await api.init("setup.json")
176+
await setup.init()
178177
await startcheck()
179178

180179

0 commit comments

Comments
 (0)