This commit is contained in:
2023-07-22 21:16:38 +07:00
parent 230d7bca78
commit 9cc2aabc38
28 changed files with 2513 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Home Assistant Community Add-on: Example
# ==============================================================================
if [[ "${1}" -ne 0 ]] && [[ "${1}" -ne 256 ]]; then
bashio::log.warning "example 2 crashed, halting add-on"
/run/s6/basedir/bin/halt
fi
bashio::log.info "example 2 stoped, restarting..."
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
#
# Home Assistant Add-on: SimpleScheduler
#
# ==============================================================================
bashio::log.info "Starting service.d [Interface]"
exec /simplescheduler/interface.sh
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
# Home Assistant Community Add-on: Example
# ==============================================================================
if [[ "${1}" -ne 0 ]] && [[ "${1}" -ne 256 ]]; then
bashio::log.warning "example 2 crashed, halting add-on"
/run/s6/basedir/bin/halt
fi
bashio::log.info "example 2 stoped, restarting..."
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
#
# Home Assistant Add-on: SimpleScheduler
#
# ==============================================================================
bashio::log.info "Starting service.d [Scheduler]"
exec /simplescheduler/scheduler.sh
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
#
# Home Assistant Add-on: SimpleScheduler
#
# ==============================================================================
bashio::log.info "Running interface.sh"
python3 /simplescheduler/main.py
@@ -0,0 +1,815 @@
from flask import Flask, render_template, request, redirect, make_response
import paho.mqtt.client as mqtt
import flask.cli
import logging
import glob
import os
import json
import uuid
from datetime import datetime, timedelta
import pytz
import requests
import re
import psutil
import simpleschedulerconf
lwt_topic = "homeassistant/switch/simplescheduler/availability"
sun_data = ""
schedulers_list = []
options = []
weekday = []
has_changed: bool = False
mqttclient = None
ha_timezone = "utc"
scheduler_pid = 0
request_timeout = 5 # seconds
app = Flask(__name__)
@app.route("/")
@app.route("/main")
def webserver_home():
return render_template('index.html',
data=load_json_schedulers(),
o=get_options(),
css=get_css(),
switchlist=get_switch_html_select_options(),
friendlynames=get_switch_friendly_names(),
sort=get_sort_list(),
weekday=weekday,
statusbarinfo=get_statusbar_info()
)
@app.route("/new")
def webserver_new():
return render_template('new.html')
@app.route('/delete', methods=['GET'])
def webserver_delete():
args = request.args
sid = args.get('id')
file = simpleschedulerconf.json_folder + sid + '.json'
if os.path.exists(file):
os.remove(file)
if options['MQTT']['enabled']:
mqttclient.publish('homeassistant/switch/simplescheduler/' + sid + '/config', "", qos=0, retain=1)
return redirect("main")
@app.route('/edit', methods=['GET'])
def webserver_edit():
is_new = False
args = request.args
sid = args.get('id')
stype: str = args.get('type')
file = simpleschedulerconf.json_folder + sid + '.json'
if sid != "0":
with open(file, "r") as read_file:
param = json.load(read_file)
else:
param = json.loads(get_json_template(stype))
param['id'] = uuid.uuid4().hex
is_new = True
return render_template('edit.html',
p=param,
o=get_options(),
weekday=weekday,
switchlist=get_switch_html_select_options(),
is_new=is_new
)
@app.route('/config', methods=['GET'])
def webserver_config():
return render_template('config.html',
o=get_options()
)
@app.route('/saveconfig', methods=['GET'])
def webserver_saveconfig():
jsondata = {}
translations = {}
components = {}
mqttconf = {}
content = request.args
for item in content:
if content[item] == '0' or content[item] == '1':
value = int(content[item])
else:
value = content[item]
if "." in item:
p = item.split(".")
cat = p[0].lower()
subcat = p[1]
if cat == "translations": translations[subcat] = value
if cat == "components": components[subcat] = value
if cat == "mqtt": mqttconf[subcat] = value
else:
jsondata[item] = value
jsondata["translations"] = translations
jsondata["components"] = components
jsondata["MQTT"] = mqttconf
option_file_path = os.path.join(simpleschedulerconf.json_folder, "options.dat")
with open(option_file_path, 'w') as option_file:
json_config = json.dump(jsondata, option_file)
init()
return redirect("main")
@app.route('/clone', methods=['GET'])
def webserver_clone():
args = request.args
sid = args.get('id')
file = simpleschedulerconf.json_folder + sid + '.json'
if os.path.exists(file) and sid != "0":
with open(file, "r") as read_file:
param = json.load(read_file)
newsid = uuid.uuid4().hex
newfile = simpleschedulerconf.json_folder + newsid + '.json'
param['id'] = newsid
param['name'] = param['name'] + " (2) "
with open(newfile, 'w') as jsonFile:
json.dump(param, jsonFile)
return redirect("main")
@app.route("/update", methods=['POST'])
def webserver_update():
sid = request.form.get('id')
enabled = request.form.get("enabled")
dontretry = request.form.get("dontretry")
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')
off_tod = request.form.get('off_tod')
on_dow = ""
off_dow = ""
for o in request.form.getlist('on_dow[]'):
on_dow += o
for o in request.form.getlist('off_dow[]'):
off_dow += o
data = json.loads(get_json_template(type))
data['id'] = sid
data['name'] = name if name else sid
data['enabled'] = enabled if enabled else 0
data['dontretry'] = dontretry if dontretry else 0
data['entity_id'] = entity_id
if type == 'weekly':
data['weekly']['on_1'] = request.form.get('on_1')
data['weekly']['on_2'] = request.form.get('on_2')
data['weekly']['on_3'] = request.form.get('on_3')
data['weekly']['on_4'] = request.form.get('on_4')
data['weekly']['on_5'] = request.form.get('on_5')
data['weekly']['on_6'] = request.form.get('on_6')
data['weekly']['on_7'] = request.form.get('on_7')
data['weekly']['off_1'] = request.form.get('off_1')
data['weekly']['off_2'] = request.form.get('off_2')
data['weekly']['off_3'] = request.form.get('off_3')
data['weekly']['off_4'] = request.form.get('off_4')
data['weekly']['off_5'] = request.form.get('off_5')
data['weekly']['off_6'] = request.form.get('off_6')
data['weekly']['off_7'] = request.form.get('off_7')
elif type == 'recurring':
data['recurring']['on_start'] = request.form.get('on_start')
data['recurring']['on_end'] = request.form.get('on_end')
data['recurring']['on_interval'] = request.form.get('on_interval')
data['recurring']['off_start'] = request.form.get('off_start')
data['recurring']['off_end'] = request.form.get('off_end')
data['recurring']['off_interval'] = request.form.get('off_interval')
data['on_tod'] = on_tod
data['off_tod'] = off_tod
data['on_dow'] = on_dow
data['off_dow'] = off_dow
else:
data['on_tod'] = on_tod
data['off_tod'] = off_tod
data['on_dow'] = on_dow
data['off_dow'] = off_dow
file = simpleschedulerconf.json_folder + sid + '.json'
with open(file, 'w') as jsonFile:
json.dump(data, jsonFile)
if options['MQTT']['enabled']:
mqtt_send_config(mqttclient)
# mqtt_publish_state(mqttclient, id, enabled, True)
return redirect("main")
@app.route("/sort", methods=['GET'])
def webserver_sort():
data = request.args
save_sort_list(data)
return make_response("", 200)
@app.route("/log", methods=['GET'])
def webserver_log():
response = ""
logfilepath = os.path.join(simpleschedulerconf.json_folder, "simplescheduler.log")
with open(logfilepath, "r", encoding='utf-8') as logfile:
response += logfile.read()
return make_response(response, 200)
@app.route("/dirty")
def webserver_dirty():
global has_changed
if has_changed:
r = '1'
has_changed = False
else:
r = '0'
return make_response(r, 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(' ')
for e in events:
p = e.split('>') # separate time from extra commands
t: str = p[0]
extra = ""
if len(p) > 1 and showvalue: # verify extra commands
prefix = p[1][0]
v: str = p[1][1:]
if prefix == 'F':
extra = '<span class="event-type-f"><i class="mdi mdi-fan" aria-hidden="true"></i>' + v + '%</span>'
if prefix == 'P':
extra = '<span class="event-type-p"><i class="mdi mdi-arrow-up-down" aria-hidden="true"></i>' + v + '%</span>'
if prefix == 'B':
if v[0] == 'A':
v = v[1:]
extra = '<span class="event-type-b"><i class="mdi mdi-lightbulb" aria-hidden="true"></i>' + v + '</span>'
else:
extra = '<span class="event-type-b"><i class="mdi mdi-lightbulb" aria-hidden="true"></i>' + v + '%</span>'
if prefix == 'T':
if v[0] == 'O':
v = v[1:]
extra = '<span class="event-type-to"><i class="mdi mdi-thermometer" aria-hidden="true"></i>' + v + '&deg;</span>'
else:
extra = '<span class="event-type-t"><i class="mdi mdi-power" aria-hidden="true"></i>' + v + '&deg;</span>'
result += '<span>' + t + extra + '</span >'
return result
return dict(format_event=format_event)
@app.context_processor
def utility_processor():
def get_friendly_html_dow(value: str, is_on: bool):
result: str = "<div>"
if len(value) > 0:
onOffClass = "dowHiglightG" if is_on else "dowHiglightR"
for wd in range(1, 8):
d = weekday[wd]
dclass = ""
if str(wd) in value:
dclass = onOffClass
result += '<div class="dowIcon ' + dclass + ' " >' + d + '</div>'
result += '</div>'
return result
return dict(get_friendly_html_dow=get_friendly_html_dow)
def on_connect(client, userdata, flags, rc):
if rc == 0:
printlog("STATUS: MQTT connected! ")
client.publish(lwt_topic, payload="online", qos=0, retain=True)
client.subscribe("homeassistant/switch/simplescheduler/#")
else:
printlog("ERROR: MQTT Error " + str(rc))
def on_message(client, userdata, msg):
global has_changed
payload = 0
pieces = msg.topic.split("/")
if len(pieces) > 4:
if pieces[4] == "set":
sid = pieces[3]
printlog('MQTT: RCV ' + msg.topic + " --> " + msg.payload.decode())
if msg.payload.decode() == 'ON':
payload = 1
update_json_file(sid, 'enabled', payload)
if options['MQTT']['enabled']:
mqtt_publish_state(client, sid, payload, True)
has_changed = "1"
def get_statusbar_info():
r = {
"sunrise": "N/A",
"sunset": "N/A",
"timezone": "N/A",
"scheduler": "Not running",
"mqtt": "Disabled"
}
tz = get_ha_timezone()
sunrise, sunset = get_sun(tz)
r['timezone'] = tz if tz else "N/A"
r['sunrise'] = sunrise.strftime("%H:%M") if sunrise else "N/A"
r['sunset'] = sunset.strftime("%H:%M") if sunset else "N/A"
pid = get_scheduler_pid()
if pid > 0:
r['scheduler'] = "Running (PID %s)" % pid
if options['MQTT']['enabled']:
r['mqtt'] = "Connected" if mqttclient.is_connected() else "Disconnected"
return r
def get_switch_list(domains):
full_switch_list = []
url = simpleschedulerconf.HASSIO_URL + "/states"
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
try:
r = requests.get(url=url, headers=headers, timeout=request_timeout)
for block in r.json():
item = {
"id": block["entity_id"],
"state": block["state"],
"friendly_name": "",
"domain": "",
}
pieces = block["entity_id"].split(".")
item["domain"] = pieces[0]
attributes = block["attributes"]
if "friendly_name" in attributes:
item["friendly_name"] = attributes["friendly_name"]
if item["domain"] in domains:
full_switch_list.append(item)
full_switch_list.sort(key=lambda x: x["id"], reverse=False)
except:
printlog("ERROR: Unable to obtain entities info from Home Assistant")
return full_switch_list
def get_switch_friendly_names():
friendly_names = {}
url = simpleschedulerconf.HASSIO_URL + "/states"
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
try:
r = requests.get(url=url, headers=headers, timeout=request_timeout)
for block in r.json():
key = block["entity_id"]
value = key
if "friendly_name" in block["attributes"]:
value = block["attributes"]["friendly_name"]
friendly_names[key] = value
except:
printlog("ERROR: Unable to obtain entities names from Home Assistant")
return friendly_names
def update_json_file(object_id, field_name, field_value):
file = simpleschedulerconf.json_folder + object_id + '.json'
try:
if os.path.exists(file):
with open(file, "r") as jsonFile:
data = json.load(jsonFile)
data[field_name] = field_value
with open(file, "w") as jsonFile:
json.dump(data, jsonFile)
except:
printlog("ERROR: Unable to update JSON file")
return True
def load_json_schedulers():
ss = []
os.chdir(simpleschedulerconf.json_folder)
for file in glob.glob("*.json"):
with open(file, "r") as read_file:
try:
ss.append(json.load(read_file))
except:
printlog("ERROR: scheduler file %s is corrupted" % file )
return ss
def mqtt_publish_state(client, object_id, pub_value, echo=False):
payload = 'OFF'
if pub_value:
payload = 'ON'
topic = 'homeassistant/switch/simplescheduler/' + object_id + '/state'
client.publish(topic, payload, qos=0, retain=1)
if echo:
printlog('MQTT: PUB ' + topic + ' --> ' + payload)
def mqtt_send_config(client):
schedulers = load_json_schedulers()
payload_template = '{"unique_id": "simplescheduler_###",' \
'"name": "SimpleScheduler: @@@" ,' \
'"icon":"mdi:calendar-clock" ,' \
'"cmd_t": "homeassistant/switch/simplescheduler/###/set",' \
'"stat_t": "homeassistant/switch/simplescheduler/###/state",' \
'"avty_t": "homeassistant/switch/simplescheduler/availability",' \
'"pl_avail":"online",' \
'"pl_not_avail":"offline"}'
config_topic_template = 'homeassistant/switch/simplescheduler/###/config'
for S in schedulers:
if S and 'name' in S:
topic = config_topic_template.replace('###', S['id'])
payload = payload_template.replace('###', S['id'])
payload = payload.replace('@@@', S['name'])
# slug = slugify(S['name'], separator="_")
# payload = payload.replace('&&&', slug)
client.publish(topic, payload, qos=0, retain=1)
# time.sleep(.1)
mqtt_publish_state(client, S['id'], S['enabled'], False)
# time.sleep(.1)
def get_options():
# path = "/data/options.json"
path = os.path.join(simpleschedulerconf.json_folder, "options.dat")
if not os.path.exists(path):
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "options.dat")
with open(path, "r", encoding='utf-8') as read_file:
opt = json.load(read_file)
return opt
def get_css():
global options
path_light = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "light.css")
path_dark = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "dark.css")
css = ""
# try:
with open(path_light, "r", encoding='utf-8') as css_file:
css += css_file.read()
if options['dark_theme']:
with open(path_dark, "r", encoding='utf-8') as css_file:
css += css_file.read()
# except:
# printlog("ERROR: Something went wrong while loading CSS")
return css
def get_enabled_domains():
enabled_domains = []
opt = get_options()
for d in opt['components']:
if opt['components'][d]:
enabled_domains.append(d)
return enabled_domains
def get_switch_html_select_options():
htmlstring: str = ''
switch_list = get_switch_list(get_enabled_domains())
comp = ""
for s in switch_list:
c = s['id'].split('.')
if comp != c[0]:
if comp != "":
htmlstring += '</optgroup>'
comp = c[0]
htmlstring += '<optgroup label="' + comp + '">'
name = s['id'] if s['friendly_name'] == "" else s['friendly_name'] + '(' + s['id'] + ')'
htmlstring += '<option value="' + s['id'] + '">' + name + '</option>'
htmlstring += '</optgroup>'
htmlstring = htmlstring.replace(chr(39), "&#39;")
return htmlstring
def get_json_template(t: str):
t = t.lower()
json_template: str = ''
if t == 'w' or t == 'weekly':
json_template = '{"id":"","name":"","enabled":"1","entity_id":[""],"weekly":{"on_1":"","on_2":"","on_3":"",' \
'"on_4":"","on_5":"","on_6":"","on_7":"","off_1":"","off_2":"","off_3":"","off_4":"",' \
'"off_5":"","off_6":"","off_7":""}} '
if t == 'd' or t == 'daily' or t is None:
json_template = '{"id":"","name":"","enabled":"1","entity_id":[""],"on_tod":"","on_dow":"","off_tod":"",' \
'"off_dow":""} '
if t == 'r' or t == 'recurring':
json_template = '{"id":"","name":"","enabled":"1","entity_id":[""],"recurring":{"on_start":"","on_end":"",' \
'"on_interval":"","off_start":"","off_end":"","off_interval":""},"on_tod":"","on_dow":"",' \
'"off_tod":"","off_dow":""} '
return json_template
def get_sort_list():
sorting = {}
sort_file_path = simpleschedulerconf.json_folder + "sort.dat"
if os.path.exists(sort_file_path):
with open(sort_file_path, "r") as sort_file:
order = json.load(sort_file)
i = 0
for sid in order['id_order']:
sorting[sid] = i
i = i + 1
return sorting
def save_sort_list(data):
idlist = []
for el in data:
idlist.append(data[el])
jsonlist = json.loads('{"id_order":[]}')
jsonlist['id_order'] = idlist
with open(simpleschedulerconf.json_folder + "sort.dat", "w") as sort_file:
json.dump(jsonlist, sort_file)
return True
def get_events_in_html():
events_html = {"events_on": "events_on", "events_off": "events_off", "days_on": "days_on", "days_off": "days_off"}
return events_html
def get_entity_status(e, check):
response = ""
url = simpleschedulerconf.HASSIO_URL + "/states/" + e
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
try:
r = requests.get(url=url, headers=headers, timeout=request_timeout)
result = r.json()
response = str(result['state']).lower()
if check:
altered_response = response
domain = e.lower().split(".")
if domain[0] == 'cover':
altered_response = 'on' if response == "open" else 'off'
if domain[0] == 'climate' and response != 'off':
altered_response = 'on'
response = altered_response
except:
printlog("ERROR: Unable to obtain entity status from Home Assistant")
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}
try:
r = requests.post(url=command_url, data=post_data, headers=headers, timeout=request_timeout)
command = command_url.replace(simpleschedulerconf.HASSIO_URL + "/services/", "")
if opt['debug']: printlog("DEBUG: %s %s" % (command, post_data))
if r.status_code != 200:
printlog("ERROR: Error calling HA API " + str(r.status_code))
except:
printlog("ERROR: Unable to call Home Assistant service")
return True
def call_ha(eid_list, action, passedvalue, friendly_name):
if not isinstance(eid_list, list):
eid_list = {eid_list}
for eid in eid_list:
command = "Turning " + action.upper()
extra = ""
v = ""
value = passedvalue.upper()
domain = eid.split(".")
command_url = simpleschedulerconf.HASSIO_URL + "/services/" + domain[0] + "/turn_" + action
postdata = '{"entity_id":"%s"}' % eid
if action == 'on':
if domain[0] == "light" and value != "":
if value[0] == "A":
v = int(value[1:])
extra = "to %d" % v
elif value.isdigit():
v = int(int(value) * 2.55)
extra = "to " + value + '%'
postdata = '{"entity_id":"%s","brightness":"%d"}' % (eid, v)
if domain[0] == "fan" and value != "":
v = value
extra = "to " + v + '%'
postdata = '{"entity_id":"%s","percentage":"%s"}' % (eid, v)
if domain[0] == "cover":
if value != "":
command_url = simpleschedulerconf.HASSIO_URL + "/services/cover/set_cover_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/cover/open_cover"
command = "Opening"
if domain[0] == "climate" and value != "":
if value[0] == "O":
v = value[1:]
command_url = simpleschedulerconf.HASSIO_URL + "/services/climate/set_temperature"
postdata = '{"entity_id":"%s","temperature":"%s"}' % (eid, v)
command = "Setting"
extra = "temperature to " + v + '°'
else:
if domain[0] == "cover":
command_url = simpleschedulerconf.HASSIO_URL + "/services/cover/close_cover"
command = "Closing"
printlog("SCHED: %s [%s] %s" % (command, friendly_name.get(eid, eid), extra))
call_ha_api(command_url, postdata)
if domain[0] == "climate" and value != "":
if value[0] != "O":
command_url = simpleschedulerconf.HASSIO_URL + "/services/climate/set_temperature"
postdata = '{"entity_id":"%s","temperature":"%s"}' % (eid, value)
call_ha_api(command_url, postdata)
command = "Setting"
extra = "temperature to " + value + '°'
printlog("SCHED: %s [%s] %s" % (command, friendly_name.get(eid, eid), extra))
return True
def is_a_retry_domain(entity):
response = True
if "scene." in entity : response = False
if "script." in entity: response = False
if "automation." in entity: response = False
if "media_player." in entity: response = False
if "camera." in entity: response = False
return response
def get_events_array(s):
s = s.upper().replace(',', ' ').replace(';', ' ').strip()
s = re.sub(' +', ' ', s)
events = s.split(' ')
return events
def evaluate_event_time(s, sunrise, sunset):
event = ""
sunrise_day = ""
sunset_day = ""
if sunrise:
sunrise_day = sunrise.strftime("%d")
if sunset:
sunset_day = sunset.strftime("%d")
today = datetime.now().strftime("%d")
if len(s) > 3:
p = s.upper().split('>')
event = p[0]
operator = "~"
if event[:3] == "SUN":
if event.find('+') != -1: operator = "+"
if event.find('-') != -1: operator = "-"
eventime = event.split(operator)
event = ""
if eventime[0] == "SUNRISE" and sunrise_day == today:
event = sunrise.strftime("%H:%M")
if eventime[0] == "SUNSET" and sunset_day == today:
event = sunset.strftime("%H:%M")
if event != "" and len(eventime) > 1:
hm = event.split(":")
if operator == '+':
event = (datetime(2022, 1, 1, int(hm[0]), int(hm[1])) + timedelta(
minutes=int(eventime[1]))).strftime("%H:%M")
else:
event = (datetime(2022, 1, 1, int(hm[0]), int(hm[1])) - timedelta(
minutes=int(eventime[1]))).strftime("%H:%M")
if event:
hm = event.split(":")
if 0 <= int(hm[0]) < 24 and 0 <= int(hm[1]) < 60:
event = datetime(2022, 1, 1, int(hm[0]), int(hm[1])).strftime("%H:%M") # fix missing leading zeroes
return event
def get_sun(tz, sunrise="", sunset=""):
if tz:
mytimezone = pytz.timezone(tz)
url = simpleschedulerconf.HASSIO_URL + "/states/sun.sun"
headers = {'content-type': 'application/json',
'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
try:
r = requests.get(url=url, headers=headers, timeout=request_timeout)
result = r.json()
response = result['attributes']
sunrise = datetime.fromisoformat(response['next_rising']).astimezone(mytimezone)
sunset = datetime.fromisoformat(response['next_setting']).astimezone(mytimezone)
except:
printlog("ERROR: Unable to obtain sun info from Home Assistant")
return sunrise, sunset
def get_ha_timezone():
response = ""
url = simpleschedulerconf.HASSIO_URL + "/config"
headers = {'content-type': 'application/json', 'Authorization': 'Bearer ' + simpleschedulerconf.SUPERVISOR_TOKEN}
try:
r = requests.get(url=url, headers=headers, timeout=request_timeout)
result = r.json()
response = result['time_zone']
except:
printlog("ERROR: Unable to obtain timezone from Home Assistant")
else:
try:
if not response:
response = os.environ["TZ"]
except:
printlog("ERROR: Unable to obtain timezone from OS")
return response
def get_scheduler_pid():
pid = 0
try:
for process in psutil.process_iter():
if "/simplescheduler/scheduler.py" in process.cmdline():
pid = process.pid
except:
printlog("ERROR: Unable to obtain scheduler PID")
return pid
def printlog(message):
t = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
fullrow = "[%s] %s" % (t, message)
print(fullrow)
if not os.path.exists(simpleschedulerconf.json_folder):
os.makedirs(simpleschedulerconf.json_folder)
logfilepath = os.path.join(simpleschedulerconf.json_folder, "simplescheduler.log")
with open(logfilepath, "a", encoding='utf-8') as logfile:
logfile.write(fullrow + "\n")
def init():
global options
global weekday
global schedulers_list
global ha_timezone
options = get_options()
weekday = \
[
options['translations']['text_sunday'][:2],
options['translations']['text_monday'][:2],
options['translations']['text_tuesday'][:2],
options['translations']['text_wednesday'][:2],
options['translations']['text_thursday'][:2],
options['translations']['text_friday'][:2],
options['translations']['text_saturday'][:2],
options['translations']['text_sunday'][:2]
]
if options['debug']:
printlog(" QUESTION: What do you get if you multiply six by nine?")
printlog(" ANSWER: 42")
schedulers_list = load_json_schedulers()
ha_timezone = get_ha_timezone()
if __name__ == '__main__':
printlog('STATUS: Starting main program')
init()
if options['MQTT']['enabled']:
printlog('STATUS: Starting MQTT')
mqttclient = mqtt.Client(client_id="SimpleScheduler", clean_session=False)
mqttclient.on_connect = on_connect
mqttclient.on_message = on_message
mqttclient.will_set(lwt_topic, payload="offline", qos=0, retain=True)
mqttclient.username_pw_set(options['MQTT']['username'], options['MQTT']['password'])
try:
mqttclient.connect(options['MQTT']['server'], int(options['MQTT']['port']), 60)
except:
printlog("ERROR: MQTT Connection failed")
else:
mqttclient.loop_start()
mqtt_send_config(mqttclient)
# Disable Flask Messages
log = logging.getLogger('werkzeug')
log.disabled = True
flask.cli.show_server_banner = lambda *args: None
# app.logger.disabled = True
printlog('STATUS: Starting WebServer')
app.run(host='0.0.0.0', port=8099, debug=False)
@@ -0,0 +1,42 @@
{
"translations": {
"text_monday": "Monday",
"text_tuesday": "Tuesday",
"text_wednesday": "Wednesday",
"text_thursday": "Thursday",
"text_friday": "Friday",
"text_saturday": "Saturday",
"text_sunday": "Sunday",
"text_ON": "ON",
"text_OFF": "OFF",
"text_save": "Save",
"text_enabled": "Enabled",
"text_device": "Device",
"text_name": "Name"
},
"components": {
"light": true,
"scene": true,
"switch": true,
"script": true,
"camera": true,
"climate": true,
"cover": true,
"vacuum": true,
"fan": true,
"automation": true,
"input_boolean": true,
"media_player": true
},
"MQTT": {
"enabled": false,
"server": "core-mosquitto",
"port": "1883",
"username": "",
"password": ""
},
"max_retry": "3",
"details_uncovered": true,
"dark_theme": false,
"debug": false
}
@@ -0,0 +1,128 @@
import uuid
from datetime import datetime
from time import sleep
import main
command_queue = {}
sunrise = ""
sunset = ""
options = []
if __name__ == '__main__':
main.printlog('STATUS: Starting scheduler')
while True:
seconds = datetime.now().strftime("%S")
if seconds == '00':
options = main.get_options()
try:
max_retry = int(options['max_retry'])
except:
max_retry = 3
if max_retry < 0: max_retry = 3
if max_retry > 5: max_retry = 5
current_time = datetime.now().strftime("%H:%M")
current_dow = datetime.now().strftime("%w")
schedulers_list = main.load_json_schedulers()
friendly_name = main.get_switch_friendly_names()
if current_time == "00:01" or sunset == "" or sunrise == "":
if options['debug']:
main.printlog('DEBUG: Retrieving sunrise and sunset')
sunrise, sunset = main.get_sun(main.get_ha_timezone())
if current_dow == '0':
current_dow = '7'
for s in schedulers_list:
if s['enabled']:
dont_retry = s.get('dontretry',0)
if options['debug']: main.printlog("DEBUG: Parsing [%s]" % s['name'])
week_onoff = s.get('weekly')
if week_onoff:
s['weekly'] = ''
s['on_tod'] = week_onoff['on_' + current_dow]
s['off_tod'] = week_onoff['off_' + current_dow]
s['on_dow'] = current_dow
s['off_dow'] = current_dow
if current_dow in s['on_dow']:
elist = main.get_events_array(s['on_tod'])
for e in elist:
value = ""
p = e.upper().split('>')
t = p[0]
if len(p) > 1:
value = p[1][1:]
event_time = main.evaluate_event_time(t, sunrise, sunset)
if event_time == current_time:
main.printlog("SCHED: Executing ON actions for [%s]" % s['name'])
main.call_ha(s['entity_id'], "on", value, friendly_name )
for entity in s['entity_id']:
if (len(value) > 0 and value[0] != 'O') or not value : # if TemperatureOnly don't add to queue
if not dont_retry and max_retry > 0 :
if main.is_a_retry_domain(entity):
command_queue[uuid.uuid4().hex] = {"entity_id": entity, "sched_id": s['id'],
"state": "on", "value": value,
"countdown": max_retry,"max_retry": max_retry}
if current_dow in s['off_dow']:
elist = main.get_events_array(s['off_tod'])
for e in elist:
value = ""
p = e.upper().split('>')
t = p[0]
if len(p) > 1:
value = p[1][1:]
event_time = main.evaluate_event_time(t, sunrise, sunset)
if event_time == current_time:
main.printlog("SCHED: Executing OFF actions for [%s]" % s['name'])
main.call_ha(s['entity_id'], "off", value, friendly_name )
for entity in s['entity_id']:
if (len(value) > 0 and value[0] != 'O') or not value: # if TemperatureOnly don't add to queue
if not dont_retry and max_retry > 0 :
if main.is_a_retry_domain(entity):
command_queue[uuid.uuid4().hex] = {"entity_id": entity, "sched_id": s['id'],
"state": "off", "value": value,
"countdown": max_retry,"max_retry": max_retry}
sleep(5)
if options['debug']: main.printlog("DEBUG: Max Retry: %d" % max_retry)
if options['debug']: main.printlog("DEBUG: Starting Queue management - Queue length: %d" % len(command_queue))
for key in command_queue.copy():
value = command_queue[key]
entity_status = main.get_entity_status(value['entity_id'], True)
if options['debug']: main.printlog(
"DEBUG: ID:%s | Entity status:%s | Queue item:%s" % (key, entity_status.upper(), value))
if entity_status == 'unavailable':
attempt = 1 + int(value['max_retry']) - int(value['countdown'])
main.printlog("SCHED: [%s] is unavailable. Attempt %d of %d" % (
friendly_name.get(value['entity_id'], value['entity_id']), attempt, int(value['max_retry']) ))
command_queue[key]['countdown'] = int(value['countdown']) - 1
if command_queue[key]['countdown'] <= 0:
main.printlog(
"SCHED: Giving up on [%s]" % friendly_name.get(value['entity_id'], value['entity_id']))
command_queue.pop(key)
else:
if entity_status != value['state']:
attempt = 1 + int(value['max_retry']) - int(value['countdown'])
main.printlog("SCHED: Failed to set [%s]. Retry %d of %d " % (
friendly_name.get(value['entity_id'], value['entity_id']), attempt, int(value['max_retry'])))
main.call_ha(value['entity_id'], value['state'], value['value'], friendly_name )
command_queue[key]['countdown'] = int(value['countdown']) - 1
if command_queue[key]['countdown'] <= 0:
main.printlog(
"SCHED: Giving up on [%s]" % friendly_name.get(value['entity_id'], value['entity_id']))
command_queue.pop(key)
else:
main.printlog("SCHED: [%s] is %s as requested!" % (
friendly_name.get(value['entity_id'], value['entity_id']), entity_status.upper()))
command_queue.pop(key)
if options['debug']: main.printlog("DEBUG: Finished Queue management - Queue length: %d" % len(command_queue))
sleep(1)
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# ==============================================================================
#
# Home Assistant Add-on: SimpleScheduler
#
# ==============================================================================
bashio::log.info "Running scheduler.sh"
python3 /simplescheduler/scheduler.py
@@ -0,0 +1,5 @@
import os
json_folder = "/share/simplescheduler/"
SUPERVISOR_TOKEN = os.environ["SUPERVISOR_TOKEN"]
HASSIO_URL = os.environ.get("HASSIO_URL","http://hassio/homeassistant/api")
@@ -0,0 +1,60 @@
<tr class="config_section"><td colspan="4">MQTT</td></tr>
{% for t in o.MQTT %}
<tr class="config_item">
<td class="config_label">{{ t }}</td>
{% if t=="enabled": %}
<td class="config_input"><input type="checkbox" name="MQTT.{{ t }}" value="1" {{ 'checked ' if o.MQTT[t] else ' ' }} > &nbsp;&nbsp;<em>(need restart)</em></td>
{% else %}
<td class="config_input"><input type="text" value="{{ o.MQTT[t] }}" class="form-control input-sm" name="MQTT.{{ t }}" ></td>
{% endif %}
<td colspan="2">
</tr>
{% endfor %}
<tr class="config_section"><td colspan="4">DOMAINS</td></tr>
{% for t in o.components %}
<tr class="config_item">
<td class="config_label">{{ t }}</td>
<td class="config_input"><input type="checkbox" name="components.{{ t }}" value="1" {{ 'checked ' if o.components[t] else ' ' }} ></td>
<td colspan="2">
</tr>
{% endfor %}
<tr class="config_section"><td colspan="4">TRANSLATIONS</td></tr>
{% for t in o.translations %}
<tr class="config_item">
<td class="config_label">{{ t | replace("text_","") | upper }}</td>
<td class="config_input"><input type="text" value="{{ o.translations[t] }}" class="form-control input-sm" name="translations.{{ t }}" ></td>
<td colspan="2">
</tr>
{% endfor %}
<tr class="config_section"><td colspan="4">Misc</td></tr>
<tr class="config_item">
<td class="config_label">Max retry</td>
<td class="config_input"><input type="text" value="{{ o.max_retry }}" class="form-control input-sm" name="max_retry" ></td>
<td colspan="2">
</tr>
<tr class="config_item">
<td class="config_label">Details uncovered</td>
<td class="config_input"><input type="checkbox" name="details_uncovered" value="1" {{ 'checked ' if o.details_uncovered else ' ' }} ></td>
<td colspan="2">
</tr>
<tr class="config_item">
<td class="config_label">Dark theme</td>
<td class="config_input"><input type="checkbox" name="dark_theme" value="1" {{ 'checked ' if o.dark_theme else ' ' }} ></td>
<td colspan="2">
</tr>
<tr class="config_item">
<td class="config_label">Debug mode</td>
<td class="config_input"><input type="checkbox" name="debug" value="1" {{ 'checked ' if o.debug else ' ' }} ></td>
<td colspan="2">
</tr>
<tr>
<td colspan="4">
<button type="submit" class="btn btn-default bg-success" id="save-config"><span class="mdi mdi-content-save" ></span> {{ o.translations.text_save }} </button>
<button type="button" class="btn btn-secondary" id="close-config"><span class="mdi close-box-outline" ></span> Close </button>
</td>
</tr>
@@ -0,0 +1,35 @@
BODY { background-color: #111111; color: white; }
.table td, .table th { border-top: 1px solid #333; color: white; }
.week_table_header { color: #e1e1e1; }
.badge {font-weight: 400; }
.btn-circle { box-shadow: none; }
.drag_icon { color: white; }
#sidebar {
background-color: #222;
box-shadow: 5px 5px 18px 0px #777;
}
.table-hover>tbody>tr:hover>* {
background-color: rgb(255 255 255 / 15%);
color: white; }
div.row-title P {color: white;}
.week_table_cell { border-bottom: 1px solid #777; }
.text-green { color: lightgreen; }
.text-red { color: #F66; }
#recurring_preview_on,
#recurring_preview_off
{
opacity: 1;
}
#logcontent {color: #000}
@@ -0,0 +1,195 @@
<div>
<div>
<p class="scheduler_id">{{ p.id }}</p>
<form id="edit-form" class="edit-form" action="update" method="post" >
<input type="hidden" name="id" value="{{ p.id }}" >
<div class="edit-section-label"><label>{{ o.translations.text_name }}</label></div>
<div>
<input type="text" name="name" class="form-control input-sm" placeholder="{{ o.translations.text_name }}" value="{{ p.name }}">
</div>
<div>
<label class="checkbox-inline"><input type="checkbox" name="enabled" value="1" {{ 'checked ' if p.enabled else ' ' }} > {{ o.translations.text_enabled }}</label>
&nbsp;&nbsp;&nbsp;
<label class="checkbox-inline"><input type="checkbox" name="dontretry" value="1" {{ 'checked ' if p.dontretry else ' ' }} > Do not retry</label>
</div>
<div class="edit-section-label"><label>{{ o.translations.text_device }}</label></div>
<div class="indexInput">
{% if p.entity_id %}
{% for e in p.entity_id %}
<div id="inputFormRow">
<div class="input-group mb-3">
<select name="entity_id[]" aria-data="{{ e }}" class="form-control entity-dropdown-fix">{{ switchlist|safe }}</select>
<div class="input-group-append"><button id="removeRow" type="button" class="btn btn-danger"><span class="mdi mdi-delete" ></span></button></div>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<div>
<button id="addRow" type="button" class="btn btn-info bg-primary addRow">+</button>
</div>
{% if p.weekly: %}
<input type="hidden" name="type" value="weekly" >
<div class="edit-section-label"><label>{{ o.translations.text_ON }} / {{ o.translations.text_OFF }}</label></div>
<table>
<tr>
<td></td>
<td><span class="badge dowHiglightG ">{{ o.translations.text_ON }}</span></td>
<td><span class="badge dowHiglightR ">{{ o.translations.text_OFF }}</span></td>
</tr>
<tr>
<td>{{ o.translations.text_monday[:2] }}</td>
<td><input type="text" name="on_1" class="form-control input-sm" value="{{ p.weekly.on_1 }}"></td>
<td><input type="text" name="off_1" class="form-control input-sm" value="{{ p.weekly.off_1 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_tuesday[:2] }}</td>
<td><input type="text" name="on_2" class="form-control input-sm" value="{{ p.weekly.on_2 }}"></td>
<td><input type="text" name="off_2" class="form-control input-sm" value="{{ p.weekly.off_2 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_wednesday[:2] }}</td>
<td><input type="text" name="on_3" class="form-control input-sm" value="{{ p.weekly.on_3 }}"></td>
<td><input type="text" name="off_3" class="form-control input-sm" value="{{ p.weekly.off_3 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_thursday[:2] }}</td>
<td><input type="text" name="on_4" class="form-control input-sm" value="{{ p.weekly.on_4 }}"></td>
<td><input type="text" name="off_4" class="form-control input-sm" value="{{ p.weekly.off_4 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_friday[:2] }}</td>
<td><input type="text" name="on_5" class="form-control input-sm" value="{{ p.weekly.on_5 }}"></td>
<td><input type="text" name="off_5" class="form-control input-sm" value="{{ p.weekly.off_5 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_saturday[:2] }}</td>
<td><input type="text" name="on_6" class="form-control input-sm" value="{{ p.weekly.on_6 }}"></td>
<td><input type="text" name="off_6" class="form-control input-sm" value="{{ p.weekly.off_6 }}"></td>
</tr>
<tr>
<td>{{ o.translations.text_sunday[:2] }}</td>
<td><input type="text" name="on_7" class="form-control input-sm" value="{{ p.weekly.on_7 }}"></td>
<td><input type="text" name="off_7" class="form-control input-sm" value="{{ p.weekly.off_7 }}"></td>
</tr>
</table>
{% elif p.recurring: %}
<input type="hidden" name="type" value="recurring" >
<input type="hidden" name="on_tod" id="on_tod" value="{{ p.on_tod }}">
<input type="hidden" name="off_tod" id="off_tod" value="{{ p.off_tod }}">
<div class="edit-section-label"><label>{{ o.translations.text_ON }}</label></div>
<div>
{% for wd in range(1, 8): %}
<label class="checkbox-inline"><input type="checkbox" name="on_dow[]" value="{{ wd }}" {{ 'checked' if wd|string in p.on_dow else ' ' }} > {{ weekday[wd] }}</label>
{% endfor%}
</div>
<div class="form-row">
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-right" ></span></div></div>
<input type="text" value="{{ p.recurring.on_start }}" class="form-control form-control-sm time_validate" name="on_start" id="on_start" placeholder="start">
</div>
</div>
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-left" ></span></div></div>
<input type="text" value="{{ p.recurring.on_end }}" class="form-control form-control-sm time_validate" name="on_end" id="on_end" placeholder="end">
</div>
</div>
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-horizontal" ></span></div></div>
<input type="text" value="{{ p.recurring.on_interval }}"class="form-control form-control-sm interval_validate" name="on_interval" id="on_interval" placeholder="interval">
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-default bg-primary generate_button" aria-valuetext="on" ><span class="mdi mdi-eye" ></span></button>
</div>
</div>
<div id="recurring_preview_on" class="text-green" style="display:none;" ></div>
<div class="edit-section-label"><label>{{ o.translations.text_OFF }}</label></div>
<div>
{% for wd in range(1, 8): %}
<label class="checkbox-inline"><input type="checkbox" name="off_dow[]" value="{{ wd }}" {{ 'checked' if wd|string in p.off_dow else ' ' }} > {{ weekday[wd] }}</label>
{% endfor%}
</div>
<div class="form-row">
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-right" ></span></div></div>
<input type="text" value="{{ p.recurring.off_start }}" class="form-control form-control-sm time_validate" name="off_start" id="off_start" placeholder="start">
</div>
</div>
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-left" ></span></div></div>
<input type="text" value="{{ p.recurring.off_end }}" class="form-control form-control-sm time_validate" name="off_end" id="off_end" placeholder="end">
</div>
</div>
<div class="col-sm-3">
<div class="input-group mb-2">
<div class="input-group-prepend"><div class="input-group-text"><span class="mdi mdi-arrow-expand-horizontal" ></span></div></div>
<input type="text" value="{{ p.recurring.off_interval }}"class="form-control form-control-sm interval_validate" name="off_interval" id="off_interval" placeholder="interval">
</div>
</div>
<div class="col-auto">
<button type="button" class="btn btn-default bg-primary generate_button" aria-valuetext="off" ><span class="mdi mdi-eye" ></span></button>
</div>
</div>
<div id="recurring_preview_off" class="text-red" style="display:none;" ></div>
{% else %}
<input type="hidden" name="type" value="daily" >
<div class="edit-section-label"><label>{{ o.translations.text_ON }}</label></div>
<div>
<input type="text" name="on_tod" class="form-control input-sm" value="{{ p.on_tod }}">
</div>
<div>
{% for wd in range(1, 8): %}
<label class="checkbox-inline"><input type="checkbox" name="on_dow[]" value="{{ wd }}" {{ 'checked' if wd|string in p.on_dow else ' ' }} > {{ weekday[wd] }}</label>
{% endfor%}
</div>
<div class="edit-section-label"><label>{{ o.translations.text_OFF }}</label></div>
<div>
<input type="text" name="off_tod" class="form-control input-sm" value="{{ p.off_tod }}">
</div>
<div>
{% for wd in range(1, 8): %}
<label class="checkbox-inline"><input type="checkbox" name="off_dow[]" value="{{ wd }}" {{ 'checked' if wd|string in p.off_dow else ' ' }} > {{ weekday[wd] }}</label>
{% endfor%}
</div>
{% endif %}
<div style="clear:both;"></div>
<br/>
<div id="edit_buttons">
<button type="submit" class="btn btn-default bg-success float-left" id="save-button"><span class="mdi mdi-content-save" ></span> {{ o.translations.text_save }} </button>
{% if is_new==False: %}
<button type="button" class="btn btn-default bg-warning float-left" id="clone-button" aria-id="{{ p.id }}" ><span class="mdi mdi-content-copy" ></span> {{ o.translations.text_clone }} </button>
<button type="button" class="btn btn-default bg-danger float-right delete-button" aria-id="{{ p.id }}" ><span class="mdi mdi-delete" ></span> </button>
{% endif %}
</div>
</form>
</div>
</div>
<div style="clear:both;"></div>
<br/>
@@ -0,0 +1,406 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Simple Scheduler for HA</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css" >
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.1.min.js" integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.min.js" integrity="sha384-IDwe1+LCz02ROU9k972gdyvl+AESN10+x7tBKgc9I5HFtuNz0wWnPclzo6p9vxnk" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js" integrity="sha256-lSjKY0/srUM9BE3dPm+c4fBo1dky2v27Gdjm2uoZaL0=" crossorigin="anonymous"></script>
<style>
{{ css|safe }}
{% if o.details_uncovered==1 %}
.week_table{ display: table;}
{% endif %}
</style>
</head>
<body>
<div class="wrapper">
<nav id="sidebar">
<button type="button" aria-label="Close" class="btn btn-outline-secondary btn-sm" onclick="toggle_sidebar();">>>></button>
<div id="sidebar-wrapper">
...
</div>
</nav>
<form action="saveconfig" method="get" id="config-form" >
<div class="content" >
<div>
<table class="table table-hover" id="dtable">
<thead class="bg-primary">
<tr>
<th scope="col" colspan="4" >
<span class="titlebar_span" id="title">SimpleScheduler</span>
<span class="titlebar_span" id="show-add"><button type="button" class="btn btn-default bg-white main-color" ><span class="mdi mdi-plus"></span></button></span>
<span class="titlebar_span" id="show-log"><button type="button" class="btn btn-default bg-white main-color" ><span class="mdi mdi-file-document-outline" ></span></button></span>
<span class="titlebar_span" id="show-config"><button type="button" class="btn btn-default bg-white main-color" ><span class="mdi mdi-cog-outline"></span></button></span>
</th>
</tr>
</thead>
<tbody>
{% for s in data %}
<tr data-value="{{ s.id }}" data-order="{{ sort[s.id] }}" style="opacity: {{ '1' if s.enabled else '0.3' }}">
{% if s.weekly %}
<td class="text-center drag_icon fit"> <span class="mdi mdi-calendar-range mdi-24px" ></span> </td>
{% elif s.recurring %}
<td class="text-center drag_icon fit"><span class="mdi mdi-calendar-refresh mdi-24px" ></span></td>
{% else %}
<td class="text-center drag_icon fit"><span class="mdi mdi-calendar-week mdi-24px" ></span></td>
{% endif %}
<td class="text-center fit" >
<button type="button" class="btn btn-default bg-primary edit-button" aria-id="{{ s.id }}" ><span class="mdi mdi-pencil" ></span></button>
&nbsp;
<button type="button" class="btn btn-default bg-primary view-button" aria-id="{{ s.id }}" ><span class="mdi mdi-eye" ></span></button>
</td>
<td class="name_col" >
<div class="row-title"><p data-bs-toggle="tooltip" data-bs-html="true" title="{{ s.id }}" >{{ s.name }}</p></div>
<div class="entities_list">
{% if s.entity_id %}
{% for e in s.entity_id %}
<span class="badge bg-primary" data-bs-toggle="tooltip" data-bs-html="true" title="{{ e }}" >{{ friendlynames[e] }}</span>
{% endfor %}
{% endif %}
</div>
</td>
{% if s.weekly %}
<td>
<div class="week_table w_mode" id="detail_{{ s.id }}">
<div class="week_table_row week_table_header">
<div class="week_table_cell"></div>
{% for wd in range(1, 8): %}
<div class="week_table_cell" style="width: 13.5%;" >
{{ weekday[wd] }}
</div>
{% endfor%}
</div>
<div class="week_table_row text-green week_table_row_bottom_line">
<div class="week_table_cell "><span class="badge dowHiglightG ">{{ o.translations.text_ON }}</span></div>
{% for wd in range(1, 8): %}
<div class="week_table_cell">
{{ format_event(s.weekly['on_'+wd|string], True)|safe }}
</div>
{% endfor%}
</div>
<div class="week_table_row text-red week_table_row_bottom_line">
<div class="week_table_cell "><span class="badge dowHiglightR ">{{ o.translations.text_OFF }}</span></div>
{% for wd in range(1, 8): %}
<div class="week_table_cell">
{{ format_event(s.weekly['off_'+wd|string], False)|safe }}
</div>
{% endfor%}
</div>
</div>
</td>
{% elif s.recurring %}
<td class="event-cell">
<div class="week_table d_mode" id="detail_{{ s.id }}">
<div class="week_table_row ">
<div class="week_table_cell ">
{% if s.on_tod %}
<div class="event-list text-green">
<span> {{ s.recurring.on_start }} <i class="mdi mdi-arrow-left-right-bold" ></i> {{ s.recurring.on_end }} </span>
<span class="mdi mdi-timer-sand" >{{ s.recurring.on_interval }}m</span>
</div>
<!-- <div style="clear:both;" class="event-list text-green">{{ format_event(s.on_tod, True)|safe }}</div> -->
<div style="clear:both;">{{ get_friendly_html_dow(s.on_dow,True)|safe }}</div>
{% endif %}
</div>
<div class="week_table_cell ">
{% if s.off_tod %}
<div class="event-list text-red">
<span> {{ s.recurring.off_start }} <i class="mdi mdi-arrow-left-right-bold" ></i> {{ s.recurring.off_end }} </span>
<span class="mdi mdi-timer-sand" >{{ s.recurring.off_interval }}m</span>
</div>
<!-- <div style="clear:both;" class="event-list text-red">{{ format_event(s.off_tod, False)|safe }}</div> -->
<div style="clear:both;">{{ get_friendly_html_dow(s.off_dow,False)|safe }}</div>
{% endif %}
</div>
</div>
</div>
</td>
{% else %}
<td class="event-cell">
<div class="week_table d_mode" id="detail_{{ s.id }}">
<div class="week_table_row ">
<div class="week_table_cell ">
{% if s.on_tod %}
<div class="event-list text-green">{{ format_event(s.on_tod, True)|safe }}</div>
<div style="clear:both;">{{ get_friendly_html_dow(s.on_dow,True)|safe }}</div>
{% endif %}
</div>
<div class="week_table_cell ">
{% if s.off_tod %}
<div class="event-list text-red">{{ format_event(s.off_tod, False)|safe }}</div>
<div style="clear:both;">{{ get_friendly_html_dow(s.off_dow,False)|safe }}</div>
{% endif %}
</div>
</div>
</div>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</form>
<div class="overlay"></div>
<footer class="footer">
<div class="statusbar">
<p>
<span class="statusbar_span"><span class="mdi mdi-weather-sunset-up" ></span> {{ statusbarinfo.sunrise }}</span>
<span class="statusbar_span"><span class="mdi mdi-weather-sunset-down" ></span> {{ statusbarinfo.sunset }}</span>
<span class="statusbar_span"><span class="mdi mdi-map-clock-outline" ></span> {{ statusbarinfo.timezone }}</span>
<span class="statusbar_span"><span class="mdi mdi-calendar-clock" ></span> {{ statusbarinfo.scheduler }}</span>
<span class="statusbar_span">MQTT: {{ statusbarinfo.mqtt }}</span>
</p>
</div>
</footer>
</div>
<div id="log_wrapper" >
<div>
<pre id="logcontent"></pre>
<button type="button" class="btn btn-secondary" id="closelog"><span class="mdi close-box-outline" ></span> Close </button>
</div>
</div>
<div id="block_background" ></div>
<script>
$( document ).ready(function() {
var $tbody = $("#dtable tbody");
$tbody.sortable({
distance: 5,
delay: 100,
opacity: 0.6,
cursor: 'move',
update: function(e, tr) {
var orderlist = '';
$('tr.ui-sortable-handle').each(function (i) {
$(this).attr('data-order',i);
orderlist=orderlist+'&list['+i+']='+$(this).attr('data-value');
});
$("#sidebar-wrapper").load("sort?"+orderlist);
}
}).disableSelection();
$tbody.find('tr.ui-sortable-handle').sort(function (a, b) {
var tda = parseInt($(a).attr('data-order'));
var tdb = parseInt($(b).attr('data-order'));
return tda > tdb ? 1
: tda < tdb ? -1
: 0;
}).appendTo($tbody);
});
function toggle_sidebar(){
$("#sidebar").hide();
$("body").css('overflow','auto');
}
$(document).on('click', '.edit-button', function () {
v=$(this).attr('aria-id');
$("#sidebar-wrapper").html('');
$("#sidebar").show();
$("body").css('overflow','hidden');
$("#sidebar-wrapper").load("edit?id="+v, function() {
$(".entity-dropdown-fix").each(function( t ) {
this.value = $(this).attr('aria-data');
});
});
});
$(document).on('click', '.view-button', function () {
var v=$(this).attr('aria-id');
var t="#detail_"+v;
if ($(t).css('display')=="table") {
$(t).css('display','none');
}else{
$(t).css('display','table');
}
});
$(document).on('click', '#show-add', function () {
$("#sidebar-wrapper").html('');
$("#sidebar").show();
$("#sidebar-wrapper").load("new");
});
$(document).on('click', '#show-config', function () {
$("#dtable tbody").load("config");
});
$(document).on('click', '#close-config', function () {
window.location.href="main";
});
$(document).on('click', '#save-config', function () {
var f = document.getElementById("config-form");
$("input:checkbox:not(:checked)").each( function () {
$(this).prop('checked',true);
$(this).attr('value','0');
})
f.submit();
});
$(document).on('click', '#clone-button', function () {
var id=$(this).attr('aria-id');
window.location.href ='clone?id='+id;
});
$(document).on('click', '.delete-button', function () {
var id=$(this).attr('aria-id');
if (window.confirm("Are you sure?")) {
window.location.href ='delete?id='+id;
}
});
$(document).on('click', '.img-add-new', function () {
v=$(this).attr('aria-id');
$("#sidebar-wrapper").html('');
$("#sidebar").show();
$("#sidebar-wrapper").load("edit?id="+0+"&type="+v);
});
$(document).on('click', '.generate_button', function () {
action=$(this).attr('aria-valuetext');
r = document.getElementById('recurring_preview_'+action);
generate_recurrent(action);
if ($(r).css('display')=="none") {
$(r).css('display','block');
}else{
$(r).css('display','none');
}
});
$(document).on('submit', '.edit-form', function () {
var f = document.getElementById("edit-form");
var t = f.type.value;
if (t=="recurring") {
generate_recurrent("on");
generate_recurrent("off");
}
});
$(document).on('keyup', '.time_validate', function () {
obj=$(this);
field=obj[0];
var timeREGEX = /^[0-9]{2}:[0-9]{2}$/;
if (timeREGEX.test(field.value) || field.value=="") {
field.classList.remove("is-invalid");
document.getElementById("save-button").disabled=false;
} else {
field.classList.add("is-invalid");
document.getElementById("save-button").disabled=true;
}
});
$(document).on('keyup', '.interval_validate', function () {
obj=$(this);
field=obj[0];
var intervalREGEX = /^[0-9]{1,3}$/;
if (intervalREGEX.test(field.value) || field.value=="") {
document.getElementById("save-button").disabled=false;
field.classList.remove("is-invalid");
} else {
document.getElementById("save-button").disabled=true;
field.classList.add("is-invalid");
}
});
$(document).on('click', '#removeRow', function () {
$(this).closest('#inputFormRow').remove();
});
$(document).on('click', '#show-log', function () {
$("#logcontent").load("log" , function() {
$('#logcontent').scrollTop( $('#logcontent')[0].scrollHeight );
});
$("#log_wrapper").css('display','block');
$("#block_background").css('width','100%');
$("#block_background").css('height','100%');
$("#block_background").css('background-color','#777777e6');
});
$(document).on('click', '#closelog', function () {
$("#log_wrapper").css('display','none');
$("#block_background").css('width','0%');
$("#block_background").css('height','0%');
$("#block_background").css('background-color','#77777700');
});
function generate_recurrent(action) {
r = document.getElementById('recurring_preview_'+action);
r.innerHTML = "";
start=document.getElementById(action+'_start').value;
end=document.getElementById(action+'_end').value;
interval=document.getElementById(action+'_interval').value;
var startDate= new Date("2022-01-01T" + start +":00Z");
var endDate = new Date("2022-01-01T" + end +":00Z");
s = startDate.getTime();
e = endDate.getTime();
if (s<e) {
while ( s<=e ) {
out = new Date(s)
t = out.toUTCString().substring(17, 22)
if (t.length>0) r.innerHTML += t + " ";
s = s + (interval * 60000);
}
} else {
r.innerHTML = ""
}
document.getElementById(action+'_tod').value=r.innerText;
}
var intervalcheck = window.setInterval(function(){
$.get("dirty", function( r ) {
if (r=="1") location.reload();
});
}, 3000);
$(document).on('click', '#addRow', function () {
var html = '';
html += '<div id="inputFormRow">';
html += '<div class="input-group mb-3">';
html += '<select name="entity_id[]" class="form-control">{{ switchlist|safe }}</select>';
html += '<div class="input-group-append"><button id="removeRow" type="button" class="btn btn-danger"><span class="mdi mdi-delete" ></span></button></div>';
html += '</div>';
html += '</div>';
$("#info").html('Clic ADD');
$('.indexInput').append(html);
});
</script>
</body>
</html>
@@ -0,0 +1,418 @@
:root {
--maincolor: #007bff;
}
H5 {margin-bottom: 0 ; line-height: 14px; }
.main-color { color: var(--maincolor)!important; }
div.content { margin-bottom: 2em; }
.table td, .table th {
vertical-align: top;
border-bottom: 1px solid var(--maincolor);
border-top: none;
}
THEAD {
line-height: 3em;
}
#sidebar {
display:none;
min-width: 250px;
max-width: 30%;
height: 100%;
position: fixed;
top: 0;
right: 0;
padding: 1em;
z-index: 9999;
background-color: rgba(255,255,255,0.98);
box-shadow: 5px 5px 18px 0px #000;
overflow-y: auto;
}
.edit-form > div {
margin: 1em 0;
}
.form-row span.mdi { font-size: 14px; }
.btn-default { color: white;}
.dowIcon {
border: 0px solid ;
border-radius: 20px;
background: grey;
color: white;
font-size: 0.8rem;
width: 1.8rem;
height: 1.8rem;
line-height: 1.8rem;
text-align: center;
margin-right: 0.1em;
display: inline-block;
text-shadow: 1px 1px 1px #333;
box-shadow: inset -3px -4px 6px #00000077;
}
.dowHiglightR { background: red ; }
.dowHiglightG { background: green ; }
.icon-space {width: 32px;}
.text-green {color:green;}
.text-red {color:red;}
.text-white {color:white;}
.bg-primary {
color:white;
background-color: #03a9f4!important;
}
.titlebar_span {
font-size: 1.25em;
margin-right: 1em;
text-shadow: 1px 1px 3px #000;
}
.titlebar_span button {
box-shadow: 2px 2px 2px 0px #00000077;
font-size: 1.25em;
line-height: 1em;
}
div.row-title P {
font-size: 1.2em;
line-height: 1.2em;
margin-bottom: 0;
}
div.edit-section-label {
width: 100%;
border-bottom: 1px solid #777;
margin-top: 1em;
}
div.edit-section-label label{
line-height: 1em;
font-weight: bold;
}
.btn-circle.btn-xl {
width: 70px;
height: 70px;
padding: 10px 16px;
border-radius: 35px;
font-size: 24px;
line-height: 1.33;
opacity: 0.9;
}
.btn-circle {
width: 30px;
height: 30px;
padding: 6px 0px;
border-radius: 15px;
text-align: center;
font-size: 12px;
line-height: 1.42857;
background: navy;
color: white;
box-shadow: 2px 2px 10px 0px #777;
}
.floating-bottom-right {
position: fixed;
bottom: 5%;
right: 5%;
}
td.name_col{
width: 25%;
max-width: 400px;
}
.event-list > span {
font-size: 1rem;
margin-right: 1rem;
line-height: 1.6 rem;
float:left;
}
span.event-type-b {
font-size: 1rem;
color: #d39e00;
margin-left: 0.2rem;
}
span.event-type-t {
font-size: 1rem;
color: #8b442b;
margin-left: 0.1rem;
}
span.event-type-to {
font-size: 1rem;
color: #9c27b0;
margin-left: 0.1rem;
}
span.event-type-p {
font-size: 1rem;
color: #2196f3;
margin-left: 0.1rem;
}
footer {
position: fixed;
bottom: 0;
right: 0;
margin:0;
padding: 0;
width: 100%;
height: 2em;
line-height: 2em;
font-size: 1em;
background-color: grey;
color: black
}
footer .statusbar {
margin-left: 1em;
}
.statusbar_span {margin-right: 2em; }
.statusbar_span .mdi {font-size:1.2em; }
#showlog {cursor:pointer;}
#log_wrapper {
z-index: 9999;
display: none;
position: fixed;
top: 10%;
left: 10%;
width: 80%;
height: 80%;
background-color: white;
padding: 0;
margin: 0;
border: 1px solid #ccc;
overflow: hidden;
box-shadow: 5px 5px 15px 0px #777;
}
#log_wrapper > div {
position: relative;
width: 98%;
height: 96%;
margin: 1%;
padding: 0;
overflow: hidden;
}
#logcontent {
font-size: 1em;
overflow: scroll;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
#closelog {
position: absolute;
top: 1em;
right: 3em;
}
#block_background {
position: fixed;
top: 0;
bottom: 0;
height: 0;
width: 0;
background-color: #77777700;
z-index: 9998;
transition: background-color 0.25s;
}
.week_table {
display: none;
width: 100%;
margin: 0px 0px 1em;
}
.week_table_row {
display: table-row;
}
.week_table_header .week_table_cell {
border-bottom: 1px solid #777 !important;
}
.week_table_cell {
display: table-cell;
padding: 3px 10px;
border: none;
text-align: left;
border-bottom: 1px solid #ddd;
}
.week_table_cell > span {
display: block;
}
.d_mode .week_table_cell {
border-bottom: none;
width: 50%;
}
.week_table.d_mode {
margin: 0px 0px;
}
.img-add-new {
display: block;
width: 90%;
margin: 1em auto;
height: auto;
min-height: 3em;
box-shadow: 2px 2px 5px -2px #777;
cursor: pointer;
line-height: 3em;
padding: 0em 1em;
}
.img-add-new > SPAN {
vertical-align: middle;
}
.badge{
color: white;
text-shadow: 1px 1px 1px #333;
}
.hidden_cell {display: none; }
.drag_icon {cursor: move;}
.fit {
white-space: nowrap;
width: 1%;
}
p.scheduler_id {
position: absolute;
top: 1.5em;
right: 2em;
margin: 0;
padding: 0;
opacity: .5;
}
.col-sm-3 {margin-right: 0.5em; }
.col-sm-3:last-child {margin-right: 0; }
.form-row {
display: flex;
flex-wrap: nowrap;
}
#recurring_preview_on,
#recurring_preview_off {
opacity: 0.75;
}
#edit_buttons > button:first-child {
margin-right: 1em;
}
.float-left { float:left}
.float-right { float:right}
tr.config_section {
background: #777;
font-weight: bold;
color: white;
}
tr.config_section td {
border: none;
}
tr.config_item td {
border: none;
}
#config-form .form-control {
padding: 0.1rem 0.75rem;
}
tr.config_item td.config_input,
tr.config_item td.config_label {
width: 10%;
min-width: 150px;
padding: 0.25em 1em;
vertical-align: middle;
}
@media screen and (min-width: 1281px) {
html { font-size: 10pt; }
}
@media screen and (max-width: 1280px) {
html { font-size: 9pt; }
.form-row span.mdi { font-size: 10px; }
#sidebar { max-width: 50%; }
}
@media screen and (max-width: 800px) {
html { font-size: 8pt; }
#dtable TD {
display: block;
text-align: center;
border: none !important;
}
#dtable TD:nth-child(2) { }
#dtable TR {
border-bottom: 1px solid var(--maincolor);
}
#config-form .input-sm {text-align:center;}
td.name_col {width: 100%; max-width: none;}
.event-list > span { float: none; display: inline-block; }
#sidebar { max-width: 90%; }
tr.config_item td.config_input,
tr.config_item td.config_label {
width: 100%;
}
.fit {
white-space: normal;
width: auto;
}
.week_table { margin-bottom: 1em; }
.d_mode .week_table_cell { display: block; text-align: center; width: 100%; }
.week_table.d_mode {margin-bottom: 0em; }
.event-list {
width: 80%;
margin: auto;
}
}
@@ -0,0 +1,4 @@
<div class="edit-section-label"><label>Select type:</label></div>
<div id="add-d" class="img-add-new bg-primary" aria-id="D" ><span class="mdi mdi-calendar-week mdi-24px" ></span> Daily</div>
<div id="add-w" class="img-add-new bg-primary" aria-id="W" ><span class="mdi mdi-calendar-range mdi-24px" ></span> Weekly</div>
<div id="add-r" class="img-add-new bg-primary" aria-id="R" ><span class="mdi mdi-calendar-refresh mdi-24px" ></span> Recurring</div>