#!/usr/bin/python3 -u

import RPi.GPIO as GPIO
import signal
import sys
import time
import datetime
import os
import urllib.request
import paho.mqtt.client as mqtt
import paho.mqtt.subscribe as subscribe
import json
import requests
from dateutil.parser import parse
from datetime import timezone
from datetime import timedelta
import threading

import dbus
import dbus.mainloop.glib
from gi.repository import GObject
import NetworkManager
from operator import itemgetter
import ephem
import socket
import configparser
import ntplib
from subprocess import PIPE, run, CalledProcessError
import argparse
import uuid
from posix_ipc import MessageQueue # apt-get install python3-posix-ipc
import re
from random import randint

program_name = "ETHA Light Switch"
program_version = "1.0"

def format_epoch_tz(epoch_time):
	if epoch_time >= 0:
		try:
			return time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime(epoch_time))
		except OverflowError as e:
			log_message("Error in date: " + str(e) + "!")
			return"-"
	else:
		return "-"

def log_message(message):
	print("[" + ("" if (satellite == None) else satellite) + "] - " + format_epoch_tz(time.time()) + " - " + message)

DEBUG = False
def log_debug(message):
	if DEBUG:
		log_message("DEBUG: " + message)

argument_parser = argparse.ArgumentParser(description = program_name + " " + program_version)
argument_parser.add_argument("-s", "--satellite", help = "name of a satellite section in the configuration file")
argument_parser.add_argument("-d", "--config-dir", help = "directory of the configuration files (default: '/etc/etha')", default = "/etc/etha")
argument_parser.add_argument("--version", action = "version", version = "%(prog)s " + program_version)
args = vars(argument_parser.parse_args())
satellite = args["satellite"]
config_dir = re.sub("//+", "/", args["config_dir"] + "/")

config_file = config_dir + "etha-light-switch.conf"
if not os.path.isfile(config_file):
	log_message("Configuration file not found: '" + config_file + "'!")
	sys.exit(2)

config = configparser.ConfigParser()
config.read(config_file)
separate_clock_rules = True
default_flash_time_fallback = 15
shift_solar_times_fallback = False
try:
	default_flash_time = config.getint("common", "default_flash_time", fallback = default_flash_time_fallback)
	shift_solar_times = config.getboolean("common", "shift_solar_times", fallback = shift_solar_times_fallback)
	clock_rules_filename = config.get("common", "clock_rules_filename", fallback = "etha-light-switch")
	invert_nm_state_led = config.getboolean("common", "invert_nm_state_led", fallback = False)

	mqtt_topic_group = config.get("mqtt", "mqtt_topic_group", fallback = None)
	if not mqtt_topic_group:
		mqtt_topic_group = None
	# mqtt_topic_group and group_name must both be defined or undefined
	if mqtt_topic_group == None:
		group_name = config.get("common", "group_name", fallback = None)
		if not group_name:
			group_name = None
		if group_name != None:
			mqtt_topic_group = config["mqtt"]["mqtt_topic_group"]
	else:
		group_name = config["common"]["group_name"]

	button_send = False
	button_receive = False
	if satellite == None:
		light_name = config["common"]["light_name"]
		button_send = config.getboolean("common", "button_send", fallback = False)
		
		mqtt_topic_identification = config["mqtt"]["mqtt_topic_identification"]

		gpio_light_switch_led = config.getint("gpio", "gpio_light_switch_led", fallback = 0)
		gpio_nm_state_led = config.getint("gpio", "gpio_nm_state_led", fallback = 0)
		gpio_relay = json.loads(config.get("gpio", "gpio_relay", fallback = "0"))
		gpio_button = config.getint("gpio", "gpio_button", fallback = 0)
	else:
		light_name = config[satellite]["light_name"]
		default_flash_time = config.getint(satellite, "default_flash_time", fallback = default_flash_time)
		shift_solar_times = config.getboolean(satellite, "shift_solar_times", fallback = shift_solar_times)
		test_clock_rules_filename = config.get(satellite, "clock_rules_filename", fallback = clock_rules_filename)
		if test_clock_rules_filename == clock_rules_filename:
			separate_clock_rules = False
		clock_rules_filename = test_clock_rules_filename
		button_receive = config.getboolean(satellite, "button_receive", fallback = False)

		mqtt_topic_identification = config[satellite]["mqtt_topic_identification"]

		gpio_light_switch_led = config.getint(satellite, "gpio_light_switch_led", fallback = 0)
		gpio_nm_state_led = config.getint(satellite, "gpio_nm_state_led", fallback = 0)
		gpio_relay = json.loads(config.get(satellite, "gpio_relay", fallback = "0"))
		gpio_button = config.getint(satellite, "gpio_button", fallback = 0)
		
	latitude = config.getfloat("coordinates", "latitude", fallback = 0)
	longitude = config.getfloat("coordinates", "longitude", fallback = 0)

	time_server = config.get("time", "time_server", fallback = "pool.ntp.org")
	
	mqtt_broker = config.get("mqtt", "mqtt_broker", fallback = None)
	if not mqtt_broker:
		mqtt_broker = None
	mqtt_broker_scheme = config.get("mqtt", "mqtt_broker_scheme", fallback = "tcp")
	if mqtt_broker_scheme == "tcp":
		mqtt_broker_port = config.getint("mqtt", "mqtt_broker_port", fallback = 1883)
	else:
		mqtt_broker_port = config.getint("mqtt", "mqtt_broker_port", fallback = 8883)
	mqtt_username = config.get("mqtt", "mqtt_username", fallback = "")
	mqtt_password = config.get("mqtt", "mqtt_password", fallback = "")
	mqtt_topic_base = config["mqtt"]["mqtt_topic_base"]
except KeyError as key_error:
	log_message("Configuration parameter not found in '" + config_file + "': " + str(key_error))
	sys.exit(2)
except ValueError as value_error:
	log_message("Value error in '" + config_file + "': " + str(value_error))
	sys.exit(2)
log_message(program_name + " started for '" + light_name + "'!")

satellite_message_queue = []
satellite_message_queue_separate_clock_rules = []
satellite_message_queue_separate_clock_rules_condition = False
satellite_this_message_queue = None
if satellite is None:
	for section in config.sections():
		if section not in ["common", "coordinates", "time", "mqtt", "gpio"]:
			add_separate = (config.get(section, "clock_rules_filename", fallback = clock_rules_filename) == clock_rules_filename)
			satellite_message_queue_separate_clock_rules_condition |= add_separate
			satellite_message_queue.append(MessageQueue("/etha-light-switch-" + section, flags = os.O_CREAT, write = True))
			satellite_message_queue_separate_clock_rules.append(add_separate)
			log_message("IPC sending to '/etha-light-switch-" + section + "'!")

mqtt_topic_broadcast = "broadcast"

mqtt_support = False
mqtt_thread = None
mqtt_client = None
mqtt_client_id = None
mqtt_connect_wait = 10
light_events_file = config_dir + clock_rules_filename + ("" if clock_rules_filename.endswith(".json") else ".json")

mqtt_broadcast_identify_message = "IDENTIFY"

mqtt_state_message = "STATE"
mqtt_on_message = "ON"
mqtt_off_message = "OFF"
mqtt_get_json_message = "GET JSON"
mqtt_set_json_message = "SET JSON"
mqtt_get_events_message = "GET EVENTS"
mqtt_get_solar_message = "GET SOLAR"
mqtt_get_configuration_message = "GET CONFIGURATION"
mqtt_set_configuration_message = "SET CONFIGURATION"
mqtt_reboot_message = "REBOOT"
mqtt_poweroff_message = "POWEROFF"
mqtt_restart_message = "RESTART"
mqtt_reload_message = "RELOAD"
mqtt_flash_message = "FLASH"

mqtt_json_message = "JSON"
mqtt_json_error_message = "JSON ERROR"
mqtt_json_updated_message = "JSON UPDATED"
mqtt_events_message = "EVENTS"
mqtt_solar_message = "SOLAR"
mqtt_configuration_message = "CONFIGURATION"
mqtt_configuration_updated_message = "CONFIGURATION UPDATED"
mqtt_configuration_error_message = "CONFIGURATION ERROR"
mqtt_broadcast_ack_message = "BROADCAST ACK"

mqtt_term_message = "TERMINATING"

# Protocol:
# Receives:				Answers:
# =========				========
# IDENTIFY				STATE
# ON					STATE
# OFF					STATE
# FLASH					STATE
# RELOAD				STATE
# RESTART				TERMINATING ... STATE
# REBOOT				TERMINATING ... STATE
# POWEROFF				TERMINATING
# GET JSON				JSON
# SET JSON				[JSON UPDATED ... STATE] / [JSON ERROR]
# GET EVENTS			EVENTS
# GET SOLAR				SOLAR
# GET CONFIGURATION		CONFIGURATION
# SET CONFIGURATION		CONFIGURATION UPDATED

