From 20f7d14495455e87e3e4e20fba3c13ce81351fdd Mon Sep 17 00:00:00 2001 From: zwim <36999612+zwim@users.noreply.github.com> Date: Sat, 25 Sep 2021 11:02:10 +0200 Subject: [PATCH] Plugin: Auto warmth and night mode (#8129) ("Auto nightmode" only on devices without warmth.) --- .../ui/elements/filemanager_menu_order.lua | 1 + frontend/ui/elements/reader_menu_order.lua | 1 + frontend/ui/uimanager.lua | 2 +- frontend/ui/widget/doublespinwidget.lua | 6 +- frontend/ui/widget/spinwidget.lua | 1 + frontend/util.lua | 8 +- plugins/autowarmth.koplugin/_meta.lua | 6 + plugins/autowarmth.koplugin/main.lua | 843 ++++++++++++++++++ plugins/autowarmth.koplugin/suntime.lua | 310 +++++++ 9 files changed, 1174 insertions(+), 4 deletions(-) create mode 100644 plugins/autowarmth.koplugin/_meta.lua create mode 100644 plugins/autowarmth.koplugin/main.lua create mode 100644 plugins/autowarmth.koplugin/suntime.lua diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index 2a81927f6..343124d42 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -80,6 +80,7 @@ local order = { "----------------------------", "screen_dpi", "screen_eink_opt", + "autowarmth", "color_rendering", "----------------------------", "screen_timeout", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index fca001669..97a60b825 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -120,6 +120,7 @@ local order = { "----------------------------", "screen_dpi", "screen_eink_opt", + "autowarmth", "color_rendering", "----------------------------", "screen_timeout", diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index 99418d1c6..ca4b3d6d9 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -570,7 +570,7 @@ end dbg:guard(UIManager, 'schedule', function(self, time, action) assert(time.sec >= 0, "Only positive time allowed") - assert(action ~= nil) + assert(action ~= nil, "No action") end) --[[-- diff --git a/frontend/ui/widget/doublespinwidget.lua b/frontend/ui/widget/doublespinwidget.lua index c3562999e..533a81e32 100644 --- a/frontend/ui/widget/doublespinwidget.lua +++ b/frontend/ui/widget/doublespinwidget.lua @@ -86,7 +86,8 @@ function DoubleSpinWidget:update() value_max = self.left_max, value_step = self.left_step, value_hold_step = self.left_hold_step, - wrap = false, + precision = self.precision, + wrap = self.left_wrap or false, } local right_widget = NumberPickerWidget:new{ show_parent = self, @@ -95,7 +96,8 @@ function DoubleSpinWidget:update() value_max = self.right_max, value_step = self.right_step, value_hold_step = self.right_hold_step, - wrap = false, + precision = self.precision, + wrap = self.right_wrap or false, } local left_vertical_group = VerticalGroup:new{ align = "center", diff --git a/frontend/ui/widget/spinwidget.lua b/frontend/ui/widget/spinwidget.lua index 5f3706aac..ca7f6ef1a 100644 --- a/frontend/ui/widget/spinwidget.lua +++ b/frontend/ui/widget/spinwidget.lua @@ -85,6 +85,7 @@ function SpinWidget:update() value_step = self.value_step, value_hold_step = self.value_hold_step, precision = self.precision, + wrap = self.wrap or false, } local value_group = HorizontalGroup:new{ align = "center", diff --git a/frontend/util.lua b/frontend/util.lua index bf2ae6cd2..80daa2929 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -113,7 +113,13 @@ Source: https://gist.github. ---- @treturn string clock string in the form of 00:00 or 00:00:00 function util.secondsToClock(seconds, withoutSeconds) seconds = tonumber(seconds) - if seconds == 0 or seconds ~= seconds then + if not seconds then + if withoutSeconds then + return "--:--" + else + return "--:--:--" + end + elseif seconds == 0 or seconds ~= seconds then if withoutSeconds then return "00:00" else diff --git a/plugins/autowarmth.koplugin/_meta.lua b/plugins/autowarmth.koplugin/_meta.lua new file mode 100644 index 000000000..166adf5e5 --- /dev/null +++ b/plugins/autowarmth.koplugin/_meta.lua @@ -0,0 +1,6 @@ +local _ = require("gettext") +return { + name = "autowarmth", + fullname = require("device"):hasNaturalLight() and _("Auto warmth and night mode") or _("Auto night mode"), + description = _([[This plugin allows set the frontlight warmth automagically.]]), +} diff --git a/plugins/autowarmth.koplugin/main.lua b/plugins/autowarmth.koplugin/main.lua new file mode 100644 index 000000000..07809eb9b --- /dev/null +++ b/plugins/autowarmth.koplugin/main.lua @@ -0,0 +1,843 @@ +--[[-- +@module koplugin.autowarmth + +Plugin for setting screen warmth based on the sun position and/or a time schedule +]] + +local Device = require("device") + +local ConfirmBox = require("ui/widget/confirmbox") +local DoubleSpinWidget = require("/ui/widget/doublespinwidget") +local DeviceListener = require("device/devicelistener") +local Dispatcher = require("dispatcher") +local FFIUtil = require("ffi/util") +local InfoMessage = require("ui/widget/infomessage") +local InputDialog = require("ui/widget/inputdialog") +local Font = require("ui/font") +local Notification = require("ui/widget/notification") +local SpinWidget = require("ui/widget/spinwidget") +local SunTime = require("suntime") +local UIManager = require("ui/uimanager") +local WidgetContainer = require("ui/widget/container/widgetcontainer") +local _ = require("gettext") +local T = FFIUtil.template +local Screen = require("device").screen +local util = require("util") + +local activate_sun = 1 +local activate_schedule = 2 +local activate_closer_noon = 3 +local activate_closer_midnight =4 + +local midnight_index = 11 + +local device_max_warmth = Device:hasNaturalLight() and Device.powerd.fl_warmth_max or 100 +local device_warmth_fit_scale = device_max_warmth / 100 + +local function frac(x) + return x - math.floor(x) +end + +local AutoWarmth = WidgetContainer:new{ + name = "autowarmth", + easy_mode = G_reader_settings:nilOrTrue("autowarmth_easy_mode"), + activate = G_reader_settings:readSetting("autowarmth_activate") or 0, + location = G_reader_settings:readSetting("autowarmth_location") or "Geysir", + latitude = G_reader_settings:readSetting("autowarmth_latitude") or 64.31, --great Geysir in Iceland + longitude = G_reader_settings:readSetting("autowarmth_longitude") or -20.30, + altitude = G_reader_settings:readSetting("autowarmth_altitude") or 200, + timezone = G_reader_settings:readSetting("autowarmth_timezone") or 0, + scheduler_times = G_reader_settings:readSetting("autowarmth_scheduler_times") or + {0.0, 5.5, 6.0, 6.5, 7.0, 13.0, 21.5, 22.0, 22.5, 23.0, 24.0}, + warmth = G_reader_settings:readSetting("autowarmth_warmth") + or { 90, 90, 80, 60, 20, 20, 20, 60, 80, 90, 90}, + sched_times = {}, + sched_funcs = {}, -- necessary for unschedule, function, warmth +} + +-- get timezone offset in hours (including dst) +function AutoWarmth:getTimezoneOffset() + local utcdate = os.date("!*t") + local localdate = os.date("*t") + return os.difftime(os.time(localdate), os.time(utcdate))/3600 +end + +function AutoWarmth:init() + self:onDispatcherRegisterActions() + self.ui.menu:registerToMainMenu(self) + + G_reader_settings:saveSetting("autowarmth_easy_mode", self.easy_mode) + -- schedule recalculation shortly afer midnight + self:scheduleMidnightUpdate() +end + +function AutoWarmth:onDispatcherRegisterActions() + Dispatcher:registerAction("show_ephemeris", + {category="none", event="ShowEphemeris", title=_("Show ephemeris"), general=true}) + Dispatcher:registerAction("auto_warmth_off", + {category="none", event="AutoWarmthOff", title=_("Auto warmth off"), screen=true}) + Dispatcher:registerAction("auto_warmth_cycle_trough", + {category="none", event="AutoWarmthMode", title=_("Auto warmth cycle through modes"), screen=true}) +end + +function AutoWarmth:onShowEphemeris() + self:showTimesInfo(_("Information about the sun in"), true, activate_sun, false) +end + +function AutoWarmth:onAutoWarmthOff() + self.activate = 0 + G_reader_settings:saveSetting("autowarmth_activate", self.activate) + Notification:notify(_("Auto warmth turned off")) + self:scheduleMidnightUpdate() +end + +function AutoWarmth:onAutoWarmthMode() + if self.activate > 0 then + self.activate = self.activate - 1 + else + self.activate = activate_closer_midnight + end + local notify_text + if self.activate == 0 then + notify_text = _("Auto warmth turned off") + elseif self.activate == activate_sun then + notify_text = _("Auto warmth use sun position") + elseif self.activate == activate_schedule then + notify_text = _("Auto warmth use schedule") + elseif self.activate == activate_closer_midnight then + notify_text = _("Auto warmth use whatever is closer to midnight") + elseif self.activate == activate_closer_noon then + notify_text = _("Auto warmth use whatever is closer to noon") + end + G_reader_settings:saveSetting("autowarmth_activate", self.activate) + Notification:notify(notify_text) + self:scheduleMidnightUpdate() +end + +function AutoWarmth:onResume() + if self.activate == 0 then return end + + local resume_date = os.date("*t") + + -- check if resume and suspend are done on the same day + if resume_date.day == SunTime.date.day and resume_date.month == SunTime.date.month + and resume_date.year == SunTime.date.year then + local now = SunTime:getTimeInSec(resume_date) + self:scheduleWarmthChanges(now) + else + self:scheduleMidnightUpdate() -- resume is on the other day, do all calcs again + end +end + +-- wrapper for unscheduling, so that only our setWarmth gets unscheduled +function AutoWarmth.setWarmth(val) + if val then + if val > 100 then + DeviceListener:onSetNightMode(true) + else + DeviceListener:onSetNightMode(false) + end + if Device:hasNaturalLight() then + val = math.min(val, 100) + Device.powerd:setWarmth(val) + end + end +end + +function AutoWarmth:scheduleMidnightUpdate() + -- first unschedule all old functions + UIManager:unschedule(self.scheduleMidnightUpdate) -- when called from menu or resume + + local toRad = math.pi / 180 + SunTime:setPosition(self.location, self.latitude * toRad, self.longitude * toRad, + self.timezone, self.altitude) + SunTime:setAdvanced() + SunTime:setDate() -- today + SunTime:calculateTimes() + + self.sched_times = {} + self.sched_funcs = {} + + local function prepareSchedule(times, index1, index2) + local time1 = times[index1] + if not time1 then return end + + local time = SunTime:getTimeInSec(time1) + table.insert(self.sched_times, time) + table.insert(self.sched_funcs, {AutoWarmth.setWarmth, self.warmth[index1]}) + + local time2 = times[index2] + if not time2 then return end -- to near to the pole + local warmth_diff = math.min(self.warmth[index2], 100) - math.min(self.warmth[index1], 100) + if warmth_diff ~= 0 then + local time_diff = SunTime:getTimeInSec(time2) - time + local delta_t = time_diff / math.abs(warmth_diff) -- can be inf, no problem + local delta_w = warmth_diff > 0 and 1 or -1 + for i = 1, math.abs(warmth_diff)-1 do + local next_warmth = math.min(self.warmth[index1], 100) + delta_w * i + -- only apply warmth for steps the hardware has (e.g. Tolino has 0-10 hw steps + -- which map to warmth 0, 10, 20, 30 ... 100) + if frac(next_warmth * device_warmth_fit_scale) == 0 then + table.insert(self.sched_times, time + delta_t * i) + table.insert(self.sched_funcs, {self.setWarmth, + math.floor(math.min(self.warmth[index1], 100) + delta_w * i)}) + end + end + end + end + + if self.activate == activate_sun then + self.current_times = {unpack(SunTime.times, 1, midnight_index)} + elseif self.activate == activate_schedule then + self.current_times = {unpack(self.scheduler_times, 1, midnight_index)} + else + self.current_times = {unpack(SunTime.times, 1, midnight_index)} + if self.activate == activate_closer_noon then + for i = 1, midnight_index do + if not self.current_times[i] then + self.current_times[i] = self.scheduler_times[i] + elseif self.scheduler_times[i] and + math.abs(self.current_times[i]%24 - 12) > math.abs(self.scheduler_times[i]%24 - 12) then + self.current_times[i] = self.scheduler_times[i] + end + end + else -- activate_closer_midnight + for i = 1, midnight_index do + if not self.current_times[i] then + self.current_times[i] = self.scheduler_times[i] + elseif self.scheduler_times[i] and + math.abs(self.current_times[i]%24 - 12) < math.abs(self.scheduler_times[i]%24 - 12) then + self.current_times[i] = self.scheduler_times[i] + end + end + end + end + + if self.easy_mode then + self.current_times[1] = nil + self.current_times[2] = nil + self.current_times[3] = nil + self.current_times[6] = nil + self.current_times[9] = nil + self.current_times[10] = nil + self.current_times[11] = nil + end + + -- here are dragons + local i = 1 + -- find first valid entry + while not self.current_times[i] and i <= midnight_index do + i = i + 1 + end + local next + while i <= midnight_index do + next = i + 1 + -- find next valid entry + while not self.current_times[next] and next <= midnight_index do + next = next + 1 + end + prepareSchedule(self.current_times, i, next) + i = next + end + + local now = SunTime:getTimeInSec() + + -- reschedule 5sec after midnight + UIManager:scheduleIn(24*3600 + 5 - now, self.scheduleMidnightUpdate, self ) + + self:scheduleWarmthChanges(now) +end + +function AutoWarmth:scheduleWarmthChanges(time) + for i = 1, #self.sched_funcs do -- loop not essential, as unschedule unschedules all functions at once + if not UIManager:unschedule(self.sched_funcs[i][1]) then + break + end + end + + local actual_warmth + for i = 1, #self.sched_funcs do + if self.sched_times[i] > time then + UIManager:scheduleIn( self.sched_times[i] - time, + self.sched_funcs[i][1], self.sched_funcs[i][2]) + else + actual_warmth = self.sched_funcs[i][2] or actual_warmth + end + end + -- update current warmth directly + self.setWarmth(actual_warmth) +end + +function AutoWarmth:hoursToClock(hours) + if hours then + hours = hours % 24 * 3600 + 0.01 -- round up, due to reduced precision in settings.reader.lua + end + return util.secondsToClock(hours, self.easy_mode) +end + +function AutoWarmth:addToMainMenu(menu_items) + menu_items.autowarmth = { + text = Device:hasNaturalLight() and _("Auto warmth and night mode") + or _("Auto night mode"), + checked_func = function() return self.activate ~= 0 end, + sub_item_table_func = function() + return self:getSubMenuItems() + end, + } +end + +local function tidy_menu(menu, request) + for i = #menu, 1, -1 do + if menu[i].mode ~=nil then + if menu[i].mode ~= request then + table.remove(menu,i) + else + menu[i].mode = nil + end + end + end + return menu +end + +local about_text = _([[Set the frontlight warmth (if available) and night mode based on a time schedule or the sun's position. + +There are three types of twilight: + +• Civil: You can read a newspaper +• Nautical: You can see the first stars +• Astronomical: It is really dark + +Custom warmth values can be set for every kind of twilight and sunrise, noon, sunset and midnight. +The screen warmth is continuously adjusted to the current time. + +To use the sun's position, a geographical location must be entered. The calculations are very precise, with a deviation less than minute and a half.]]) +function AutoWarmth:getSubMenuItems() + return { + { + text = Device:hasNaturalLight() and _("About auto warmth and night mode") + or _("About auto night mode"), + callback = function() + UIManager:show(InfoMessage:new{ + text = about_text, + width = math.floor(Screen:getWidth() * 0.9), + }) + end, + keep_menu_open = true, + separator = true, + }, + { + text = _("Expert mode"), + checked_func = function() + return not self.easy_mode + end, + help_text = _("In the expert mode, different types of twilight can be used in addition to civil twilight."), + callback = function(touchmenu_instance) + self.easy_mode = not self.easy_mode + G_reader_settings:saveSetting("autowarmth_easy_mode", self.easy_mode) + self:scheduleMidnightUpdate() + touchmenu_instance.item_table = self:getSubMenuItems() + touchmenu_instance:updateItems() + end, + keep_menu_open = true, + }, + { + text = _("Activate"), + checked_func = function() + return self.activate ~= 0 + end, + sub_item_table = self:getActivateMenu(), + }, + { + text = _("Location settings"), + enabled_func = function() return self.activate ~= activate_schedule end, + sub_item_table = self:getLocationMenu(), + }, + { + text = _("Schedule settings"), + enabled_func = function() return self.activate ~= activate_sun end, + sub_item_table = self:getScheduleMenu(), + }, + { + text = Device:hasNaturalLight() and _("Warmth and night mode settings") + or _("Night mode settings"), + sub_item_table = self:getWarmthMenu(), + separator = true, + }, + self:getTimesMenu(_("Active parameters")), + self:getTimesMenu(_("Information about the sun in"), true, activate_sun), + self:getTimesMenu(_("Information about the schedule"), false, activate_schedule), + } +end + +function AutoWarmth:getActivateMenu() + local function getActivateMenuEntry(text, activator) + return { + text = text, + checked_func = function() return self.activate == activator end, + callback = function() + if self.activate ~= activator then + self.activate = activator + else + self.activate = 0 + end + G_reader_settings:saveSetting("autowarmth_activate", self.activate) + self:scheduleMidnightUpdate() + end, + } + end + + return { + getActivateMenuEntry( _("Sun position"), activate_sun), + getActivateMenuEntry( _("Time schedule"), activate_schedule), + getActivateMenuEntry( _("Whatever is closer to noon"), activate_closer_noon), + getActivateMenuEntry( _("Whatever is closer to midnight"), activate_closer_midnight), + } +end + +function AutoWarmth:getLocationMenu() + return {{ + text_func = function() + if self.location ~= "" then + return T(_("Location: %1"), self.location) + else + return _("Location") + end + end, + callback = function(touchmenu_instance) + local location_name_dialog + location_name_dialog = InputDialog:new{ + title = _("Location name"), + input = self.location, + buttons = { + { + { + text = _("Cancel"), + callback = function() + UIManager:close(location_name_dialog) + end, + }, + { + text = _("OK"), + callback = function() + self.location = location_name_dialog:getInputText() + G_reader_settings:saveSetting("autowarmth_location", + self.location) + UIManager:close(location_name_dialog) + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + }, + }} + } + UIManager:show(location_name_dialog) + location_name_dialog:onShowKeyboard() + end, + keep_menu_open = true, + }, + { + text_func = function() + return T(_("Coordinates: (%1, %2)"), self.latitude, self.longitude) + end, + callback = function(touchmenu_instance) + local location_widget = DoubleSpinWidget:new{ + title_text = _("Set location"), + info_text = _("Enter decimal degrees, northern hemisphere and eastern length are '+'."), + left_text = _("Latitude"), + left_value = self.latitude, + left_default = 0, + left_min = -90, + left_max = 90, + left_step = 0.1, + precision = "%0.2f", + left_hold_step = 5, + right_text = _("Longitude"), + right_value = self.longitude, + right_default = 0, + right_min = -180, + right_max = 180, + right_step = 0.1, + right_hold_step = 5, + callback = function(lat, long) + self.latitude = lat + self.longitude = long + self.timezone = self:getTimezoneOffset() -- use timezone of device + G_reader_settings:saveSetting("autowarmth_latitude", self.latitude) + G_reader_settings:saveSetting("autowarmth_longitude", self.longitude) + G_reader_settings:saveSetting("autowarmth_timezone", self.timezone) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + } + UIManager:show(location_widget) + end, + keep_menu_open = true, + }, + { + text_func = function() + return T(_("Altitude: %1m"), self.altitude) + end, + callback = function(touchmenu_instance) + UIManager:show(SpinWidget:new{ + title_text = _("Altitude"), + value = self.altitude, + value_min = -100, + value_max = 15000, -- intercontinental flight + wrap = false, + value_step = 10, + value_hold_step = 100, + ok_text = _("Set"), + callback = function(spin) + self.altitude = spin.value + G_reader_settings:saveSetting("autowarmth_altitude", self.altitude) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + extra_text = _("Default"), + extra_callback = function() + self.altitude = 200 + G_reader_settings:saveSetting("autowarmth_altitude", self.altitude) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + }) + end, + keep_menu_open = true, + }} +end + +function AutoWarmth:getScheduleMenu() + local function store_times(touchmenu_instance, new_time, num) + self.scheduler_times[num] = new_time + if num == 1 then + if new_time then + self.scheduler_times[midnight_index] + = new_time + 24 -- next day + else + self.scheduler_times[midnight_index] = nil + end + end + G_reader_settings:saveSetting("autowarmth_scheduler_times", + self.scheduler_times) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end + -- mode == nil ... show alway + -- == true ... easy mode + -- == false ... expert mode + local function getScheduleMenuEntry(text, num, mode) + return { + mode = mode, + text_func = function() + return T(_"%1: %2", text, + self:hoursToClock(self.scheduler_times[num])) + end, + checked_func = function() + return self.scheduler_times[num] ~= nil + end, + callback = function(touchmenu_instance) + local hh = 12 + local mm = 0 + if self.scheduler_times[num] then + hh = math.floor(self.scheduler_times[num]) + mm = math.floor(frac(self.scheduler_times[num]) * 60 + 0.5) + end + UIManager:show(DoubleSpinWidget:new{ + title_text = _("Set time"), + left_text = _("HH"), + left_value = hh, + left_default = 0, + left_min = 0, + left_max = 23, + left_step = 1, + left_hold_step = 3, + left_wrap = true, + right_text = _("MM"), + right_value = mm, + right_default = 0, + right_min = 0, + right_max = 59, + right_step = 1, + right_hold_step = 5, + right_wrap = true, + callback = function(left, right) + local new_time = left + right / 60 + local function get_valid_time(n, dir) + for i = n+dir, dir > 0 and midnight_index or 1, dir do + if self.scheduler_times[i] then + return self.scheduler_times[i] + end + end + return dir > 0 and 0 or 26 + end + if num > 1 and new_time < get_valid_time(num, -1) then + UIManager:show(ConfirmBox:new{ + text = _("This time is before the previous time.\nAdjust the previous time?"), + ok_callback = function() + for i = num-1, 1, -1 do + if self.scheduler_times[i] then + if new_time < self.scheduler_times[i] then + self.scheduler_times[i] = new_time + else + break + end + end + end + store_times(touchmenu_instance, new_time, num) + end, + }) + elseif num < 10 and new_time > get_valid_time(num, 1) then + UIManager:show(ConfirmBox:new{ + text = _("This time is after the subsequent time.\nAdjust the subsequent time?"), + ok_callback = function() + for i = num + 1, midnight_index - 1 do + if self.scheduler_times[i] then + if new_time > self.scheduler_times[i] then + self.scheduler_times[i] = new_time + else + break + end + end + end + store_times(touchmenu_instance, new_time, num) + end, + }) + else + store_times(touchmenu_instance, new_time, num) + end + end, + extra_text = _("Invalidate"), + extra_callback = function() + store_times(touchmenu_instance, nil, num) + end, + }) + end, + keep_menu_open = true, + } + end + + local retval = { + getScheduleMenuEntry(_("Solar midnight"), 1, false ), + getScheduleMenuEntry(_("Astronomical dawn"), 2, false), + getScheduleMenuEntry(_("Nautical dawn"), 3, false), + getScheduleMenuEntry(_("Civil dawn"), 4), + getScheduleMenuEntry(_("Sunrise"), 5), + getScheduleMenuEntry(_("Solar noon"), 6, false), + getScheduleMenuEntry(_("Sunset"), 7), + getScheduleMenuEntry(_("Civil dusk"), 8), + getScheduleMenuEntry(_("Nautical dusk"), 9, false), + getScheduleMenuEntry(_("Astronomical dusk"), 10, false), + } + + return tidy_menu(retval, self.easy_mode) +end + +function AutoWarmth:getWarmthMenu() + -- mode == nil ... show alway + -- == true ... easy mode + -- == false ... expert mode + local function getWarmthMenuEntry(text, num, mode) + return { + mode = mode, + text_func = function() + if Device:hasNaturalLight() then + if self.warmth[num] <= 100 then + return T(_("%1: %2%"), text, self.warmth[num]) + else + return T(_("%1: 100% + ☾"), text) + end + else + if self.warmth[num] <= 100 then + return T(_("%1: ☼"), text) + else + return T(_("%1: ☾"), text) + end + end + end, + callback = function(touchmenu_instance) + if Device:hasNaturalLight() then + UIManager:show(SpinWidget:new{ + title_text = text, + value = self.warmth[num], + value_min = 0, + value_max = 100, + wrap = false, + value_step = math.floor(100 / device_max_warmth), + value_hold_step = 10, + ok_text = _("Set"), + callback = function(spin) + self.warmth[num] = spin.value + self.warmth[#self.warmth - num + 1] = spin.value + G_reader_settings:saveSetting("autowarmth_warmth", self.warmth) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + extra_text = _("Use night mode"), + extra_callback = function() + self.warmth[num] = 110 + self.warmth[#self.warmth - num + 1] = 110 + G_reader_settings:saveSetting("autowarmth_warmth", self.warmth) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + }) + else + UIManager:show(ConfirmBox:new{ + text = _("Nightmode"), + ok_text = _("Set"), + ok_callback = function() + self.warmth[num] = 110 + self.warmth[#self.warmth - num + 1] = 110 + G_reader_settings:saveSetting("autowarmth_warmth", self.warmth) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + cancel_text = _("Unset"), + cancel_callback = function() + self.warmth[num] = 0 + self.warmth[#self.warmth - num + 1] = 0 + G_reader_settings:saveSetting("autowarmth_warmth", self.warmth) + self:scheduleMidnightUpdate() + if touchmenu_instance then touchmenu_instance:updateItems() end + end, + other_buttons = {{ + { + text = _("Cancel"), + } + }}, + + }) + end + end, + + keep_menu_open = true, + } + end + + local retval = { + { + text = Device:hasNaturalLight() and _("Set warmth and night mode for:") + or _("Set night mode for:"), + enabled_func = function() return false end, + }, + getWarmthMenuEntry(_("Solar noon"), 6, false), + getWarmthMenuEntry(_("Daytime"), 5), + getWarmthMenuEntry(_("Darkest time of civil dawn"), 4, false), + getWarmthMenuEntry(_("Darkest time of civil twilight"), 4, true), + getWarmthMenuEntry(_("Darkest time of nautical dawn"), 3, false), + getWarmthMenuEntry(_("Darkest time of astronomical dawn"), 2, false), + getWarmthMenuEntry(_("Midnight"), 1, false), + } + + return tidy_menu(retval, self.easy_mode) +end + +-- title +-- location: add a location string +-- activator: nil .. current_times, +-- activate_sun .. sun times +-- activate_schedule .. scheduler times +-- request_easy: true if easy_mode should be used +function AutoWarmth:showTimesInfo(title, location, activator, request_easy) + local times + if not activator then + times = self.current_times + elseif activator == activate_sun then + times = SunTime.times + elseif activator == activate_schedule then + times = self.scheduler_times + end + + -- text to show + -- t .. times + -- num .. index in times + local function info_line(text, t, num, easy) + local retval = text .. self:hoursToClock(t[num]) + if easy then + if t[num] and self.current_times[num] and self.current_times[num] ~= t[num] then + return text .. "\n" + else + return "" + end + end + + if not t[num] then -- entry deactivated + return retval .. "\n" + elseif Device:hasNaturalLight() then + if self.current_times[num] == t[num] then + if self.warmth[num] <= 100 then + return retval .. " (💡" .. self.warmth[num] .."%)\n" + else + return retval .. " (💡100% + ☾)\n" + end + else + return retval .. "\n" + end + else + if self.current_times[num] == t[num] then + if self.warmth[num] <= 100 then + return retval .. " (☼)\n" + else + return retval .. " (☾)\n" + end + else + return retval .. "\n" + end + end + end + + local location_string = "" + if location then + location_string = " " .. self:getLocationString() + end + + UIManager:show(InfoMessage:new{ + face = Font:getFace("scfont"), + width = math.floor(Screen:getWidth() * (self.easy_mode and 0.75 or 0.90)), + text = title .. location_string .. ":\n\n" .. + info_line(_("Solar midnight: "), times, 1, request_easy) .. + _(" Dawn\n") .. + info_line(_(" Astronomic: "), times, 2, request_easy) .. + info_line(_(" Nautical: "), times, 3, request_easy).. + info_line(_(" Civil: "), times, 4) .. + _(" Dawn\n") .. + info_line(_("Sunrise: "), times, 5) .. + info_line(_("\nSolar noon: "), times, 6, request_easy) .. + + info_line(_("\nSunset: "), times, 7) .. + _(" Dusk\n") .. + info_line(_(" Civil: "), times, 8) .. + info_line(_(" Nautical: "), times, 9, request_easy) .. + info_line(_(" Astronomic: "), times, 10, request_easy) .. + _(" Dusk\n") .. + info_line(_("Solar midnight: "), times, midnight_index, request_easy) + }) +end + +-- title +-- location: add a location string +-- activator: nil .. current_times, +-- activate_sun .. sun times +-- activate_schedule .. scheduler times +function AutoWarmth:getTimesMenu(title, location, activator) + return { + text_func = function() + if location then + return title .. " " .. self:getLocationString() + end + return title + end, + callback = function() + self:showTimesInfo(title, location, activator, self.easy_mode) + end, + keep_menu_open = true, + } +end + +function AutoWarmth:getLocationString() + if self.location ~= "" then + return self.location + else + return "(" .. self.latitude .. "," .. self.longitude .. ")" + end +end + +return AutoWarmth diff --git a/plugins/autowarmth.koplugin/suntime.lua b/plugins/autowarmth.koplugin/suntime.lua new file mode 100644 index 000000000..98b8d8334 --- /dev/null +++ b/plugins/autowarmth.koplugin/suntime.lua @@ -0,0 +1,310 @@ + +-- usage +-- SunTime:setPosition() +-- SunTime:setSimple() or SunTime:setAdvanced() +-- SunTime:setDate() +-- SunTime:calculate(height, hour) height==Rad(0°)-> Midday; hour=6 or 18 for rise or set +-- SunTime:calculateTimes() +-- use values + +-- math abbrevations +local toRad = math.pi/180 +local toDeg = 1/toRad + +local floor = math.floor +local sin = math.sin +local cos = math.cos +local tan = math.tan +local asin = math.asin +local acos = math.acos +local atan = math.atan + +local function Rad(x) + return x*toRad +end + +-------------------------------------------- + +local SunTime = {} + +SunTime.astronomic = Rad(-18) +SunTime.nautic = Rad(-12) +SunTime.civil = Rad(-6) +-- SunTime.eod = Rad(-49/60) -- approx. end of day + +---------------------------------------------------------------- + +-- simple 'Equation of time' good for dates between 2008-2027 +-- errors for latitude 20° are within 1min +-- 47° are within 1min 30sec +-- 65° are within 5min +-- https://www.astronomie.info/zeitgleichung/#Auf-_und_Untergang (German) +function SunTime:getZglSimple() + local T = self.date.yday + return -0.171 * sin(0.0337 * T + 0.465) - 0.1299 * sin(0.01787 * T - 0.168) +end + +-- more advanced 'Equation of time' goot for dates between 1800-2200 +-- errors are better than with the simple method +-- https://de.wikipedia.org/wiki/Zeitgleichung (German) and +-- more infos on http://www.hlmths.de/Scilab/Zeitgleichung.pdf (German) +function SunTime:getZglAdvanced() + local e = self.num_ex + local e2 = e*e + local e3 = e2*e + local e4 = e3*e + local e5 = e4*e + + local M = self.M + -- https://de.wikibooks.org/wiki/Astronomische_Berechnungen_f%C3%BCr_Amateure/_Himmelsmechanik/_Sonne + local C = (2*e - e3/4 + 5/96*e5) * sin(M) + + (5/4*e2 + 11/24*e4) * sin(2*M) + + (13/12*e3 - 43/64*e5) * sin(3*M) + + 103/96*e4 * sin(4*M) + + 1097/960*e5 * sin(5*M) -- rad + + local lamb = self.L + C + local tanL = tan(self.L) + local tanLamb = tan(lamb) + local cosEps = cos(self.epsilon) + + local zgl = atan( (tanL - tanLamb*cosEps) / (1 + tanL*tanLamb*cosEps) ) --rad + return zgl*toDeg/15 -- to hours *4'/60 +end + +-- set current date or year/month/day daylightsaving hh/mm/ss +-- if dst == nil use curent daylight saving of the system +function SunTime:setDate(year, month, day, dst, hour, min, sec) + self.oldDate = self.date + + self.date = os.date("*t") + + if year and month and day and hour and min and sec then + self.date.year = year + self.date.month = month + self.date.day = day + local feb = 28 + if year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) then + feb = 29 + end + local days_in_month = {31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + self.date.yday = day + for i = 1, month-1 do + self.date.yday = self.date.yday + days_in_month[i] + end + self.date.hour = hour or 12 + self.date.min = min or 0 + self.date.sec = sec or 0 + if dst ~= nil then + self.date.isdst = dst + end + end + + -- use cached results + if self.olddate and self.oldDate.day == self.date.day and + self.oldDate.month == self.date.month and + self.oldDate.year == self.date.year and + self.oldDate.isdst == self.date.isdst then + return + end + + self:initVars() + + if not self.getZgl then + self.getZgl = self.getZglAdvanced + end + + self.zgl = self:getZgl() +end + +function SunTime:setPosition(name, latitude, longitude, time_zone, altitude) + altitude = altitude or 200 + + self.oldDate = nil --invalidate cache + self.pos = {name, latitude = latitude, longitude = longitude, altitude = altitude} + self.time_zone = time_zone + self.refract = Rad(33/60 * .5 ^ (altitude / 5500)) +end + +function SunTime:setSimple() + self.getZgl = self.getZglSimple +end +function SunTime:setAdvanced() + self.getZgl = self.getZglAdvanced +end + +function SunTime:daysSince2000() + local delta = self.date.year - 2000 + local leap = floor(delta/4) + local days_since_2000 = delta * 365 + leap + self.date.yday -- WMO No.8 + return days_since_2000 +end + +-- more accurate parameters of earth orbit from +-- Title: Numerical expressions for precession formulae and mean elements for the Moon and the planets +-- Authors: Simon, J. L., Bretagnon, P., Chapront, J., Chapront-Touze, M., Francou, G., & Laskar, J., , +-- Journal: Astronomy and Astrophysics (ISSN 0004-6361), vol. 282, no. 2, p. 663-683 +-- Bibliographic Code: 1994A&A...282..663S +function SunTime:initVars() + self.days_since_2000 = self:daysSince2000() + local T = self.days_since_2000/36525 +-- self.num_ex = 0.016709 - 0.000042 * T +-- self.num_ex = 0.0167086342 - 0.000042 * T + -- see wikipedia: https://de.wikipedia.org/wiki/Erdbahn-> Meeus + self.num_ex = 0.0167086342 + T*(-0.0004203654e-1 + + T*(-0.0000126734e-2 + T*( 0.0000001444e-3 + + T*(-0.0000000002e-4 + T* 0.0000000003e-5)))) + +-- self.epsilon = (23 + 26/60 + 21/3600 - 46.82/3600 * T) * toRad + -- see wikipedia: https://de.wikipedia.org/wiki/Erdbahn-> Meeus + local epsilon = 23 + 26/60 + (21.412 + T*(-46.80927 + + T*(-0.000152 + T*(0.00019989 + + T*(-0.00000051 - T*0.00000025)))))/3600 --° + self.epsilon = epsilon * toRad + + -- local L = (280.4656 + 36000.7690 * T ) --° + -- see Numerical expressions for precession formulae ... + -- shift from time to Equinox as data is given for JD2000.0, but date is in days from 20000101 + local nT = T * 1.0000388062 + --mean longitude + local L = 100.46645683 + (nT*(1295977422.83429E-1 + + nT*(-2.04411E-2 - nT* 0.00523E-3)))/3600--° + self.L = (L - floor(L/360)*360) * toRad + + -- wikipedia: https://de.wikipedia.org/wiki/Erdbahn-> Meeus + local omega = 102.93734808 + nT*(17.194598028e-1 + + nT*( 0.045688325e-2 + nT*(-0.000017680e-3 + + nT*(-0.000033583e-4 + nT*( 0.000000828e-5 + + nT* 0.000000056e-6))))) --° + + -- Mittlere Länage + local M = L - omega + self.M = (M - floor(M/360)*360) * toRad + + -- Deklination nach astronomie.info + -- local decl = 0.4095 * sin(0.016906 * (self.date.yday - 80.086)) + --Deklination nach Brodbeck (2001) + -- local decl = 0.40954 * sin(0.0172 * (self.date.yday - 79.349740)) + + --Deklination nach John Kalisch (derived from WMO-No.8) + --local x = (36000/36525 * (self.date.yday+hour/24) - 2.72)*toRad + --local decl = asin(0.397748 * sin(x + (1.915*sin(x) - 77.51)*toRad)) + + -- Deklination WMO-No.8 page I-7-37 + --local T = self.days_since_2000 + hour/24 + --local L = 280.460 + 0.9856474 * T -- self.M + --L = (L - floor(L/360)*360) * toRad + --local g = 357.528 + 0.9856003 * T + --g = (g - floor(g/360)*360) * toRad + --local l = L + (1.915 * sin (g) + 0.020 * sin (2*g))*toRad + --local ep = self.epsilon + -- -- sin(decl) = sin(ep)*sin(l) + --self.decl = asin(sin(ep)*sin(l)) + + -- Deklination WMO-No.8 page I-7-37 + local l = self.L + math.pi + (1.915 * sin (self.M) + 0.020 * sin (2*self.M))*toRad + self.decl = asin(sin(self.epsilon)*sin(l)) + + -- Nutation see https://de.wikipedia.org/wiki/Nutation_(Astronomie) + local A = { 2.18243920 - 33.7570460 * T, + -2.77624462 + 1256.66393 * T, + 7.62068856 + 16799.4182 * T, + 4.36487839 - 67.140919 * T} + local B = {92025e-4 + 8.9e-4 * T, + 5736e-4 - 3.1e-4 * T, + 977e-4 - 0.5e-4 * T, + -895e-4 + 0.5e-4 * T} + local delta_epsilon = 0 + for i = 1, #A do + delta_epsilon = delta_epsilon + B[i]*cos(A[i]) + end + + -- add nutation to declination + self.decl = self.decl + delta_epsilon/3600 + + -- https://de.wikipedia.org/wiki/Kepler-Gleichung#Wahre_Anomalie + self.E = self.M + self.num_ex * sin(self.M) + self.num_ex^2 / 2 * sin(2*self.M) + self.a = 149598022.96E3 -- große Halbaches in m + self.r = self.a * (1 - self.num_ex * cos(self.E)) +-- self.eod = -atan(6.96342e8/self.r) - Rad(33.3/60) -- without nutation + self.eod = -atan(6.96342e8/self.r) - self.refract -- with nutation +-- ^--sun radius ^- astronomical refraction (500m altitude) +end +-------------------------- + +function SunTime:getTimeDiff(height) + local val = (sin(height) - sin(self.pos.latitude)*sin(self.decl)) + / (cos(self.pos.latitude)*cos(self.decl)) + + if math.abs(val) > 1 then + return + end + return 12/math.pi * acos(val) +end + +-- get time for a certain height +-- set hour to 6 for rise or 18 for set +-- result rise or set time +-- nil sun does not reach the height +function SunTime:calculateTime(height, hour) + hour = hour or 12 + local dst = self.date.isdst and 1 or 0 + local timeDiff = self:getTimeDiff(height, hour) + if not timeDiff then + return + end + + local local_correction = self.time_zone - self.pos.longitude*12/math.pi + dst - self.zgl + if hour < 12 then + return 12 - timeDiff + local_correction + else + return 12 + timeDiff + local_correction + end +end + +function SunTime:calculateTimeIter(height, hour) + return self:calculateTime(height, hour) +end + +function SunTime:calculateTimes() + self.rise = self:calculateTimeIter(self.eod, 6) + self.set = self:calculateTimeIter(self.eod, 18) + + self.rise_civil = self:calculateTimeIter(self.civil, 6) + self.set_civil = self:calculateTimeIter(self.civil, 18) + self.rise_nautic = self:calculateTimeIter(self.nautic, 6) + self.set_nautic = self:calculateTimeIter(self.nautic, 18) + self.rise_astronomic = self:calculateTimeIter(self.astronomic, 6) + self.set_astronomic = self:calculateTimeIter(self.astronomic, 18) + + self.noon = (self.rise + self.set) / 2 + self.midnight = self.noon + 12 + + self.times = {} + self.times[1] = self.noon - 12 + self.times[2] = self.rise_astronomic + self.times[3] = self.rise_nautic + self.times[4] = self.rise_civil + self.times[5] = self.rise + self.times[6] = self.noon + self.times[7] = self.set + self.times[8] = self.set_civil + self.times[9] = self.set_nautic + self.times[10] = self.set_astronomic + self.times[11] = self.noon + 12 +end + +-- get time in seconds (either actual time in hours or date struct) +function SunTime:getTimeInSec(val) + if not val then + val = os.date("*t") + end + + if type(val) == "table" then + return val.hour*3600 + val.min*60 + val.sec + end + + return val*3600 +end + +return SunTime