update
This commit is contained in:
@@ -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
|
||||||
@@ -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 /
|
||||||
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
#### 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click on CONFIGURE and leave all the default, but be sure to enable auto discovery
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### 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.
|
||||||
@@ -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 |
@@ -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
|
||||||
@@ -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
@@ -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