ipc_stop_thread = b"0"
ipc_button_light_on = b"1"
ipc_button_light_off = b"2"
ipc_button_light_flash = b"3"
ipc_reload_tables = b"4"
ipc_reload_configuration = b"5"
ipc_meaning = {ipc_stop_thread: "Stop IPC thread",
			ipc_button_light_on: "Button press; light on",
			ipc_button_light_off: "Button press; light off",
			ipc_button_light_flash: "Button press; flash",
			ipc_reload_tables: "Reload tables",
			ipc_reload_configuration: "Reload configuration"}

switch_event_timer = None
main_loop = None

gpio_nm_state_led_on = False
nm_state_led_blink_time = 0.5
nm_state_led_timer = None
nm_state_led_blink_lock = threading.Lock()

gpio_light_switch_led_on = False
light_switch_led_timer = None
light_switch_led_blink_time = 0.25
light_switch_led_blink_lock = threading.Lock()

button_pressed_timer = None
button_pressed_timer_time = 3
just_switched_on = False
button_pressed_timer_lock = threading.Lock()
flash_notification_timer = None
flash_notification_timer_time = 3
flash_notification_timer_lock = threading.Lock()
flash_notification_time = 0.25
network_restart_notification_time = 0.125
flash_timer = None
flash_timer_lock = threading.Lock()
flash_timer_end = 0
 
acknowledge_gpio_leds_time = 0.125

switch_table_init_done = False
switch_light_state_on = False
light_events_json = ""
last_time_high_low = GPIO.HIGH

ntp_timeout = 5
ntp_max_offset = 5
started_by_systemd = False
script_name = ""
systemd_unit = ""
network_manager_systemd = "NetworkManager"
network_manager_restart_by_button = False

program_on = False
long_press_counter = 0

# This list is filled with the JSON file etha-light-switch.json
# ["day"]: An array of days for which this element is valid (1 = Monday, 2 = Tuesday, ... 7 = Sunday)
# ["period_from"]: Time of light switch on; this is either a sun time name or a time in the form HH:MM
# ["period_to"]: Time of light switch off; this is either a sun time name or a time in the form HH:MM
# ["period_to_next_day"]: Boolean indicating that period_to is on the following day
# ["divider_from"]: Divider for period_from
# ["divider_to"]: Divider for period_to
# The divider means something only when the corresponding period is a sun time name; it is a divider into the next sun time period; range: 0..1 (not including the 1)
# The formula is: sun time + ((next sun time - sun time) * divider); with a divider of 0.5 you're halfway the next period
# This means for example that a time between sunset and civil_twilight_end can be calculated
# Sun_name_names is used as a round robin table (astronomical_twilight_end is followed by astronomical_twilight_begin)
events_list = []

# This dict gets filled with the sun times of max 4 days: yesterday, today, tomorrow and the day after tomorrow
# [date]: The date for which the sun times are valid
# [sun_time_name][epoch time]: One for each sun_time_name (see sun_time_names) 
sun_times = {}

# List with the times that switches occur
# [0]: The epoch time of the switch
# [1]: "on", "off" or "no-op"; the "no-op" is in case that there are no switches on a particular day
# [2]: Boolean stating whether the switch table should be updated with the following day
switch_table = []
switch_noop_time_utc = datetime.time(23, 59, 0, tzinfo=datetime.timezone.utc)
reverse_interval_time = 59

# Zie: https://rhodesmill.org/pyephem/rise-set.html
sun_time_names = ["solar_midnight",
				"astronomical_twilight_begin",
				"nautical_twilight_begin",
				"civil_twilight_begin",
				"sunrise",
				"solar_noon",
				"sunset",
				"civil_twilight_end",
				"nautical_twilight_end",
				"astronomical_twilight_end"]
sun_time_settings = [[3, None, None],
					[0, "-18", True],
					[0, "-12", True],
					[0, "-6", True],
					[0, "-0:34", False],
					[2, None, None],
					[1, "-0:34", False],
					[1, "-6", True],
					[1, "-12", True],
					[1, "-18", True]]


def cleanup_and_stop():
	nm_init_event.set()
	if (mqtt_thread is not None) and (mqtt_client is not None):
		mqtt_send_message(mqtt_term_message)
		mqtt_client.loop_stop()
		mqtt_client.disconnect()
	if gpio_light_switch_led != 0:
		if light_switch_led_timer is not None:
			light_switch_led_timer.cancel()
	if main_loop != None:
		main_loop.quit()
	if switch_event_timer is not None:
		switch_event_timer.cancel()
	if nm_state_led_timer is not None:
		nm_state_led_timer.cancel()
	cancel_all_timers()
	for mq in satellite_message_queue:
		mq.close()
	if satellite_this_message_queue is not None:
		satellite_this_message_queue.send(ipc_stop_thread, timeout = 0)
	# Test if Linux is rebooting or switching to poweroff state
	# If so, leave the LEDs and the relay as-is, if necessary switch the state LED on again
	# When reboot or poweroff is nearly complete, GPIO will be powered off
	is_system_running = run("systemctl is-system-running", stdout=PIPE, shell=True, universal_newlines=True)
	if is_system_running.stdout.strip() == "stopping":
		system_power_down()
		log_message("System poweroff or reboot")
	else:
		if gpio_light_switch_led != 0:
			GPIO.cleanup(gpio_light_switch_led)
		if gpio_nm_state_led != 0:
			GPIO.cleanup(gpio_nm_state_led)
		if gpio_relay != 0:
			GPIO.cleanup(gpio_relay)
		if gpio_button != 0:
			GPIO.cleanup(gpio_button)
	log_message(program_name + " stopped!")

	
def print_configuration():
	log_message("================ Configuration: ================")
	if satellite is not None:
		log_message("satellite:                 " + satellite)
	if mqtt_broker is not None:
		log_message("mqtt_broker_scheme:        " + mqtt_broker_scheme)
		log_message("mqtt_broker:               " + mqtt_broker)
		log_message("mqtt_broker_port:          " + str(mqtt_broker_port))
		log_message("mqtt_username:             " + mqtt_username)
		if mqtt_topic_group is not None:
			log_message("mqtt_topic_group:          " + mqtt_topic_group)
		log_message("mqtt_topic_identification: " + mqtt_topic_identification)
	if (satellite is not None) or (mqtt_broker is not None):
		log_message("------------------------------------------------")
	if group_name is not None:
		log_message("group_name:                " + group_name)
	log_message("light_name:                " + light_name)
	log_message("default_flash_time:        " + str(default_flash_time))
	log_message("shift_solar_times:         " + str(shift_solar_times))
	log_message("clock_rules_filename:      " + clock_rules_filename)
	if satellite is None:
		log_message("button_send:               " + str(button_send))
	else:
		log_message("button_receive:            " + str(button_receive))
	log_message("invert_nm_state_led:       " + str(invert_nm_state_led))
	log_message("latitude:                  " + str(latitude))
	log_message("longitude:                 " + str(longitude))
	log_message("gpio_light_switch_led:     " + str(gpio_light_switch_led))
	log_message("gpio_nm_state_led:         " + str(gpio_nm_state_led))
	log_message("gpio_relay:                " + str(gpio_relay))
	log_message("gpio_button:               " + str(gpio_button))
	log_message("================================================")

def set_signal_handler():
	signal.signal(signal.SIGINT, signal_handler)
	signal.signal(signal.SIGTERM, signal_handler)
	signal.signal(signal.SIGHUP, sighup_handler)


def signal_handler(signalnum, frame):
	log_message("Signal " + signal.Signals(signalnum).name + " received!")
	cleanup_and_stop()
	sys.exit(0)


def sighup_handler(signalnum, frame):
	log_message("Signal " + signal.Signals(signalnum).name + " received!")
	reload_tables(True)
	ipc_send(False, ipc_reload_tables)
	
def ipc_send(button_press, ipc_signal):
	if (len(satellite_message_queue) > 0) and ((button_press and button_send) or \
	((not button_press) and satellite_message_queue_separate_clock_rules_condition) or \
	(ipc_signal == ipc_reload_configuration)):
		log_message("IPC message sending: " + ipc_meaning[ipc_signal])
		for i, mq in enumerate(satellite_message_queue):
			if (button_press and button_send) or ((not button_press) and satellite_message_queue_separate_clock_rules[i]) or \
			(ipc_signal == ipc_reload_configuration):
				mq.send(ipc_signal)

