Skip to content

Commit

Permalink
Version 2.5
Browse files Browse the repository at this point in the history
- New feature: add template conditions
- New feature: support valve (#146)
- Fix crash due to new mqtt library (#151)
- Improved documentation (including #152)
  • Loading branch information
arthurdent75 committed Feb 29, 2024
1 parent b5688c1 commit 7da20d9
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 69 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
**Version 2.5**
- New feature: add template conditions
- New feature: support valve (#146)
- Fix crash due to new mqtt library (#151)
- Improved documentation (including #152)

**Version 2.2.1**
- Fix "Simple Scheduler no longer switching on lamps with brightness" (#142)

Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,36 @@ If you need more advanced features:
- You can set the temperature of a climate without turning it on. Write **16:30>TO22.5** to set the temperature to 22.5°
- You can set the humidity of a (de)humidifier. Write **16:30>H65** to set the humidity to 65%
- You can set the position of a cover. Write **16:30>P25** will set the cover at 25%
- You can set the fan speed. Write **16:30>F25** will turn on the fan at 25%
- You can set the fan speed. Write **16:30>F25** will turn on the fan at 25%
- You can set the valve position. Write **16:30>P25** will set the valve position at 25%
- Brightness/Temperature/Position/Speed only works in the "TURN ON" section (obviously)!
- It's not mandatory to add both ON and OFF time. You can leave one of them empty if you don't need it. For example, you want to turn off a light every day at 22:00, but you don't need to turn it on.
- You can also choose to disable a schedule: the schedule will stay there, but it will not be executed until you will enable it back
- You can **drag the rows to sort them**, so you can keep them organized as you like!

Look at the picture above to see all these things in action (and combined!).

### Conditions
For each scheduler, you can add a condition that will be checked at the time of the execution.
If the condition is 'true' the action will be performed and (obviously) it won't be executed if the condition is 'false'.
The condition is a template expression that you can add in the "template" field. \
The condition is evaluated at every triggering time written in the scheduler before the execution. \
If the field is empty, no check will be performed and the action will always be executed. \
If you fill the field, two additional time fields will apper. This allows you to set on/off times when condition result is true and different on/off times when condition result is false. \
The template expression **must return a boolean** ('True' or 'False'). \
So be sure to "convert" switches, lights, and any other entity states to boolean. A few examples:
```
{{ states('switch.my_switch') | bool }}
{{ not states('light.my_light') | bool }}
{{ states('binary_sensor.workday) | bool }}
{{ states('sensor.room_temperature') | float > 23.5 }}
{{ is_state('person.my_kid', 'not_home') }}
{{ states('sensor.room_temperature') | float > 23.5 and is_state('sun.sun', 'above_horizon') }}
```
If the template returns 'on', 'open', 'home', 'armed', '1' and so on, they all will all be treated as 'False'. \
If the template expression has syntax errors it will be considered 'false', and it will be reported in the addon log.\
Use the template render utility in Developer Tools to test the condition before putting it into the scheduler.

### Frontend switch to enable/disable (with MQTT)
If you want to enable/disable schedulers in frontend and/or automation, you can achieve that through MQTT.
This feature is disabled by default because it requires a working MQTT server (broker) and Home Assistant MQTT integration.
Expand Down Expand Up @@ -79,6 +101,12 @@ Every schedule (or row, if you prefer) is a JSON file stored in the [share/simpl
This way the data can "survive" an addon upgrade or reinstallation.
You can easily backup and restore them in case of failure. In the same way, you can (accidentally?) delete them. So be aware of that.

### Log
The log file is stored in the same folder where the JSON files are stored [share/simplescheduler]
You can delete it if it become too large, but be sure to stop the addon first.
You can enable a verbose log by checking the box **debug mode** in the addon configuration.
When enabled, the log file can easily become very large, so be sure to keep the debug mode on only the required time.

### Last but not least
If you want to convince me to stay up at night to work on this, just <a target="_blank" href="https://www.paypal.com/donate/?hosted_button_id=8FN58C8SM9LLW">buy me a beer 🍺</a> \
You may say that regular people need coffee to do that. Well, I'm not a regular person.
8 changes: 4 additions & 4 deletions rootfs/etc/services.d/interface/finish
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Home Assistant Community Add-on: Example
# Home Assistant Community Add-on: Interface
# ==============================================================================
if [[ "${1}" -ne 0 ]] && [[ "${1}" -ne 256 ]]; then
bashio::log.warning "example 2 crashed, halting add-on"
/run/s6/basedir/bin/halt
bashio::log.warning "Interface process crashed..."
# /run/s6/basedir/bin/halt
fi

bashio::log.info "example 2 stoped, restarting..."
bashio::log.info "Restarting..."
8 changes: 4 additions & 4 deletions rootfs/etc/services.d/scheduler/finish
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Home Assistant Community Add-on: Example
# Home Assistant Community Add-on: Scheduler
# ==============================================================================
if [[ "${1}" -ne 0 ]] && [[ "${1}" -ne 256 ]]; then
bashio::log.warning "example 2 crashed, halting add-on"
/run/s6/basedir/bin/halt
bashio::log.warning "Scheduler process crashed..."
# /run/s6/basedir/bin/halt
fi

bashio::log.info "example 2 stoped, restarting..."
bashio::log.info "Restarting..."
84 changes: 75 additions & 9 deletions rootfs/simplescheduler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import simpleschedulerconf

valid_domains = ["light", "scene", "switch", "script", "camera", "climate", "cover", "vacuum", "fan", "humidifier",
"automation", "input_boolean", "media_player"]
"valve","automation", "input_boolean", "media_player"]
lwt_topic = "homeassistant/switch/simplescheduler/availability"
sun_data = ""
schedulers_list = []
Expand Down Expand Up @@ -152,12 +152,15 @@ def webserver_update():
sid = request.form.get('id')
enabled = request.form.get("enabled")
dontretry = request.form.get("dontretry")
template = request.form.get("template")
name = request.form.get("name")
entity_id = request.form.getlist('entity_id[]')
type = request.form.get('type')
if type != 'weekly':
on_tod = request.form.get('on_tod')
on_tod_false = request.form.get('on_tod_false')
off_tod = request.form.get('off_tod')
off_tod_false = request.form.get('off_tod_false')
on_dow = ""
off_dow = ""
for o in request.form.getlist('on_dow[]'):
Expand All @@ -170,6 +173,7 @@ def webserver_update():
data['name'] = name if name else sid
data['enabled'] = enabled if enabled else 0
data['dontretry'] = dontretry if dontretry else 0
data['template'] = template.replace('"',"'") if template else ''
data['entity_id'] = entity_id
if type == 'weekly':
data['weekly']['on_1'] = request.form.get('on_1')
Expand Down Expand Up @@ -202,6 +206,8 @@ def webserver_update():
data['off_tod'] = off_tod
data['on_dow'] = on_dow
data['off_dow'] = off_dow
data['on_tod_false'] = on_tod_false
data['off_tod_false'] = off_tod_false
file = simpleschedulerconf.json_folder + sid + '.json'
with open(file, 'w') as jsonFile:
json.dump(data, jsonFile)
Expand Down Expand Up @@ -237,14 +243,32 @@ def webserver_dirty():
r = '0'
return make_response(r, 200)

@app.route("/validatetemplate", methods=['GET'])
def webserver_validatetemplate():
response = ""
data = request.args
t = data["t"]
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
command_url = simpleschedulerconf.HASSIO_URL + "/template"
post_data = '{"template":"' + t + '"}'
try:
r = requests.post(url=command_url, data=post_data, headers=headers, timeout=request_timeout)
if r.content:
response = r.content.decode().lower()
if r.status_code != 200:
if "message" in response:
json_response = json.loads(r.content)
response = json_response['message']
finally:
return make_response(response, 200)

@app.context_processor
def utility_processor():
def format_event(value: str, showvalue: bool):
if not value: return ''
result: str = ""
extra: str = ""
events = value.upper().replace(',', ' ').replace(';', ' ').split(' ')
events = value.upper().replace(',', ' ').replace(';', ' ').split(" ")

for e in events:
p = e.split('>') # separate time from extra commands
Expand All @@ -264,12 +288,12 @@ def format_event(value: str, showvalue: bool):
else:
extra = '<span class="event-type-t"><i class="mdi mdi-power" aria-hidden="true"></i>' + v + '&deg;</span>'
if prefix == 'H':
extra = '<span class="event-type-h"><i class="mdi mdi-water-percent" aria-hidden="true"></i>' + v + '%</span>'
extra = '<span class="event-type-h"><i class="mdi mdi-water-percent" aria-hidden="true"></i>' + v + '%</span>'
if prefix == 'B':
vv = v.split('|')
brightness = vv[0]
extrainfo = ""
if len(vv) > 1 :
if len(vv) > 1:
color = vv[1]
colorValue = color[1:]
if color[0] == 'K':
Expand All @@ -278,12 +302,12 @@ def format_event(value: str, showvalue: bool):
extrainfo = ' <div class="colorsample" title="#' + color + '" style="background-color:#' + color + ';" ></div>'
if brightness[0] == 'A':
brightness = brightness[1:]
extra = '<span class="event-type-b"><i class="mdi mdi-lightbulb" aria-hidden="true"></i>' + brightness + extrainfo +'</span>'
extra = '<span class="event-type-b"><i class="mdi mdi-lightbulb" aria-hidden="true"></i>' + brightness + extrainfo + '</span>'
else:
extra = '<span class="event-type-b"><i class="mdi mdi-lightbulb" aria-hidden="true"></i>' + brightness + '%' + extrainfo + '</span>'

if t: result += '<span>' + t + extra + '</span >'

result += '<span>' + t + extra + '</span >'
return result

return dict(format_event=format_event)
Expand Down Expand Up @@ -589,7 +613,7 @@ def get_entity_status(e, check):
if check:
altered_response = response
domain = e.lower().split(".")
if domain[0] == 'cover':
if domain[0] == 'cover' or domain[0] == 'valve':
altered_response = 'on' if response == "open" else 'off'
if domain[0] == 'climate' and response != 'off':
altered_response = 'on'
Expand All @@ -600,6 +624,33 @@ def get_entity_status(e, check):
return response


def evaluate_template(t: str):
opt = get_options()
response: bool = False
content = ""
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
command_url = simpleschedulerconf.HASSIO_URL + "/template"
post_data = '{"template":"' + t + '"}'
try:
if opt['debug']: printlog("DEBUG: Evaluating template: %s" % (t))
r = requests.post(url=command_url, data=post_data, headers=headers, timeout=request_timeout)
if r.content:
content = r.content.decode()
if r.status_code != 200:
printlog("ERROR: Error calling HA API " + str(r.status_code))
if "message" in content.lower():
json_response = json.loads(r.content)
error= json_response['message']
printlog("ERROR: template error: %s" % (error))
else:
if content.lower() == 'true':
response = True

except:
printlog("ERROR: Unable to call Home Assistant template API")
return response


def call_ha_api(command_url: str, post_data: str):
opt = get_options()
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
Expand Down Expand Up @@ -644,13 +695,13 @@ def call_ha(eid_list, action, passedvalue, friendly_name):
postdata = '{"entity_id":"%s","brightness":"%d"}' % (eid, v)

if part_two:
if part_two[0]=="K":
if part_two[0] == "K":
kelvin = int(part_two[1:])
postdata = '{"entity_id":"%s","brightness":"%d","color_temp_kelvin":"%d"}' % (eid, v, kelvin)
else:
HEX_color = part_two
rgb = list(int(HEX_color[i:i + 2], 16) for i in (0, 2, 4))
postdata = '{"entity_id":"%s","brightness":"%d","rgb_color":%s}' % (eid, v , rgb)
postdata = '{"entity_id":"%s","brightness":"%d","rgb_color":%s}' % (eid, v, rgb)

if domain[0] == "fan" and value != "":
v = value
Expand All @@ -668,6 +719,17 @@ def call_ha(eid_list, action, passedvalue, friendly_name):
command_url = simpleschedulerconf.HASSIO_URL + "/services/cover/open_cover"
command = "Opening"

if domain[0] == "valve":
if value != "":
command_url = simpleschedulerconf.HASSIO_URL + "/services/valve/set_valve_position"
postdata = '{"entity_id":"%s","position":"%s"}' % (eid, value)
command = "Setting"
extra = "position to " + value + '%'
else:
if action == "on":
command_url = simpleschedulerconf.HASSIO_URL + "/services/valve/open_valve"
command = "Opening"

if domain[0] == "climate" and value != "":
if value[0] == "O":
v = value[1:]
Expand All @@ -681,6 +743,10 @@ def call_ha(eid_list, action, passedvalue, friendly_name):
command_url = simpleschedulerconf.HASSIO_URL + "/services/cover/close_cover"
command = "Closing"

if domain[0] == "valve":
command_url = simpleschedulerconf.HASSIO_URL + "/services/valve/close_valve"
command = "Closing"

printlog("SCHED: %s [%s] %s" % (command, friendly_name.get(eid, eid), extra))
call_ha_api(command_url, postdata)

Expand Down
1 change: 1 addition & 0 deletions rootfs/simplescheduler/options.dat
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"vacuum": true,
"fan": true,
"humidifier": true,
"valve": true,
"automation": true,
"input_boolean": true,
"media_player": true
Expand Down
Loading

0 comments on commit 7da20d9

Please sign in to comment.