update
This commit is contained in:
+10
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 + '°</span>'
|
||||
else:
|
||||
extra = '<span class="event-type-t"><i class="mdi mdi-power" aria-hidden="true"></i>' + v + '°</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), "'")
|
||||
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
@@ -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 ' ' }} > <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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user