def ipc_receive():
	while satellite_this_message_queue.current_messages > 0:
		satellite_this_message_queue.receive(0)
	log_message("IPC message queue cleared!")
	while True:
		message, priority = satellite_this_message_queue.receive(None)
		log_message("IPC message received: " + ipc_meaning[message])
		if message == ipc_stop_thread:
			break
		if button_receive:
			if (message == ipc_button_light_on) and not switch_light_state_on:
				switch_light_on("Light switched on by IPC message!", False)
			if (message == ipc_button_light_off) and switch_light_state_on:
				switch_light_off("Light switched off by IPC message!")
			if message == ipc_button_light_flash:
				start_flash(default_flash_time)
		if (message == ipc_reload_tables) and (not separate_clock_rules):
			reload_tables(True)
		if message == ipc_reload_configuration:
			reload_configuration()
	satellite_this_message_queue.close()
			
def reload_tables(read_json_file):
	if switch_table_init_done:
		if switch_event_timer is not None:
			switch_event_timer.cancel()
		log_message("Reloading tables!")
		switch_table_init(read_json_file)

# Find out which sun time name is the one following the given one, looping round
def get_next_period(period):
	next_index = sun_time_names.index(period) + 1
	return sun_time_names[next_index] if (next_index < (len(sun_time_names))) else sun_time_names[0]

# A period may not be defined for certain places at certain times
# (e.g. the sun never rises or sets during the summer in the very north).
# When a period is not defined, the corresponding sun_times contains -1.
# When this happens the next valid period will be substituted.
# If no valid period is found, None will be returned. 
def get_valid_period(on_date, period):
	if (sun_times[on_date][period] < 0):
		if shift_solar_times:
			index = sun_time_names.index(period)
			if 0 <= index < (len(sun_time_names) / 2):
				while (sun_times[on_date][sun_time_names[index]] < 0) and (index < (len(sun_time_names) / 2)):
					index += 1
			else:
				while (sun_times[on_date][sun_time_names[index]] < 0) and (index >= (len(sun_time_names) / 2)):
					index -= 1
			if sun_times[on_date][sun_time_names[index]] < 0:
				return None
			else:
				return sun_time_names[index]
		else:
			return None
	else:
		return period

def get_epoch_time_from(on_date, period, next_day, divider):
	this_date = (on_date + timedelta(days = 1)) if next_day else on_date
	# Is this period a solar time (e.g. "Sunset") or a time in the format HH:MM?
	if period in sun_time_names:
		if this_date not in sun_times:
			get_sun_times(this_date)
		period = get_valid_period(this_date, period)
		if period == None:
			return None
		return_epoch_time = sun_times[this_date][period]
		if divider > 0:
			next_period = get_next_period(period)
			if next_period == sun_time_names[0]:
				that_date = this_date + timedelta(days = 1)
				if that_date not in sun_times:
					get_sun_times(that_date)
			else:
				that_date = this_date
			next_period = get_valid_period(that_date, next_period)
			if next_period == None:
				return None
			that_epoch_time = sun_times[that_date][next_period]
			return round(return_epoch_time + ((that_epoch_time - return_epoch_time) * divider))
		else:
			return return_epoch_time
	else:
		return datetime.datetime.combine(this_date, datetime.datetime.strptime(period, "%H:%M").time()).timestamp()

def fill_switch_table_entry(from_epoch_time, to_epoch_time):
	log_debug("fill_switch_table(" + str(from_epoch_time) + ", " + str(from_epoch_time) + ")")
	global switch_table
	# Don't add event if it falls in the middle of another event
	add_event = True
	for switch in range(len(switch_table) - 1):
		if (((switch_table[switch][1] == "on") and (switch_table[switch][0] <= from_epoch_time)) and
		((switch_table[switch + 1][1] == "off") and (switch_table[switch + 1][0] >= to_epoch_time))):
			add_event = False
			break
	# Remove events in between this event
	if add_event:
		for i, switch in reversed(list(enumerate(switch_table))):
			if to_epoch_time >= switch[0] >= from_epoch_time:
				del switch_table[i]
		switch_table.append([from_epoch_time, "on", False])
		switch_table.append([to_epoch_time, "off", False])
		switch_table.sort(key=itemgetter(0))
		# The switch_table gets cleaned; it is looped over several times until there are no more deletions
		switch_deleted = True
		while switch_deleted:
			switch_deleted = False
			for switch in range(len(switch_table) - 1):
				# Two consecutive ons: the second one is removed
				if (switch_table[switch][1] == "on") and (switch_table[switch + 1][1] == "on"):
					del switch_table[switch + 1]
					switch_deleted = True
					break
				# Two consecutive offs: the first one is removed
				elif (switch_table[switch][1] == "off") and (switch_table[switch + 1][1] == "off"):
					del switch_table[switch]
					switch_deleted = True
					break
				# An on and a consecutive off (or vice versa): if they're within reverse_interval_time seconds, then they're both removed,
				# so the light doesn't switch on or off for a very limited period of time 
				if (switch_table[switch][1] != switch_table[switch + 1][1]) and (switch_table[switch + 1][0] - switch_table[switch][0] <= reverse_interval_time):
					del switch_table[switch + 1]
					del switch_table[switch]
					switch_deleted = True
					break
				
def get_pattern_time(minutes, randomize):
	if minutes <= 1:
		return 60
	if not randomize:
		return minutes * 60
	return randint(61, minutes * 60)

def fill_switch_table(on_date):
	log_debug("fill_switch_table(" + str(on_date) + ")")
	global switch_table
	on_day = on_date.isoweekday()
	for event in events_list:
		if on_day in event["day"]:
			# Get today's times for this event.
			from_epoch_time = get_epoch_time_from(on_date, event["period_from"], False, event["divider_from"])
			to_epoch_time = get_epoch_time_from(on_date, event["period_to"], event["period_to_next_day"], event["divider_to"])
			if (from_epoch_time is not None) and (to_epoch_time is not None) and (from_epoch_time < to_epoch_time):
				if event["pattern_on"] is None:
					fill_switch_table_entry(from_epoch_time, to_epoch_time)
				else:
					time_start = from_epoch_time
					while time_start < to_epoch_time:
						time_on = get_pattern_time(event["pattern_on"], event["pattern_randomize"])
						time_off = get_pattern_time(event["pattern_off"], event["pattern_randomize"])
						fill_switch_table_entry(time_start, min(time_start + time_on, to_epoch_time))
						time_start += time_on + time_off
	if DEBUG: print_switch_table(True)
	clean_switch_table_old_entries()


def finalize_switch_table(on_date):
	log_debug("finalize_switch_table(" + str(on_date) + ")")
	# The last element of today in the switch table must make sure that the switch table is filled again with switches for the the next day
	# If there are no elements for today, create a "no-op" one
	if len(switch_table) == 0:
		switch_table.append([datetime.datetime.combine(on_date, switch_noop_time_utc).timestamp(), "no-op", True])
	else:
		for switch in range(len(switch_table)):
			if (switch >= (len(switch_table) - 1)) or (datetime.datetime.utcfromtimestamp(switch_table[switch + 1][0]).date() > on_date):
				if (datetime.datetime.utcfromtimestamp(switch_table[switch][0]).date() == on_date) and (switch_table[switch][0] > time.time()):
					switch_table[switch][2] = True
				else:
					switch_table.append([datetime.datetime.combine(on_date, switch_noop_time_utc).timestamp(), "no-op", True])
				break
	switch_table.sort(key = itemgetter(0))
	if DEBUG: print_switch_table(True)

def return_switch_table():
	return_json = []
	flash_timer_end_tested = False
	test_switch_after_flash = False
	previous_state_on = switch_light_state_on
	for switch in switch_table:
		# Insert flash_timer_end into the returned json at the correct position.
		if (not flash_timer_end_tested) and (flash_timer is not None) and (flash_timer_end < switch[0]):
			return_json.append(return_switch_table_flash_end())
			previous_state_on = False
			flash_timer_end_tested = True
			test_switch_after_flash = True
		if (switch[0] > time.time()):
			return_json_line = {}
			# If the first event equals the current state, then it is pointless.
			# If the first event after flash_timer_end is "off", then it is also pointless.
			# All events before flash_timer_end are also pointless.
			if ((switch[1] == "on") & previous_state_on) | ((switch[1] == "off") & (not previous_state_on)):
