Browse Source

Plugin: Auto warmth and night mode (#8129)

("Auto nightmode" only on devices without warmth.)
reviewable/pr8261/r1
zwim 2 months ago
committed by GitHub
parent
commit
20f7d14495
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1174 additions and 4 deletions
  1. +1
    -0
      frontend/ui/elements/filemanager_menu_order.lua
  2. +1
    -0
      frontend/ui/elements/reader_menu_order.lua
  3. +1
    -1
      frontend/ui/uimanager.lua
  4. +4
    -2
      frontend/ui/widget/doublespinwidget.lua
  5. +1
    -0
      frontend/ui/widget/spinwidget.lua
  6. +7
    -1
      frontend/util.lua
  7. +6
    -0
      plugins/autowarmth.koplugin/_meta.lua
  8. +843
    -0
      plugins/autowarmth.koplugin/main.lua
  9. +310
    -0
      plugins/autowarmth.koplugin/suntime.lua

+ 1
- 0
frontend/ui/elements/filemanager_menu_order.lua View File

@ -80,6 +80,7 @@ local order = {
"----------------------------",
"screen_dpi",
"screen_eink_opt",
"autowarmth",
"color_rendering",
"----------------------------",
"screen_timeout",

+ 1
- 0
frontend/ui/elements/reader_menu_order.lua View File

@ -120,6 +120,7 @@ local order = {
"----------------------------",
"screen_dpi",
"screen_eink_opt",
"autowarmth",
"color_rendering",
"----------------------------",
"screen_timeout",

+ 1
- 1
frontend/ui/uimanager.lua View File

@ -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)
--[[--

+ 4
- 2
frontend/ui/widget/doublespinwidget.lua View File

@ -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",

+ 1
- 0
frontend/ui/widget/spinwidget.lua View File

@ -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",

+ 7
- 1
frontend/util.lua View File

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

+ 6
- 0
plugins/autowarmth.koplugin/_meta.lua View File

@ -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.]]),
}

+ 843
- 0
plugins/autowarmth.koplugin/main.lua View File

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

+ 310
- 0
plugins/autowarmth.koplugin/suntime.lua View File

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

Loading…
Cancel
Save