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
+118
View File
@@ -0,0 +1,118 @@
**Version 2.11**
- Fix "Allow non admin users to view panel" (#74)
**Version 2.1**
- Fix "Corrupted JSON files crash the addon" (#123)
- Fix "Setting hours to 24 crash the addon" (#124)
**Version 2.0.50 (beta)**
- Fix MQTT Switches unavailable on restart (#111)
- Fix error if entity is null (#110)
**Version 2.0.48 (beta)**
- create json folder if missing
- enable flask logs
**Version 2.0 (beta)**
- Rewritten from scratch in Python
- Complete reengineering of the docker structure
- New feature: Configuration moved to frontend (you need to set option again!)
- New feature: Recurring type scheduler (from - to - every)
- New feature: Added "Do not retry" flag
- New feature: Added "Clone" button
- New MQTT Engine
- Auto respawn processes (frontend and scheduler) in case of crash
- Full UTF-8 support in scheduler name (include regional, mathematical, symbols and emoji)
- Log moved to frontend
- Log improvement (more clear and more detalied)
- Improvement of "Max Retry" option behavior
- Avoid queuing of some domains (script, scene, ecc)
- Massive bugfix
**Version 0.64**
- Improvement: Set absolute value for light (#76)
- Improvement: MQTT Switches become unavailable if addon is not running
- Fix MQTT Switches duplicate with an ending 2 (#80)
**Version 0.62**
- Timezone issue (#75)
- Added some fixes to allow installation on not-supervised env (thanks to @micw)
**Version 0.61**
- Allow non admin users to view panel (#74)
- Fix "Missing translations" (#69)
**Version 0.60**
- Switch Docker image to HA Debian base
- Complete reengineering of the docker structure
- Fix "returned a non-zero code" issue in supervised installation (#65)
- Added support for VACUUM
**Version 0.50**
- New feature: Retry unavailable entities
- New feature: enable/disable schedulers in frontend (through MQTT)
- Improvement: Set fan percent speed
- Fixed bug in sorting (#61)
- Log improvement
- Several bugfixes
**Version 0.40**
- New feature: week-based scheduler
- Improvement: added dark theme
- Improvement: added Fan
- Updated UI
- Several bugfixes
**Version 0.35**
- Fix Temperature Only on multiple climate issue (#53)
**Version 0.34**
- Improvement: Change climate temperature without turning it on (#51)
- Update material icons to latest version
**Version 0.33**
- Improvement: manage commas as separator in time list (#46)
- Improvement: Added Cover (with position) (#49)
**Version 0.32**
- Fix "too many time" visualization issue (#48)
**Version 0.31**
- Fix single quote issue (#44)
- Javascript optimization
**Version 0.30**
- Can set brightness to Lights
- Can set temperature in Climates
- Can add positive/negative offset to sunset/sunrise
- Embed style and script to avoid cache issues
- Bugfix
**Version 0.22**
- Fixed a bug in scheduler
**Version 0.21**
- Fixed visualization of UTF-8 chars (issue #11)
**Version 0.20**
- Add Name to scheduling
- Can add more entities in one scheduler
- Can add multiple time in one scheduler
- Can drag rows to sort them
- Changed edit from row to sidebar
- Little graphic rewiew
- Code improvements
**Version 0.16**
- Fix option.json issue
- Removed table sorting
**Version 0.15**
- Fix Timezone Issue
**Version 0.14**
- Fix the “permission denied” issue
- Sortable Columns
- Added status bar with scheduler engine status
- Added “debug option” to see all the error messages
- various bugfixes/improvements
+21
View File
@@ -0,0 +1,21 @@
ARG BUILD_FROM
FROM ${BUILD_FROM}
# Update Packages
RUN apt-get update -y && apt upgrade -y
# Setup base
RUN apt-get install -y \
coreutils \
wget \
curl \
python3 \
python3-dev \
python3-pip
# Install python modules
RUN pip3 install Flask requests paho-mqtt pytz psutil
# Copy root filesystem
COPY rootfs /
+81
View File
@@ -0,0 +1,81 @@
# SimpleScheduler
A Home Assistant AddOn to schedule switches,lights and other entities on a weekly base in a visual way without coding.\
You can keep all the schedules in one place and add/change in a few clicks, even in your mobile app.
![SimpleScheduler](https://raw.githubusercontent.com/arthurdent75/SimpleScheduler/master/asset/logo.png)
### Installation
Add the repository and then the addon by clickin on the badges:\
[<img src="https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg">](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Farthurdent75%2FSimpleScheduler) \
[<img src="https://my.home-assistant.io/badges/supervisor_addon.svg">](https://my.home-assistant.io/redirect/supervisor_addon/?addon=00185a40_simplescheduler) \
If something goes wrong, you can install it manually.\
You can add the URL of this page in your "add-on store" as a new repository:\
*Settings > Add-ons > ADD-ON STORE button in bottom right corner > three dots in top right corner > Repositories*\
Click *Check for updates* and you will find the add-on "Simple Scheduler" listed.
If you are not using a supervised installation, you can run the addon as a standalone docker.
Take a look here: [docker_install.MD](https://github.com/arthurdent75/SimpleScheduler/blob/master/asset/docker_install.MD "docker_install.MD")
### Type of scheduler
There are three kind of scheduler:
- **Daily**: you can set a ON/OFF time (or a list of them) and than you choose on which weekdays you want to enable them.
- **Weekly**: you can set a different ON/OFF time (or list of them) for every single day of the week.
- **Recurring**: you can set a recurring ON/OFF (FROM hh:mm TO hh:mm EVERY n MINUTES) and choose on which weekdays you want to enable them.
"*A picture is worth a thousand words*", so if you take a look at the screenshot it will be even more clear.
### How to use it
The add-on is very easy and intuitive (or, at least, that's what I hope)\
Once installed, open the GUI, click on the round plus button in the bottom right and choose your schedule type.\
Choose one or more entity from the dropdown, fill the ON time (in 24 hours format with leading zero, as suggested) and select the weekdays. Do the same for the OFF time and click "save".
That's it!
### Advanced Feature
If you need more advanced feature:
- You can set multiple time in the same scheduler: just write them in the field separated by spaces.
- You can use the words **sunrise** and **sunset** and also add an offset (in minutes) to it if you need (e.g: **sunrise+30** or **sunset-60** )
- You can set the percent brightness of a light. Write **16:30>B30** turn on the light at 30%
- You can set the absolute brightness of a light. Write **16:30>BA30** turn on the light at 30
- You can set the temperature of a climate. Write **16:30>T22.5** to turn on the climate and set the temperature to 22.5°
- You can set the temperature of a climate without turning it on. Write **16:30>TO22.5** to set the temperature to 22.5°
- You can set the position of a cover. Write **16:30>P25** will set the cover at 25%
- You can set the fan speed. Write **16:30>F25** will turn on the fan at 25%
- Brightess/Temperature/Position/Speed only work in the "TURN ON" section (obviously)!
- It's not mandatory to add both ON and OFF time. You can leave one of them empty if you don't need it. For example, you want to turn off a light every day at 22:00, but you don't need to turn it on.
- You can also choose to disable a schedule: the schedule will stay there, but it will not be executed until you will enable it back
- You can **drag the rows to sort them**, so you can keep them organized as you like!
Look at the picture above to see all this things in actions (and combined!).
### Frontend switch to enable/disable (with MQTT)
If you want to enable/disable schedulers in frontend and/or automation, you can achieve that through MQTT.
This feature is disabled by default, because it require a working MQTT server (broker) and Home Assistant MQTT integration.
Take a look at the [MQTT.MD](https://github.com/arthurdent75/SimpleScheduler/blob/master/asset/MQTT.MD "MQTT.MD") file to know more.
### Retry on unavailable
By default, SimpleScheduler will retry 3 times if an entity is unavailable. The first retry attempt happens after 5 second, than every minutes. You can change the numbers of retries in the addon options.
### Hidden scheduler details
When you have a lot of schedulers the view can become messy. As a default, all the scheduler details are hidden, so you can have a clear look.
You can toggle the visibility with the *eye* icon near the scheduler name.
If you prefer to have all the schedule always visible, you can easily achive that by setting **details_uncovered: true** in the addon configuration
### Dark theme
If you prefer a dark theme, you can activate it in the addon configuration by setting **dark_theme: true**
### Translation
The default text language is English. They are very few words.
If you want to translate them, you just need to take a look at the configuration section of the addon.
Rewrite the words you would like to have in your language and restart the addon.
For the weekdays, as you can easily understand, only the first two chars are used.
### Two words about the stored data
Every schedule (or row, if you prefer) is a JSON file stored in the [share/simplescheduler] folder under the SAMBA share.
This way the data can "survive" to an addon upgrade or reinstallation.
You can easily backup and restore them in case of failure. In the same way, you can (accidentally?) delete them. So be aware of that.
### Last but not least
If you want to convince me to stay up at night to work on this, just <a target="_blank" href="https://www.paypal.com/donate/?hosted_button_id=8FN58C8SM9LLW">buy me a beer 🍺</a> \
You may say that regular people needs coffee to do that. Well, I'm not a regular person.
+61
View File
@@ -0,0 +1,61 @@
# MQTT Configuration
To enble/disable schedulers in the frontend and/or in an automation, you can use this feature. This allow to create dynamic switches entities in Home Assistant for each scheduler.
The switch is in this form:
`switch.simplescheduler_id_scheduler_name`
Switch is created within a minute of the creation of a new scheduler, and removed as you delete a scheduler.
To use the feature you need three things:
1. a MQTT server (also named *broker*)
2. enable the MQTT integration in Home Assistant
3. activate the feature in Simplescheduler
It's less complicated than it seems!
## If you already have MQTT in Home Assistant
Set up Simplescheduler to point to your MQTT broker and set *Enabled* to *true*
MQTT:
enabled: true
server: your_mqtt_server_address
port: '1883'
username: your_mqtt_user
password: your_mqtt_password
If auth is not used, leave the *username* and *password* fields empty.
You also need the MQTT Integration in your Home Assistant with discovery enabled. If you use MQTT you should already have it but if you don't, take a look at step 2 in the next section.
## If you don't have MQTT configured
You have to follow three simple steps:
#### 1. Install the addon "Mosquitto broker"
Install the addon "Mosquitto broker", start it and enable "Start on boot".
No configuration needed.
![](https://raw.githubusercontent.com/arthurdent75/SimpleScheduler/master/asset/mqtt_addon.png)
#### 2. Install the MQTT integration
In ***Configuration > Device & Services*** add the MQTT integration.
If you succesfully complete the prevous task, Home Assistant should automatically discover the integration and notify you to add it.
![](https://raw.githubusercontent.com/arthurdent75/SimpleScheduler/master/asset/mqtt_integration.png)
Click on CONFIGURE and leave all the default, but be sure to enable auto discovery
![](https://raw.githubusercontent.com/arthurdent75/SimpleScheduler/master/asset/mqtt_discovery.png)
#### 3. Configure Simpleschedule
Simplescheduler is already pre-configured to use the Mosquitto Addon, so leave the *server* and the *port* parameter as default.
Set *enabled* to *true* and add the username and the password that you use to login in Home Assistant.
MQTT:
enabled: true
server: core-mosquitto
port: '1883'
username: your_homeassistant_user
password: your_homeassistant_password
After that, restart the addon.
If, for some reason, you don't want to write your HA credentials in the addon, you can use the credential that the MQTT integration use to talk to the MOSQUITTO addon.
The username is always *homeassistant* while the password is randomly generated during the addon installation. You can retrieve it in the MQTT integration by clicking on "RE-CONFIGURE MQTT", reveal the password with the eye icon and then copy the password.
+31
View File
@@ -0,0 +1,31 @@
# Install this addon as a standalone docker
## Build the docker container
There is no ready-to-use docker image at the moment, so you have to build it. \
After you clone the repository, cd to the folder that contain the Dockerfile and run:
```
docker build . -t mysimplescheduler:latest --build-arg BUILD_FROM="ghcr.io/hassio-addons/debian-base/amd64:5.3.1"
```
For architectures other than amd64, just replace amd64 with one of the following: aarch64 , armhf , armv7 , i386.
## Create a token to access homeassistant
Log in to home assistant, go to your user profile, create a long running access token and copy it.
## Create a folder to store persistent data
You need to create a folder where the addon will save the data. You need to specify tha path in the docker run command
## Run the docker image
Run:
```
docker run -d -e SUPERVISOR_TOKEN=previously-created-token -e HASSIO_URL=http://your-hass-url:port/api -v /path/to/persistent/data:/share/simplescheduler -p 8099:8099 mysimplescheduler:latest
```
You can access the scheduler now at `http://localhost:8099`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

+10
View File
@@ -0,0 +1,10 @@
---
build_from:
aarch64: ghcr.io/hassio-addons/debian-base/aarch64:5.3.1
amd64: ghcr.io/hassio-addons/debian-base/amd64:5.3.1
armhf: ghcr.io/hassio-addons/debian-base/armhf:5.3.1
armv7: ghcr.io/hassio-addons/debian-base/armv7:5.3.1
i386: ghcr.io/hassio-addons/debian-base/i386:5.3.1
codenotary:
base_image: notary@home-assistant.io
signer: notary@home-assistant.io
+21
View File
@@ -0,0 +1,21 @@
---
version: "2.11"
name: Simple Scheduler
description: Simple timer on weekly base
slug: simplescheduler
url: https://github.com/arthurdent75/SimpleScheduler
codenotary: notary@home-assistant.io
arch:
- aarch64
- amd64
- armhf
- armv7
- i386
init: false
homeassistant_api: true
ingress: true
panel_admin: false
panel_title: Scheduler
panel_icon: mdi:calendar-clock
map:
- share:rw
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+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>