#			if ((len(return_json) == 0) & (((switch[1] == "on") & switch_light_state_on) | ((switch[1] == "off") & (not switch_light_state_on)))) or \
#			(test_switch_after_flash & (switch[1] == "off")):
				return_json_line["irrelevant"] = True
			else:
				return_json_line["irrelevant"] = False
			if (flash_timer is not None) and (switch[0] <= flash_timer_end) and (switch[1] != "no-op"):
				return_json_line["irrelevant_flash"] = True
			else:
				return_json_line["irrelevant_flash"] = False
			if switch[1] == "no-op":
				return_json_line["switch"] = 2
			else:
				test_switch_after_flash = False
				if switch[1] == "on":
					return_json_line["switch"] = 1
					previous_state_on = True
				else:
					return_json_line["switch"] = 0
					previous_state_on = False
			return_json_line["time"] = format_epoch_tz(switch[0])
			return_json_line["schedule"] = switch[2]
			return_json_line["flash_end"] = False
			return_json.append(return_json_line)
	# Insert flash_timer_end here if not done before.
	if (not flash_timer_end_tested) and (flash_timer is not None):
		return_json.append(return_switch_table_flash_end())
	return json.dumps(return_json)


def return_switch_table_flash_end():
	return_json_line = {}
	return_json_line["irrelevant"] = False
	return_json_line["irrelevant_flash"] = False
	return_json_line["switch"] = 0
	return_json_line["time"] = format_epoch_tz(flash_timer_end)
	return_json_line["schedule"] = False
	return_json_line["flash_end"] = True
	return return_json_line


def print_switch_table(complete):
	# complete is for debugging purposes
	if complete:
		log_message("============== All light events: ===============")
	else:
		log_message("============ Upcoming light events: ============")
	for switch in switch_table:
		if complete or (switch[0] > time.time()):
			log_message(("Switch light " + switch[1] + " at: ").ljust(23) + format_epoch_tz(switch[0]))
	log_message("================================================")

	
def clean_switch_table_old_entries():
	# Remove all old events from the switch table, except for one
	except_one_found = False
	for i, switch in reversed(list(enumerate(switch_table))):
		if except_one_found:
			del switch_table[i]
		if (switch[0] <= time.time()):
			except_one_found = True

			
def solar_times_to_json(on_date_string):
	return_json_line = {}
	on_date = datetime.datetime.strptime(on_date_string, "%Y-%m-%d").date()
	if on_date not in sun_times:
		get_sun_times(on_date)
	return_json_line["date"] = on_date_string
	return_json_line["latitude"] = latitude
	return_json_line["longitude"] = longitude
	for sun_time_name in sun_time_names:
		return_json_line[sun_time_name] = format_epoch_tz(sun_times[on_date][sun_time_name])
	return json.dumps(return_json_line)

	
def get_sun_times(on_date):
	global sun_times

	# Remove sun_times not needed anymore
	old_date = (on_date - timedelta(days=4))
	for date in list(sun_times):
		if date <= old_date:
			del sun_times[date]
	
	# Get the sun times for on_date if they're not in sun_times
	if on_date not in sun_times:
		log_message("==================== Solar times: ====================")
# 		try:
# 			response = requests.get("https://api.sunrise-sunset.org/json?lat=" + str(latitude) + "&lng=" + str(longitude) +
# 								"&date=" + on_date.strftime("%Y-%m-%d") + "&formatted=0")
# 		except requests.exceptions.RequestException as e:
# 			log_message("Error in requests.get api.sunrise-sunset.org: " + e + "!")
# 		else:
# 			sunset_times = json.loads(response.text)
# 			if sunset_times["status"] == "OK":
# 				sun_time_element = {}
# 				for sun_time_name in sun_time_names:
# 					sun_time_element[sun_time_name] = parse(sunset_times["results"][sun_time_name]).timestamp()
# 					log_message((sun_time_name + ": ").ljust(29) + format_epoch_tz(sun_time_element[sun_time_name]))
# 				
# 				log_message("======================================================")
# 				sun_times[on_date] = sun_time_element
# 			else:
# 				log_message("Error in response from api.sunrise-sunset.org (status=" + sunset_times["status"] + ")!")
		sun_time_element = {}
		ephem_observer.date = on_date
		time = 0
		for i, sun_time_name in enumerate(sun_time_names):
			if sun_time_settings[i][0] == 1:
				ephem_observer.horizon = sun_time_settings[i][1]
				try:
					time = parse(str(ephem_observer.next_setting(ephem.Sun(), use_center = sun_time_settings[i][2])) + " UTC").timestamp()
				except ephem.CircumpolarError:
					sun_time_element[sun_time_name] = -1
				else:
					sun_time_element[sun_time_name] = time
			elif sun_time_settings[i][0] == 0:
				ephem_observer.horizon = sun_time_settings[i][1]
				try:
					time = parse(str(ephem_observer.next_rising(ephem.Sun(), use_center = sun_time_settings[i][2])) + " UTC").timestamp()
				except ephem.CircumpolarError:
					sun_time_element[sun_time_name] = -1
				else:
					sun_time_element[sun_time_name] = time
			elif sun_time_settings[i][0] == 2:
				sun_time_element[sun_time_name] = parse(str(ephem_observer.next_transit(ephem.Sun())) + " UTC").timestamp()
			elif sun_time_settings[i][0] == 3:
				sun_time_element[sun_time_name] = parse(str(ephem_observer.previous_antitransit(ephem.Sun(),
					start = ephem_observer.next_transit(ephem.Sun()))) + " UTC").timestamp()
			log_message((sun_time_name + ": ").ljust(29) + format_epoch_tz(sun_time_element[sun_time_name]))
		log_message("======================================================")
		sun_times[on_date] = sun_time_element


def parse_event_time(event_time):
	if event_time.replace(":", "").isdecimal():
		return datetime.datetime.combine(datetime.datetime.utcnow(), datetime.datetime.strptime(event_time, "%H:%M").time()).timestamp()
	else:
		return event_time

# Read the light events JSON file ('/etc/etha/etha-light-switch.json'). If it does not exist, an empty one will be created.
def parse_light_events_file():
	global light_events_json
	try:
		with open(light_events_file) as file:
			try:
				light_events_json = json.load(file)
			except ValueError as e:
				log_message("Error parsing file '" + light_events_file + "': " + str(e) + "!")
				cleanup_and_stop()
				sys.exit(6)
	except IOError:
		log_message("File '" + light_events_file + "' not found or not readable!")
		light_events_json = json.loads("{}")
		try:
			with open(light_events_file, "w") as outfile:
				json.dump(light_events_json, outfile, indent = 1)
		except IOError:
			log_message("Empty file '" + light_events_file + "' could not be written!")
			cleanup_and_stop()
			sys.exit(74)
		else:
			log_message("Empty file '" + light_events_file + "' created!")
	parse_light_events()

def parse_light_events():
	global events_list
	events_list = []
	for event in light_events_json:
		if event["active"]:
			if "pattern" in event:
				pattern_on = event["pattern"]["on"]
				pattern_off = event["pattern"]["off"]
				pattern_randomize = event["pattern"]["randomize"]
			else:
				pattern_on = None
				pattern_off = None
				pattern_randomize = None
			events_list.append({"day": event["day"],
							"period_from": event["period"]["from"],
							"period_to": event["period"]["to"],
							"period_to_next_day": event["period"]["to_next_day"],
							"divider_from": event["divider"]["from"],
							"divider_to": event["divider"]["to"],
							"pattern_on": pattern_on,
							"pattern_off": pattern_off,
							"pattern_randomize": pattern_randomize})

def switch_light_on(message, delay_send_state):
	global switch_light_state_on
	log_debug(("switch_light_on({}, {})").format(message, delay_send_state))
	cancel_flash_notification()
	set_gpio_light_switch_led(True)
	if gpio_relay != 0:
		GPIO.output(gpio_relay, GPIO.HIGH)
	log_message(message)
	switch_light_state_on = True
	if not delay_send_state:
		mqtt_send_state()


def switch_light_off(message):
	global switch_light_state_on
	log_debug(("switch_light_off({})").format(message))
	cancel_flash_notification()
	set_gpio_light_switch_led(False)
	if gpio_relay != 0:
		GPIO.output(gpio_relay, GPIO.LOW)
	log_message(message)
	switch_light_state_on = False
	mqtt_send_state()

		
def set_next_switch_light_event():
	global switch_event_timer
	i = 0
	while switch_table[i][0] <= time.time():
		i += 1
	switch_event_timer = threading.Timer(switch_table[i][0] - time.time(), switch_light_event, switch_table[i])
	switch_event_timer.start()
	log_message("Light event scheduled: switch " + switch_table[i][1] + " at " + format_epoch_tz(switch_table[i][0]))
	return switch_table[i][1]


