Skip to content

Commit

Permalink
Update documentation, rename action scripts
Browse files Browse the repository at this point in the history
This brings the README.md up-to-date and renames the Tuya action scripts
such that they are less ambiguous.

iot-tuya.pl:
add check for missing configuration;
  • Loading branch information
tvogel committed Nov 26, 2023
1 parent 9a6aeed commit 4099a01
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 91 deletions.
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# Controlling smart heating valves from a CalDAV resource
# Controlling smart heating valves from a CalDAV calendar resource

This Python script can be used to check a CalDAV resource for occupation of a
room and control heating using generic web requests accordingly. This script
does the check just once for the current instance of time and invokes
correspondingly one of two web requests. In order to run this script regularly,
use `cron` or a similar service. I highly recommend using
room and control heating using web requests or Tuya scenes accordingly. This
script does the check just once for the current instance of time and
consequently invokes one of two configured actions. In order to run this script
regularly, use `cron` or a similar service. I highly recommend using
[cronic](https://habilis.net/cronic/) for wrapping the call to this script in
cron. Also, logging can be achieved using `tee -a` to a file of your choice
(see [`caldav-trigger.sh`](caldav-trigger.sh)).

Even though this script was written for smart heating valves, it can of course
be used for anything that you can control in an on/off fashion using web
be used for anything that you can control in an *on*/*off* fashion using web
requests.

Parameters and credentials for accessing the CalDAV resource and invoking the
web request are configured using the system environment or a `.env` file. See
[`example.env`](example.env).
intended actions are configured using the system environment or a `.env` file.
See [`example.env`](example.env).

Furthermore, there are three parameters controlling the behavior:
There are three parameters controlling the behavior:
* `preheat_minutes`: This time ahead of an occupation, the `heat_on` action is
invoked.
* `cooloff_minutes`: This time ahead of the end of an occupation, the `heat_off`
Expand Down Expand Up @@ -47,11 +47,40 @@ be shortened, prolonged, deleted or created and the `no_heat_tag` can be added
or removed with short notice and the next run of the script will make the
changes effective.

## Actions

In order to turn the heating *on* or *off*, an action script configured in
`.env` is invoked with `on` or `off` as parameter. Currently, these are the
options:

- [webhooks.py](webhooks.py): This script allows to call generic web-hooks, e.g.
on [IFTTT](https://ifttt.com)
- [iot-tuya.py](iot-tuya.py): This script allows to invoke Tuya scenes via the
Tuya Open API. You'll need to register at https://iot.tuya.com/, create a
cloud project and link your Tuya/SmartLife account there.
- [tuya-qr-sharing.py](tuya-qr-sharing.py): This script will invoke Tuya scenes
via the
[Tuya Device Sharing SDK](https://github.com/tuya/tuya-device-sharing-sdk).
This is easier because you can authorize the script via scanning a QR code in
your Tuya or SmartLife app without registering a project on
https://iot.tuya.com/.

You can easily adapt this logic to your own action scripts by implementing new
action scripts. Pull-requests are welcome!

The [iot-tuya.py](iot-tuya.py) and [tuya-qr-sharing.py](tuya-qr-sharing.py)
scripts have additional commands that help in configuring the desired Tuya
scenes and performing the QR authorization step. Please run them without
command line parameters to see the options.

## Code

The CalDAV and web request access is coded in the main script
The CalDAV and action invocation is coded in the main script
[`caldav-trigger.py`](caldav-trigger.py), the evaluation logic is in
[`logic.py`](logic.py) with `pytest-mock` unit-tests in
[`logic_test.py`](logic_test.py).

The actions are implemented in the respective scripts, linked above.

All dependencies are given in [requirements.txt](requirements.txt).

38 changes: 19 additions & 19 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ no_heat_tag = "!cold!"
preheat_minutes = 60
cooloff_minutes = 30

action = "webhooks.py" # or "tuya.py" or "smartlife.py"
action = "webhooks.py" # or "iot-tuya.py" or "tuya-qr-sharing.py"

webhooks_url="https://maker.ifttt.com/trigger/{action}/with/key/{key}"
webhooks_key="webhooks_key"
Expand All @@ -20,22 +20,22 @@ webhooks_heat_off_action="heating_off"
# defaults to "no timeout"
# webhooks_timeout=60

tuya_access_id = '<from https://iot.tuya.com/cloud/basic>'
tuya_access_secret = '<from https://iot.tuya.com/cloud/basic>'
tuya_username = '<from Tuya App>'
tuya_password = '<from Tuya App>'
tuya_endpoint_url = 'https://openapi.tuyaeu.com'
tuya_country_code = '49'
tuya_schema = 'tuyaSmart'
tuya_home = '<from tuya.py homes>'
tuya_scene_off = '<from tuya.py scenes>'
tuya_scene_on = '<from tuya.py scenes>'
iot_tuya_access_id = '<from https://iot.tuya.com/cloud/basic>'
iot_tuya_access_secret = '<from https://iot.tuya.com/cloud/basic>'
iot_tuya_username = '<from Tuya App>'
iot_tuya_password = '<from Tuya App>'
iot_tuya_endpoint_url = 'https://openapi.tuyaeu.com'
iot_tuya_country_code = '49'
iot_tuya_schema = 'tuyaSmart'
iot_tuya_home = '<from iot-tuya.py homes>'
iot_tuya_scene_off = '<from iot-tuya.py scenes>'
iot_tuya_scene_on = '<from iot-tuya.py scenes>'

smartlife_user_code = '<from SmartLife/Tuya app under Settings/Account/User-Code>'
smartlife_username = '<automatically retrieved after QR authorization>'
smartlife_token_info = '<automatically retrieved after QR authorization>'
smartlife_terminal_id= '<automatically retrieved after QR authorization>'
smartlife_endpoint = '<automatically retrieved after QR authorization>'
smartlife_home = '<from smartlife.py scenes>'
smartlife_scene_off = '<from smartlife.py scenes>'
smartlife_scene_on = '<from smartlife.py scenes>'
tuya_qr_sharing_user_code = '<from SmartLife/Tuya app under Settings/Account/User-Code>'
tuya_qr_sharing_username = '<automatically retrieved after QR authorization>'
tuya_qr_sharing_token_info = '<automatically retrieved after QR authorization>'
tuya_qr_sharing_terminal_id= '<automatically retrieved after QR authorization>'
tuya_qr_sharing_endpoint = '<automatically retrieved after QR authorization>'
tuya_qr_sharing_home = '<from tuya-qr-sharing.py scenes>'
tuya_qr_sharing_scene_off = '<from tuya-qr-sharing.py scenes>'
tuya_qr_sharing_scene_on = '<from tuya-qr-sharing.py scenes>'
56 changes: 34 additions & 22 deletions tuya.py → iot-tuya.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,37 @@
EXIT_AUTHENTICATION_FAILED = 1
EXIT_SYNTAX_ERROR = 2
EXIT_LIST_HOMES_FAILED = 3
EXIT_TUYA_HOME_MISSING = 4
EXIT_HOME_MISSING = 4
EXIT_LIST_SCENES_FAILED = 5
EXIT_TUYA_SCENE_MISSING = 6
EXIT_SCENE_MISSING = 6
EXIT_TRIGGER_SCENE_FAILED = 7

def main() -> int:
dotenv.load_dotenv()

for key in [ 'endpoint_url',
'access_id',
'access_secret',
'username',
'password',
'country_code',
'schema' ]:
if not os.environ.get('iot_tuya_' + key):
print( f"Please specify missing {'iot_tuya_' + key} in your .env file.",
file=sys.stderr)
return EXIT_AUTHENTICATION_FAILED

# Init
# TUYA_LOGGER.setLevel(logging.DEBUG)
openapi = TuyaOpenAPI(
os.environ.get('tuya_endpoint_url'),
os.environ.get('tuya_access_id'),
os.environ.get('tuya_access_secret'))
os.environ.get('iot_tuya_endpoint_url'),
os.environ.get('iot_tuya_access_id'),
os.environ.get('iot_tuya_access_secret'))
response = openapi.connect(
os.environ.get('tuya_username'),
os.environ.get('tuya_password'),
os.environ.get('tuya_country_code'),
os.environ.get('tuya_schema'))
os.environ.get('iot_tuya_username'),
os.environ.get('iot_tuya_password'),
os.environ.get('iot_tuya_country_code'),
os.environ.get('iot_tuya_schema'))

if not response['success']:
print('Authentication failed: %s' % response['msg'], file=sys.stderr)
Expand All @@ -44,7 +56,7 @@ def main() -> int:
cmd = ''

if not cmd in [ 'homes', 'scenes', 'on', 'off' ]:
print('Syntax: tuya.py (on|off|homes|scenes)', file=sys.stderr)
print('Syntax: iot-tuya.py (on|off|homes|scenes)', file=sys.stderr)
return EXIT_SYNTAX_ERROR;

if cmd == 'homes':
Expand All @@ -58,12 +70,12 @@ def main() -> int:
print('%s: %s' % (home['name'], home['home_id']))
return EXIT_OK

home_id = os.environ.get('tuya_home')
home_id = os.environ.get('iot_tuya_home')

if cmd == 'scenes':
if not home_id:
print('Set tuya_home in .env first, in order to list scenes!', file=sys.stderr)
return EXIT_TUYA_HOME_MISSING
print('Set iot_tuya_home in .env first, in order to list scenes!', file=sys.stderr)
return EXIT_HOME_MISSING
response = openapi.get('/v1.1/homes/{home_id}/scenes'.format(home_id = home_id))
if not response['success']:
print('Error listing scenes: %s' % response['msg'], file=sys.stderr)
Expand All @@ -74,25 +86,25 @@ def main() -> int:
return EXIT_OK

if not home_id:
print('Set tuya_home in .env first, in order to trigger scenes!', file=sys.stderr)
return EXIT_TUYA_HOME_MISSING
print('Set iot_tuya_home in .env first, in order to trigger scenes!', file=sys.stderr)
return EXIT_HOME_MISSING

if cmd == 'on':
scene_id = os.environ.get('tuya_scene_on')
scene_id = os.environ.get('iot_tuya_scene_on')
if not scene_id:
print('Set tuya_scene_on in .env first!', file=sys.stderr)
return EXIT_TUYA_SCENE_MISSING
print('Set iot_tuya_scene_on in .env first!', file=sys.stderr)
return EXIT_SCENE_MISSING
elif cmd == 'off':
scene_id = os.environ.get('tuya_scene_off')
scene_id = os.environ.get('iot_tuya_scene_off')
if not scene_id:
print('Set tuya_scene_off in .env first!', file=sys.stderr)
return EXIT_TUYA_SCENE_MISSING
print('Set iot_tuya_scene_off in .env first!', file=sys.stderr)
return EXIT_SCENE_MISSING

response = openapi.post('/v1.0/homes/{home_id}/scenes/{scene_id}/trigger'.format(home_id = home_id, scene_id = scene_id))
if not response['success']:
print('Error triggering scene: %s' % response['msg'], file=sys.stderr)
return EXIT_TRIGGER_SCENE_FAILED
print('Tuya trigger succeeded.')
print('iot.tuya trigger succeeded.')
return EXIT_OK

if __name__ == '__main__':
Expand Down
Loading

0 comments on commit 4099a01

Please sign in to comment.