def switch_light_event(switch_event_time, switch_event_switch, switch_event_update):
	global program_on
	if switch_event_switch == "on":
		program_on = True
		if flash_timer == None:
			switch_light_on("Light event switched light on!", False)
		else:
			log_message("Light switch on event prevented by flash!")
	elif switch_event_switch == "off":
		program_on = False
		if flash_timer == None:
			switch_light_off("Light event switched light off!")
		else:
			log_message("Light switch off event prevented by flash!")

	clean_switch_table_old_entries()

	switch_event_date = datetime.datetime.utcfromtimestamp(switch_event_time).date()
	if switch_event_update:
		new_date = switch_event_date + timedelta(days = 1)
		fill_switch_table(new_date)
		fill_switch_table(new_date + timedelta(days = 1))
		finalize_switch_table(new_date)
		print_switch_table(False)

	set_next_switch_light_event()


def switch_table_init(read_json_file):
	global switch_table, program_on
	if read_json_file:
		parse_light_events_file()
	else:
		parse_light_events()
	switch_table = []
	fill_switch_table((datetime.datetime.utcnow() - timedelta(days = 1)).date())
	fill_switch_table(datetime.datetime.utcnow().date())
	fill_switch_table((datetime.datetime.utcnow() + timedelta(days = 1)).date())
	finalize_switch_table(datetime.datetime.utcnow().date())
	print_switch_table(False)
	
	event_op = set_next_switch_light_event()
	counter = 1
	while (counter < len(switch_table)) and (event_op == "no-op"):
		event_op = switch_table[counter][1]
		counter += 1
	if event_op == "off":
		program_on = True
		switch_light_on("Initially switched light on (previous state)!", False)
	else:
		program_on = False
		switch_light_off("Initially switched light off (previous or unknown state)!")


def button_pressed(channel):
	global last_time_high_low, button_pressed_timer, just_switched_on, long_press_counter, network_manager_restart_by_button
	log_debug("button_pressed()")
	gpio_input_high_low = GPIO.input(channel)
	# Prohibit bouncing (multiple GPIO.RISING events or GPIO.FALLING events in succession).
	if gpio_input_high_low != last_time_high_low:
		last_time_high_low = gpio_input_high_low
		if gpio_input_high_low == GPIO.LOW:
			with button_pressed_timer_lock:
				long_press_counter = 0
				button_pressed_timer = threading.Timer(button_pressed_timer_time, button_pressed_long)
				button_pressed_timer.start()
			cancel_flash_notification()
			if not switch_light_state_on:
				switch_light_on("Light switched on by button press!", False)
				ipc_send(True, ipc_button_light_on)
				# Prevent the next GPIO.HIGH event (which will occur when the button is released) to switch the light off again. 
				just_switched_on = True
		else:
			with button_pressed_timer_lock:
				if button_pressed_timer is not None:
					button_pressed_timer.cancel()
					button_pressed_timer = None
			if long_press_counter == 1:
				start_flash(default_flash_time)
				ipc_send(True, ipc_button_light_flash)
			elif long_press_counter == 2:
				cancel_flash_notification(False)
				log_message(network_manager_systemd + " restart requested by button press!")
				network_manager_restart_by_button = True;
				run("systemctl restart " + network_manager_systemd, shell=True)
			elif long_press_counter == 3:
				reboot_now()
			if not just_switched_on:
				if switch_light_state_on:
					switch_light_off("Light switched off by button press!")
					ipc_send(True, ipc_button_light_off)
			# Now it is valid to switch the light off at the next GPIO.HIGH event (button pressed and released).
			just_switched_on = False


# At this time the light is on and the button is still pressed; a flash timer is requested (or a reboot/poweroff).
# Decision is based on long_press_counter:
# 0 - The button has been pressed for button_pressed_timer_time; a flash timer is started if the button is now released.
# 1 - The button has been pressed for 2 times button_pressed_timer_time; NetworkManager will restart if the button is now released.
# 2 - The button has been pressed for 2 times button_pressed_timer_time; system will reboot if the button is now released.
# 3 - The button was never released and has been pressed for 3 times button_pressed_timer_time; the system will now poweroff.
def button_pressed_long():
	global button_pressed_timer, just_switched_on, flash_notification_timer, long_press_counter
	log_debug("button_pressed_long()")
	if long_press_counter == 0:
		if gpio_light_switch_led != 0:
			with flash_notification_timer_lock:
				if flash_notification_timer is not None:
					flash_notification_timer.cancel()
				flash_notification_timer = threading.Thread(target = flash_notification, args = (flash_notification_timer_time,))
				flash_notification_timer.start()
	elif long_press_counter == 1:
		if gpio_light_switch_led != 0:
			with flash_notification_timer_lock:
				if flash_notification_timer is not None:
					flash_notification_timer.cancel()
				# gpio_light_switch_led will blink with a short interval. Set the button_pressed_long a second time to query for reboot.
				flash_notification_timer = threading.Thread(target = flash_notification, args = (flash_notification_time,))
				flash_notification_timer.start()
	elif long_press_counter == 2:
		if gpio_light_switch_led != 0:
			with flash_notification_timer_lock:
				if flash_notification_timer is not None:
					flash_notification_timer.cancel()
				# gpio_light_switch_led will blink rapidly with a very short interval. Set the button_pressed_long a third time to query for poweroff.
				flash_notification_timer = threading.Thread(target = flash_notification, args = (network_restart_notification_time, network_restart_notification_time))
				flash_notification_timer.start()
	else:
		poweroff_now()
	with button_pressed_timer_lock:
		if button_pressed_timer is not None:
			long_press_counter += 1
			button_pressed_timer = threading.Timer(button_pressed_timer_time, button_pressed_long)
			button_pressed_timer.start()
	# Don't switch the light off when the button is released.
	just_switched_on = True

	
def start_flash(flash_time, initial_blink_off = True):
	global flash_notification_timer, flash_timer, flash_timer_end
	log_debug(("start_flash({})").format(flash_time))
	# Start flash notification signal cycle.
	if gpio_light_switch_led != 0:
		if flash_notification_timer is None:
			# flash_notification() is started on a thread, so that the time.sleep() in flash_notification() will not block. 
			flash_notification_timer = threading.Thread(target = flash_notification, args = (flash_notification_timer_time, flash_notification_time, initial_blink_off))
			flash_notification_timer.start()
	with flash_timer_lock:
		if flash_timer == None:
			time_now = time.time()
			flash_timer_end = time_now + (flash_time * 60)
			flash_timer = threading.Timer(flash_timer_end - time_now, end_of_flash)
			flash_timer.start()
			mqtt_send_state()
			log_message("Flash started for " + str(flash_time) + " minutes!")


def flash_notification(next_flash_notification_timer_time, off_time = flash_notification_time, initial_blink_off = True):
	global flash_notification_timer
	log_debug(("flash_notification({}, off_time = {})").format(next_flash_notification_timer_time, off_time))
	with flash_notification_timer_lock:
		if flash_notification_timer is not None:
			if initial_blink_off:
				set_gpio_light_switch_led(False)
				time.sleep(off_time)
				set_gpio_light_switch_led(True)
			flash_notification_timer = threading.Timer(next_flash_notification_timer_time, flash_notification, (next_flash_notification_timer_time, off_time))
			flash_notification_timer.start()


def end_of_flash():
	cancel_flash_notification(False)
	switch_light_off("Flash ended, resuming clock program with light switched off!")


def cancel_flash_notification(show_canceled_message = True):
	global flash_notification_timer, flash_timer
	log_debug(("cancel_flash_notification(show_canceled_message = {})").format(show_canceled_message))
	if gpio_light_switch_led != 0:
		with flash_notification_timer_lock:
			if flash_notification_timer is not None:
				flash_notification_timer.cancel()
				flash_notification_timer = None
	with flash_timer_lock:
		if flash_timer is not None:
			flash_timer.cancel()
			flash_timer = None
			if show_canceled_message:
				log_message("Flash canceled!")
				
def reboot_now():
	log_message("System is rebooting...")
	cancel_all_timers()
	system_power_down()
	run("reboot", shell = True)


def poweroff_now():
	log_message("System is powering off...")
	cancel_all_timers()
	system_power_down()
	run("poweroff", shell = True)


def cancel_all_timers():
	log_debug("cancel_all_timers()")
	global button_pressed_timer, nm_state_led_timer
	if gpio_button != 0:
		GPIO.remove_event_detect(gpio_button)
		with button_pressed_timer_lock:
			if button_pressed_timer is not None:
				button_pressed_timer.cancel()
				button_pressed_timer = None
	cancel_flash_notification()
	with nm_state_led_blink_lock:
		if nm_state_led_timer is not None:
			nm_state_led_timer.cancel()
			nm_state_led_timer = None
				
def system_power_down():
	set_gpio_nm_state_led_on()
	set_gpio_light_switch_led(True)


def set_gpio_nm_state_led(new_state_on):
	global gpio_nm_state_led_on
	if new_state_on:
		gpio_nm_state_led_on = True
		if gpio_nm_state_led != 0:
			if invert_nm_state_led:
				GPIO.output(gpio_nm_state_led, GPIO.LOW)
			else:
				GPIO.output(gpio_nm_state_led, GPIO.HIGH)
	else:
		gpio_nm_state_led_on = False
		if gpio_nm_state_led != 0:
			if invert_nm_state_led:
				GPIO.output(gpio_nm_state_led, GPIO.HIGH)
			else:
				GPIO.output(gpio_nm_state_led, GPIO.LOW)

def set_gpio_nm_state_led_blink():
	global nm_state_led_timer
	if gpio_nm_state_led != 0:
		with nm_state_led_blink_lock:
			if nm_state_led_timer is not None:
				set_gpio_nm_state_led(not gpio_nm_state_led_on)
				nm_state_led_timer = threading.Timer(nm_state_led_blink_time, set_gpio_nm_state_led_blink)
				nm_state_led_timer.start()


def set_gpio_nm_state_led_on():
	global nm_state_led_timer
	if gpio_nm_state_led != 0:
		with nm_state_led_blink_lock:
			if nm_state_led_timer is not None:
				nm_state_led_timer.cancel()
				nm_state_led_timer = None
	set_gpio_nm_state_led(True)

	
def acknowledge_gpio_leds():
	set_gpio_nm_state_led(not gpio_nm_state_led_on)
	set_gpio_light_switch_led(not gpio_light_switch_led_on)
	time.sleep(acknowledge_gpio_leds_time)
	set_gpio_nm_state_led(not gpio_nm_state_led_on)
	set_gpio_light_switch_led(not gpio_light_switch_led_on)


def set_gpio_light_switch_led(new_state_on):
	global gpio_light_switch_led_on
	if new_state_on:
		gpio_light_switch_led_on = True
		if gpio_light_switch_led != 0:
			GPIO.output(gpio_light_switch_led, GPIO.HIGH)
	else:
		gpio_light_switch_led_on = False
		if gpio_light_switch_led != 0:
			GPIO.output(gpio_light_switch_led, GPIO.LOW)

def set_gpio_light_switch_led_blink():
	global light_switch_led_timer
	if gpio_light_switch_led != 0:
		with light_switch_led_blink_lock:
			if light_switch_led_timer is not None:
				set_gpio_light_switch_led(not gpio_light_switch_led_on)
				light_switch_led_timer = threading.Timer(light_switch_led_blink_time, set_gpio_light_switch_led_blink)
				light_switch_led_timer.start()

			
def set_gpio_light_switch_led_off():
	global light_switch_led_timer
	if gpio_light_switch_led != 0:
		with light_switch_led_blink_lock:
			if light_switch_led_timer is not None:
				light_switch_led_timer.cancel()
				light_switch_led_timer = None
	set_gpio_light_switch_led(False)


def wait_for_time_to_synchronize():
	global light_switch_led_timer
	light_switch_led_timer = True
	set_gpio_light_switch_led_blink()
	ntp_client = ntplib.NTPClient()
	log_message("Waiting for time synchronization to complete...")
	while True:
		try:
			ntp_time = ntp_client.request(time_server, timeout = ntp_timeout)
			log_message("NTP offset: " + str(ntp_time.offset))
			if abs(ntp_time.offset) <= ntp_max_offset:
				break
		except socket.gaierror as gai_error:
			log_message("NTP error: " + str(gai_error))
			cleanup_and_stop()
			sys.exit(6)
		except ntplib.NTPException as ntp_exception:
			log_message("NTP error: " + str(ntp_exception))
	log_message("Time synchronized!")
	set_gpio_light_switch_led_off()

	
def nm_test(state):
	global nm_state_led_timer, switch_table_init_done, network_manager_restart_by_button
	log_message("NetworkManager state: " + NetworkManager.const('STATE', state) + "!")
	if (state == NetworkManager.NM_STATE_CONNECTED_GLOBAL):
		set_gpio_nm_state_led_on()
		wait_for_time_to_synchronize()
		if not switch_table_init_done:
			ephem_init()
			switch_table_init(True)
			mqtt_init()
			switch_table_init_done = True
		elif network_manager_restart_by_button:
			reload_tables(True)
			network_manager_restart_by_button = False
	else:
		if gpio_nm_state_led != 0:
			if nm_state_led_timer is None:
				nm_state_led_timer = True
				set_gpio_nm_state_led_blink()
		
def nm_state_changed(nm, interface, signal, state):
	nm_test(state)

def nm_init():
	if gpio_nm_state_led != 0:
		GPIO.setup(gpio_nm_state_led, GPIO.OUT)
		GPIO.output(gpio_nm_state_led, GPIO.LOW)
	dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
	NetworkManager.NetworkManager.OnStateChanged(nm_state_changed)
	bus = dbus.SystemBus()
	proxy_network_manager = bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
	network_manager = dbus.Interface(proxy_network_manager, "org.freedesktop.NetworkManager")
	nm_test(network_manager.state())

def return_configuration():
	return_json = {}
	return_json["group_name"] = "" if (group_name == None) else group_name
	return_json["light_name"] = light_name
	return_json["default_flash_time"] = default_flash_time
	return_json["shift_solar_times"] = shift_solar_times
	return_json["invert_nm_state_led"] = invert_nm_state_led
	return_json["latitude"] = latitude
	return_json["longitude"] = longitude
	return return_json

def set_configuration(json_message):
	global group_name, light_name, default_flash_time, shift_solar_times, latitude, longitude, sun_times, invert_nm_state_led
	json_configuration = json.loads(json_message)
	group_name_updated = configuration_update(json_configuration, "common", "group_name", ("" if (group_name == None) else group_name))
	if satellite == None:
		update_section = "common"
	else:
		update_section = satellite
	light_name_updated = configuration_update(json_configuration, update_section, "light_name", light_name)
	default_flash_time_updated = configuration_update(json_configuration, update_section, "default_flash_time", default_flash_time)
	shift_solar_times_updated = configuration_update(json_configuration, update_section, "shift_solar_times", shift_solar_times)
	invert_nm_state_led_updated = configuration_update(json_configuration, "common", "invert_nm_state_led", invert_nm_state_led)
	latitude_updated = configuration_update(json_configuration, "coordinates", "latitude", latitude)
	longitude_updated = configuration_update(json_configuration, "coordinates", "longitude", longitude)
	if group_name_updated | light_name_updated | default_flash_time_updated | shift_solar_times_updated | invert_nm_state_led_updated | latitude_updated | longitude_updated:
		try:
			update_configuration_file = open(config_file, "w")
		except IOError:
			log_message("Configuration file '" + config_file + "' could not be written!")
			mqtt_send_message(mqtt_configuration_error_message)
		else:
			config.write(update_configuration_file)
			update_configuration_file.close()
			if group_name_updated:
				group_name = None if (json_configuration["group_name"] == "") else json_configuration["group_name"] 
			light_name = json_configuration["light_name"]
			default_flash_time = json_configuration["default_flash_time"]
			shift_solar_times = json_configuration["shift_solar_times"]
			if invert_nm_state_led_updated:
				invert_nm_state_led = json_configuration["invert_nm_state_led"]
				set_gpio_nm_state_led(gpio_nm_state_led_on)
			if latitude_updated:
				latitude = json_configuration["latitude"]
			if longitude_updated:
				longitude = json_configuration["longitude"]
			log_message("Configuration updated!")
			print_configuration()
			if shift_solar_times_updated | latitude_updated | longitude_updated:
				if latitude_updated | longitude_updated:
					ephem_observer.lat = str(latitude)
					ephem_observer.lon = str(longitude)
					# Invalidate sun_times because new sun_times have to be calculated.
					sun_times.clear()
				reload_tables(True)
			else:
				mqtt_send_state()
			if satellite == None:
				ipc_send(False, ipc_reload_configuration)
			mqtt_send_message(mqtt_configuration_updated_message)
	else:
		log_message("Configuration not changed!")
		# Send CONFIGURATION UPDATED anyway, because the Android user may have canceled the previous configuration update before confirmation was received.
		mqtt_send_message(mqtt_configuration_updated_message)

		
def configuration_update(json_configuration, section, key, value):
	try:
		new_value = json_configuration[key]
	except json.JSONDecodeError:
		return False
	if new_value != value:
		try:
			config.set(section, key, str(new_value))
		except configparser.NoSectionError:
			config.add_section(section)
			config.set(section, key, str(new_value))
		return True
	else:
		return False

# Gets called by ipc_receive when the configuration of the master has been dynamically changed (through the Android app).
def reload_configuration():
	global group_name, light_name, default_flash_time, shift_solar_times, latitude, longitude, sun_times, invert_nm_state_led
	config.read(config_file)
	group_name = config.get("common", "group_name", fallback = group_name)
	group_name = None if (group_name == "") else group_name
	light_name = config.get(satellite, "light_name", fallback = light_name)
	
	default_flash_time = config.getint("common", "default_flash_time", fallback = default_flash_time_fallback)
	default_flash_time = config.getint(satellite, "default_flash_time", fallback = default_flash_time)
	
	invert_nm_state_led = config.getboolean("common", "invert_nm_state_led", fallback = invert_nm_state_led)
	set_gpio_nm_state_led(gpio_nm_state_led_on)

	new_shift_solar_times = config.getboolean("common", "shift_solar_times", fallback = shift_solar_times_fallback)
	new_shift_solar_times = config.getboolean(satellite, "shift_solar_times", fallback = new_shift_solar_times)
	
	new_latitude = config.getfloat("coordinates", "latitude", fallback = latitude)
	new_longitude = config.getfloat("coordinates", "longitude", fallback = longitude)
	log_message("Configuration reloaded!")
	print_configuration()
	if (new_latitude != latitude) or (new_longitude != longitude) or (new_shift_solar_times != shift_solar_times):
		shift_solar_times = new_shift_solar_times
		if (new_latitude != latitude) or (new_longitude != longitude):
			latitude = new_latitude
			longitude = new_longitude
			ephem_observer.lat = str(latitude)
			ephem_observer.lon = str(longitude)
			sun_times.clear()
		reload_tables(True)
	else:
		mqtt_send_state()

def mqtt_send_state():
	log_debug(("mqtt_send_state()").format())
	state = {}
	state["lo"] = switch_light_state_on
	state["sd"] = started_by_systemd
	state["id"] = mqtt_topic_identification
	state["ln"] = light_name
	state["df"] = default_flash_time
	state["fl"] = False if (flash_timer == None) else True
	state["po"] = program_on
	state["pv"] = program_version
	state["gr"] = "" if (mqtt_topic_group == None) else mqtt_topic_group
	state["gn"] = "" if (group_name == None) else group_name
	state["sa"] = "" if (satellite == None) else satellite
	state["sc"] = separate_clock_rules
	state["hn"] = socket.gethostname()
	mqtt_send_message(mqtt_state_message + " " + json.dumps(state))


def mqtt_send_message(mqtt_message):
	if (mqtt_broker is not None) and (mqtt_thread is not None) and mqtt_support:
		topic = mqtt_topic_base + "/" + ("" if (mqtt_topic_group == None) else (mqtt_topic_group + "/")) + mqtt_topic_identification
		log_message("MQTT message publishing on topic '" + topic + "': '" + mqtt_message + "'!")
		mqtt_message_info = mqtt_client.publish(topic, payload = "<" + mqtt_client_id + "> " + mqtt_message)
		if mqtt_message_info.rc != mqtt.MQTT_ERR_SUCCESS:
			log_message(("MQTT publishing failed with result code {}!").format(mqtt_message_info.rc))
	
def on_mqtt_message(client, userdata, message):
	mqtt_message = str(message.payload.decode("utf-8"))
	log_debug(("on_mqtt_message(message = '{}')").format(mqtt_message))
	valid_mqtt_message = False
	if mqtt_message[0] == "<":
		end_of_client_id = mqtt_message.index("> ")
		remote_client_id = mqtt_message[1:end_of_client_id]
		if remote_client_id:
			if remote_client_id == mqtt_client_id:
				return
			if message.topic == mqtt_topic_base + "/" + mqtt_topic_broadcast:
				on_mqtt_broadcast_message(remote_client_id, message.topic, mqtt_message[end_of_client_id + 2:])
				return
			if (mqtt_topic_group is not None) and (message.topic == mqtt_topic_base + "/" + mqtt_topic_group):
				on_mqtt_group_broadcast_message(remote_client_id, message.topic, mqtt_message[end_of_client_id + 2:])
				return
			if (((mqtt_topic_group is None) and (message.topic == mqtt_topic_base + "/" + mqtt_topic_identification)) or \
					((mqtt_topic_group is not None) and (message.topic == mqtt_topic_base + "/" + mqtt_topic_group + "/" + mqtt_topic_identification))):
				on_mqtt_single_message(remote_client_id, message.topic, mqtt_message[end_of_client_id + 2:])
				return
	log_message("Invalid MQTT message received on topic '" + message.topic + "': '" + mqtt_message + "'!")
		
# MQTT messages on topic #/broadcast.
def on_mqtt_broadcast_message(remote_client_id, topic, mqtt_message):
	log_message("MQTT broadcast received from '" + remote_client_id + "' on topic '" + topic + "': '" + mqtt_message + "'!")
	if mqtt_message == mqtt_broadcast_identify_message:
		mqtt_send_state()
	elif (mqtt_message == mqtt_off_message):
		if switch_light_state_on:
			switch_light_off("Light switched off by MQTT broadcast!")
		else:
			mqtt_send_state()
	elif mqtt_message == mqtt_on_message:
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT broadcast!", False)
		else:
			cancel_flash_notification()
			mqtt_send_state()
	elif (mqtt_message.startswith(mqtt_flash_message + " ") or (mqtt_message == mqtt_flash_message)):
		initial_blink_off = True
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT flash broadcast!", True)
			initial_blink_off = False
		else:
			cancel_flash_notification()
			mqtt_send_state()
		if mqtt_message == mqtt_flash_message:
			start_flash(default_flash_time, initial_blink_off)
		else:
			start_flash(int(mqtt_message.split()[-1]), initial_blink_off)

# MQTT messages on topic #/(mqtt_topic_group).
def on_mqtt_group_broadcast_message(remote_client_id, topic, mqtt_message):
	log_message("MQTT group broadcast received from '" + remote_client_id + "' on topic '" + topic + "': '" + mqtt_message + "'!")
	if (mqtt_message == mqtt_off_message):
		if switch_light_state_on:
			switch_light_off("Light switched off by MQTT group broadcast!")
		else:
			mqtt_send_state()
	elif mqtt_message == mqtt_on_message:
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT group broadcast!", False)
		else:
			cancel_flash_notification()
			mqtt_send_state()
	elif (mqtt_message.startswith(mqtt_flash_message + " ") or (mqtt_message == mqtt_flash_message)):
		initial_blink_off = True
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT flash group broadcast!", True)
			initial_blink_off = False
		else:
			cancel_flash_notification()
			mqtt_send_state()
		if mqtt_message == mqtt_flash_message:
			start_flash(default_flash_time, initial_blink_off)
		else:
			start_flash(int(mqtt_message.split()[-1]), initial_blink_off)

# MQTT messages on topic #/[(group)/](identification).
def on_mqtt_single_message(remote_client_id, topic, mqtt_message):
	global light_events_json
	log_message("MQTT message received from '" + remote_client_id + "' on topic '" + topic + "': '" + mqtt_message + "'!")
	if switch_light_state_on and (mqtt_message == mqtt_off_message):
		switch_light_off("Light switched off by MQTT message!")
	elif mqtt_message == mqtt_on_message:
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT message!", False)
		else:
			cancel_flash_notification()
	elif (mqtt_message.startswith(mqtt_flash_message + " ") or (mqtt_message == mqtt_flash_message)):
		initial_blink_off = True
		if not switch_light_state_on:
			switch_light_on("Light switched on by MQTT flash message!", True)
			initial_blink_off = False
		else:
			cancel_flash_notification()
		if mqtt_message == mqtt_flash_message:
			start_flash(default_flash_time, initial_blink_off)
		else:
			start_flash(int(mqtt_message.split()[-1]), initial_blink_off)
	elif mqtt_message == mqtt_state_message:
		acknowledge_gpio_leds()
		mqtt_send_state()
	elif mqtt_message == mqtt_reload_message:
		acknowledge_gpio_leds()
		reload_tables(True)
	elif mqtt_message == mqtt_get_json_message:
		acknowledge_gpio_leds()
		mqtt_send_message(mqtt_json_message + " " + json.dumps(light_events_json))
	elif mqtt_message.startswith(mqtt_set_json_message + " "):
		acknowledge_gpio_leds()
		if separate_clock_rules:
			light_events_json_save = light_events_json 
			light_events_json = json.loads(mqtt_message[(len(mqtt_set_json_message) + 1):])
			try:
				with open(light_events_file, "w") as outfile:
					json.dump(light_events_json, outfile, indent=1)
			except IOError:
				log_message("File '" + light_events_file + "' could not be written!")
				mqtt_send_message(mqtt_json_error_message)
				light_events_json = light_events_json_save
			else:
				reload_tables(False)
				ipc_send(False, ipc_reload_tables)
				mqtt_send_message(mqtt_json_updated_message)
	elif mqtt_message == mqtt_get_events_message:
		acknowledge_gpio_leds()
		mqtt_send_message(mqtt_events_message + " " + return_switch_table())
	elif mqtt_message == mqtt_get_configuration_message:
		acknowledge_gpio_leds()
		mqtt_send_message(mqtt_configuration_message + " " + json.dumps(return_configuration()))
	elif mqtt_message.startswith(mqtt_set_configuration_message + " "):
		acknowledge_gpio_leds()
		set_configuration(mqtt_message[(len(mqtt_set_configuration_message) + 1):])
	elif mqtt_message.startswith(mqtt_get_solar_message + " "):
		acknowledge_gpio_leds()
		mqtt_send_message(mqtt_solar_message + " " + solar_times_to_json(mqtt_message[(len(mqtt_get_solar_message) + 1):]))
	elif mqtt_message == mqtt_reboot_message:
		acknowledge_gpio_leds()
		reboot_now()
	elif mqtt_message == mqtt_poweroff_message:
		acknowledge_gpio_leds()
		poweroff_now()
	elif (mqtt_message == mqtt_restart_message) & started_by_systemd:
		acknowledge_gpio_leds()
		run("systemctl restart " + systemd_unit, shell=True)


def on_mqtt_connect(client, userdata, flags, rc):
	log_debug("on_mqtt_connect(" + str(rc) + ")")
	global mqtt_support
	if rc == 0:
		mqtt_support = True
		log_message("MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + ") connected!")
		subscribe_result = client.subscribe(mqtt_topic_base + "/" + mqtt_topic_broadcast, 0)
		if subscribe_result[0] == 0:
			log_message("MQTT subscribed to '" + mqtt_topic_base + "/" + mqtt_topic_broadcast + "!")
			if mqtt_topic_group == None:
				subscribe_result = client.subscribe(mqtt_topic_base + "/" + mqtt_topic_identification, 0)
				if subscribe_result[0] == 0:
					log_message("MQTT subscribed to '" + mqtt_topic_base + "/" + mqtt_topic_identification + "!")
				else:
					log_message("MQTT subscription to '" + mqtt_topic_base + "/" + mqtt_topic_identification + "' failed!")
			else:
				subscribe_result = client.subscribe(mqtt_topic_base + "/" + mqtt_topic_group, 0)
				if subscribe_result[0] == 0:
					log_message("MQTT subscribed to '" + mqtt_topic_base + "/" + mqtt_topic_group + "!")
					subscribe_result = client.subscribe(mqtt_topic_base + "/" + mqtt_topic_group + "/" + mqtt_topic_identification, 0)
					if subscribe_result[0] == 0:
						log_message("MQTT subscribed to '" + mqtt_topic_base + "/" + mqtt_topic_group + "/" + mqtt_topic_identification + "!")
					else:
						log_message("MQTT subscription to '" + mqtt_topic_base + "/" + mqtt_topic_group + "/" + mqtt_topic_identification + "' failed!")
				else:
					log_message("MQTT subscription to '" + mqtt_topic_base + "/" + mqtt_topic_group + "' failed!")
		else:
			log_message("MQTT subscription to '" + mqtt_topic_base + "/" + mqtt_topic_broadcast + "' failed!")
		if subscribe_result[0] == 0:
			mqtt_send_state()
		else:
			mqtt_client.loop_stop()
			log_message(program_name + " continuing without MQTT support!")
	else:
		log_message("MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + ") connection refused: " + mqtt.connack_string(rc))
		mqtt_client.loop_stop()
		log_message(program_name + " continuing without MQTT support!")
		
def on_mqtt_disconnect(client, userdata, rc):
	log_message("MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + ") disconnected!")


def mqtt_init():
	global mqtt_thread
	if mqtt_broker is not None:
		mqtt_thread = threading.Thread(target = mqtt_loop)
		mqtt_thread.start()


def ephem_init():
	# See: https://rhodesmill.org/pyephem/rise-set.html
	global ephem_observer
	ephem_observer = ephem.Observer()
	ephem_observer.lat = str(latitude)
	ephem_observer.lon = str(longitude)
	ephem_observer.pressure = 0

	
def general_init():
	global started_by_systemd, systemd_unit, script_name
	script_name = os.path.basename(sys.argv[0])
#	systemd_pid = run("systemctl show " + script_name + " -p MainPID --value", stdout = PIPE, shell = True, universal_newlines = True)
	try:
		systemd_status = run("systemctl status " + str(os.getpid()) + " --lines=0", check = True, shell = True, stdout = PIPE, universal_newlines = True)
	except CalledProcessError:
		pass
	else:
		systemd_unit = systemd_status.stdout.split()[1]
		if systemd_unit.endswith(".service"):
			started_by_systemd = True
	log_message(script_name + ((" (" + systemd_unit + ")") if started_by_systemd else " not") + " started by systemd!")


def mqtt_loop():
	log_debug("mqtt_loop()")
	global mqtt_client, mqtt_broker, nm_init_event
	mqtt_online = False
	log_message("Connecting to MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + ")...")
	while not mqtt_online:
		mqtt_online = True
		try:
			mqtt_client.connect(mqtt_broker, port=mqtt_broker_port)
		except ConnectionRefusedError:
			log_message("MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + ") not available; waiting for it...")
			mqtt_online = False
			nm_init_event.wait(timeout = mqtt_connect_wait)
			if nm_init_event.is_set():
				mqtt_online = False
				break
#			time.sleep(mqtt_connect_wait)
		except Exception as error:
			log_message("Error connecting to MQTT broker (" + mqtt_broker_scheme + "://" + mqtt_broker + ":" + str(mqtt_broker_port) + "): " + str(error) + "!")
			mqtt_online = False
			break

	if mqtt_online:
		mqtt_client.loop_start()
	else:
		mqtt_broker = None
		if (not nm_init_event.is_set()):
			log_message(program_name + " continuing without MQTT support!")

def nm_main_loop():
	main_loop.run()


print_configuration()
set_signal_handler()
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
# Light
if gpio_light_switch_led != 0:
	GPIO.setup(gpio_light_switch_led, GPIO.OUT)
	GPIO.output(gpio_light_switch_led, GPIO.LOW)
if gpio_relay != 0:
	GPIO.setup(gpio_relay, GPIO.OUT)
	GPIO.output(gpio_relay, GPIO.LOW)

if mqtt_broker is None:
	log_message("MQTT broker (mqtt_broker) is not specified; " + program_name + " continuing without MQTT support!")
else:
	mqtt_client_id = socket.gethostname() + ("-[]-" if (satellite == None) else ("-[" + satellite + "]-")) + str(uuid.uuid4())
	mqtt_client = mqtt.Client(client_id = mqtt_client_id)
	if mqtt_broker_scheme != "tcp":
		mqtt_client.tls_set()
	mqtt_client.username_pw_set(mqtt_username, password = mqtt_password)
	mqtt_client.on_connect = on_mqtt_connect
	mqtt_client.on_disconnect = on_mqtt_disconnect
	mqtt_client.on_message = on_mqtt_message
	mqtt_client.reconnect_delay_set(min_delay = 1, max_delay = 10)

general_init()
nm_init_event = threading.Event()
nm_init()
# Button
if gpio_button != 0:
	GPIO.setup(gpio_button, GPIO.IN, pull_up_down = GPIO.PUD_UP)
	GPIO.add_event_detect(gpio_button, GPIO.BOTH, callback = button_pressed)
if button_receive or (not separate_clock_rules):
	satellite_this_message_queue = MessageQueue("/etha-light-switch-" + satellite, flags = os.O_CREAT, read = True, write = True)
	log_message("IPC receiving from '/etha-light-switch-" + satellite + "'!")
	ipc_thread = threading.Thread(target = ipc_receive)
	ipc_thread.start()

main_loop = GObject.MainLoop()
nm_thread = threading.Thread(target = nm_main_loop)
nm_thread.start()

# Signal again, because DBusGMainLoop "eats" the signal
set_signal_handler()
while True:
	signal.pause()
