[Scripted Automation] Multi-day Time of Day Library

Tags: #<Tag:0x00007f745330d738> #<Tag:0x00007f745330d648>

With the release of Ephemeris (I’ll start working up some docs soon) we now have built into OH 2.5 M4 and later the ability to define day types. For example, weekends, school days, bank holidays, birthdays, etc. I have taken the Design Pattern: Time Of Day Python example and expanded it to use Ephemeris, letting you define a different set of time periods based on the type of day.

I would greatly appreciate someone testing this out before I submit it as a Community library. I’ve done as much testing as I can but as soon as someone else touches some code new errors are found. :wink:

Here is the code. Hopefully the docstrings are clear on how to set it up and use it.

automation/jsr223/python/community/time-of-day/tod_ephem.py

from core.rules import rule
from core.triggers import when
from core.actions import Ephemeris, ScriptExecution
from core.metadata import get_key_value
from core.utils import send_command_if_different, post_update_if_different
import configuration
reload(configuration)
from configuration import tod_group_ephem, tod_item_ephem
import threading
from org.joda.time import DateTime
from time import sleep

# Semiphore for keeping the Rule from triggering while the Items are being
# updated by the tod_update_items function.
updating = threading.BoundedSemaphore(1)

def tod_update_items(log):
    """
    Updates all descendents of tod_item_ephem if they posesses a ToD start_time
    metadata value and that start time string for today is different from the
    current state of the Item. This will thus update the Item's date to today if
    we have passed midnight.

    Arguments:
        - log: Logger passed in from the Rule that calls this function
    """
    # acquire a semiphore to keep these updates from executing the tod Rule
    if not updating.acquire(False):
        log.error("Failed to acquire the semiphore to initialize ToD Items!")
        return

    # loop through all the decendents of the group, for those with metadata
    # refresh the Item with the value in the metadata
    log.info("Updating ToD Items from metadata")

    now = DateTime.now()
    items_initialized = False
    for start_time in ir.getItem(tod_group_ephem).allMembers:

        # Get the time string from the Item metadata
        time_str = get_key_value(start_time.name, "ToD", "start_time")

        # If there is a value, parse it and set the Item to that time for today
        if time_str:
            log.debug("Handling {}".format(start_time))
            time_parts = time_str.split(':')
            num_parts = len(time_parts)
            if num_parts < 2:
                log.error("{} is malformed metadata to initialize"
                          " {}".format(t, start_time.name))
            else:
                tod_t = now.withTime(int(time_parts[0]),
                                     int(time_parts[1]),
                                     int(time_parts[2]) if num_parts > 2 else 0,
                                     int(time_parts[3]) if num_parts > 3 else 0)
                if str(start_time.state) != str(tod_t):
                    if post_update_if_different(start_time, str(tod_t)):
                        log.info("Updated {} to {}".format(start_time.name,
                                                           tod_t))
                        items_initialized = True

    # Sleep for a little bit if one or more Items were updated
    try:
        if items_initialized:
            sleep(0.3) # Give Items a chance to update
    except:
        log.warn("Interrupted while sleeping waiting for Items to finsih "
                 "updating! Hoping for the best.")

    finally:
        # release the semiphore
        updating.release()
        log.info("Done updating ToD Items")

def get_start_times(log):
    def get_group(type, check, curr, is_day):
        """
        Checks to see if there is a Group defined for type and whether today is
        in that that type.

        Arguments:
            - type: the day type.
            - check: check to perform to see if a Group defines the start times
                     for the given type
            - curr: the currently selected Group of start times.
            - is_day: True if today is of type

        Returns:
            - The first found Group that has metadata that matches type if
              is_day is True.
            - curr if is_day is False or there is no Group found for type.
        """

        # skip if it's not this day type
        if not is_day:
            return curr

        # get the group of start times if one exists
        log.debug("Checking for {}:".format(type))
        grps = [grp for grp in tod_grps if check(grp, type)]
        rval = curr
        if len(grps) > 0:
            rval = grps[0]
        if len(grps) > 1:
            log.warn("There is more than one {} Group! Only using the"
                               " first one {}".format(type, rval.name))
        return rval

    def get_groups(type, check, curr, key, day_check):
        """
        Determines if today is defined in any of the custom daysets or custom
        holiday files.

        Arguments:
            - type: the day type
            - check: the test to find the Groups that represent the day type
                     from the list of members of tod_grps_ephem
            - curr: the current Group of start times selected
            - key: name of the key containing additional relevant information
                ("set" for custom daysets or "file" for custom bank holidays)
            - day_check: function to call to determine if today is in the dayset
                         or custom bank holidays file.

        Returns:
            - One of the Groups if today is in the defined dayset or holiday
            file. If there is more than one, it is not fixed which Group is
            returned.
            - curr if today is not a defined in the dayset or custom bank
              holiday file.
        """
        log.debug("Checking for {}:".format(type))
        rval = curr
        for grp in [grp for grp in tod_grps if check(grp, type)]:
            value = get_key_value(grp.name, "ToD", key)
            if value is None:
                log.error("Group {} doesn't have a key {}!"
                                    .format(grp.name, key))
            elif day_check(value):
                rval = curr
        return rval

    tod_grps = ir.getItem(tod_group_ephem).members
    check = lambda grp, type: get_key_value(grp.name, "ToD", "type") == type
    start_times = None

    start_times = get_group("default",
                            lambda grp, type: check(grp, type) or not get_key_value(grp.name, "ToD", "type"),
                            start_times,
                            True)

    start_times = get_group("weekday", check, start_times,
                            not Ephemeris.isWeekend())

    start_times = get_group("weekend", check, start_times, Ephemeris.isWeekend())

    start_times = get_groups("dayset", check, start_times, "set",
                             lambda ds: Ephemeris.isInDayset(ds))

    start_times = get_group("holiday", check, start_times,
                    Ephemeris.getBankHolidayName(0) is not None)

    start_times = get_groups("custom", check, start_times, "file",
                 lambda file: Ephemeris.getBankHolidayName(0, file) is not None)

    ephem_tod.log.info("Creating Time of Day timers using {}".format(start_times.name))
    return start_times

# Time of Day timers to that drive the time of day events.
tod_timers = {}

def clear_timers():
    """ Cancel all the existing time of day timers. """
    for name, timer in tod_timers.items():
        if timer is not None and not timer.hasTerminated():
            timer.cancel()
        del tod_timers[name]

def create_tod_timers(log, start_times):
    """
    Using the passed in Group, create Timers for each start time to command the
    tod_item_ephem to the new time of day state.

    Arguments:
        - log: logger from the Rule
        - start_times: Group with DateTime Items as it's members that indicate
          the start time of a time of day (state) and the name of the state
          defined in metadata.
    """

    now = DateTime.now()
    clear_timers()

    # Create timers for all the members of tod_group.
    most_recent_time = now.minusDays(1)
    most_recent_state = str(items[tod_item_ephem])
    for start_time in start_times.members:

        item_time = DateTime(str(start_time.state))
        trigger_time = now.withTime(item_time.getHourOfDay(),
                                    item_time.getMinuteOfHour(),
                                    item_time.getSecondOfMinute(),
                                    item_time.getMillisOfSecond())

        # Update the Item if it still has yesterday's date.
        if item_time.isBefore(trigger_time):
            events.postUpdate(start_time, str(trigger_time))

        state = get_key_value(start_time.name, "ToD", "tod_state")
        # If there is no state we can't use this Item.
        if state is None:
            log.error("{} does not have tod_state metadata!"
                      .format(start_time.name))

        # If we have already passed this time, keep track of the most recent
        # time and state.
        elif (trigger_time.isBefore(now)
                and trigger_time.isAfter(most_recent_time)):
            most_recent_time = trigger_time
            most_recent_state = state
            log.debug("The most recent state is now {}".format(state))

        # Create future timers
        elif trigger_time.isAfter(now):

            def tod_transition(state, log):
                """
                Called at a time of day transition, commands to the new time of
                day state.
                """
                log.info("Transitioning time of day from {} to {}."
                         .format(items[tod_item_ephem],state))
                events.sendCommand(tod_item_ephem, state)

            log.debug("Setting timer for Item {}, Time {}, and State {}"
                              ".".format(start_time.name, trigger_time, state))
            tod_timers[start_time.name] = ScriptExecution.createTimer(
                                            trigger_time,
                                            lambda s=state: tod_transition(s,
                                                                           log))

        else:
            log.debug("{} is in the past but there is a more recent "
                      "state".format(state))

    # Command the time of day to the current state
    log.info("Ephem Time of day is now {}".format(most_recent_state))
    send_command_if_different(tod_item_ephem, most_recent_state)

@rule("Time of Day with Ephemeris",
      description=("Reusable Time of Day using Ephemeris to define different "
                   "times of day based on day type."),
      tags=["designpattern"])
@when("System started")
@when("Time cron 0 1 0 * * ? *")
@when("Descendent of {} changed".format(tod_group_ephem))
def ephem_tod(event):
    """
    Time of Day: A time based state machine which commands a state Item
    (tod_item_ephem in configuration.py) with the current time of day as a
    String.

    This Rule uses Ephemeris to determine the type of day (e.g. weekday) and
    selects the Group of start times for each time of day state based on the
    day type. See the Ephemeris documentation for details.

    To create the state machine, first create a Group and populate
    tod_group_ephem with that Group's name in configuration.py. Then create a
    Group for each type of day you have a different set of times of day. Add
    metadata to these Groups to identify what type of day those start times
    define.
    Supported types of days include:
        - default: this Group will be selected if no other type of day is
                   detected. Defined with the following metadata:
                       - Empty (i.e. no metadata)
                       - ToD="day"[type="default"]
        - weekday: this Group will be selected when Ephemeris.isWeekend()
                   returns False. Which days are defined as weekdays can be
                   configred in PaperUI. Defined with the following metadata:
                       - ToD="day"[type="weekday"]
        - weekend: this Group will be selected when Ephemeris.isWeekend()
                   returns True. Which days are defined as weekends can be
                   configured in PaperUI. Defined with the following metadata:
                       - ToD="day"[type="weekend"]
        - dayset: Ephemeris allows you to define custom daysets in
                  $OH_CONF/services/ephemeris.cfg. For example, if you have work
                  days that differ from your weekend/weekday schedule, you can
                  define a "workday" dayset. Defined with the following
                  metadata:
                      - ToD="day"[type="dayset", set="school"]
                  Use the name of the set as it is defined in ephemeris.cfg.
        - holiday: When Ephemeris is configured with country and region (and
                   city if used) in PaperUI, Ephemeris will automatically be
                   populated with the list of bank holidays (see the JollyDay
                   package on github). Defined with the following metadata:
                       - ToD="day"[type="holiday"]
        - custom: The Ephemeris capability allows one to define their own sets
                  of special days (e.g. birthdays and anniversaries) in a
                  specially formatted XML file. This allows you to define
                  additional bank holidays, an alternitive set of bank holidays
                  if the JollyDay list doesn't correspond with your actual
                  special days, or just define special days you want to treat
                  differently. Defined with the following metadata:
                      - ToD="day"[type="custom", file="/openhab/conf/services/custom1.xml"]

    For example, to define times of day that covers the weekend, weekdays, and
    a custom set of bank holidays you might have the following Groups.

        Group Ephem_TimeOfDay_StartTimes
        Group Ephem_Weekday (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="weekday"] }
        Group Ephem_Weekend (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="weekend"] }
        Group Ephem_Custom1 (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="custom", file="/openhab/conf/services/custom1.xml"] }

    You must be sure to define a Group for every type of day about. It can be
    handy to define the default Group to ensure that happens. The day types are
    checked in the order listed above. So, for example, if you have all of the
    above types defined, on a day that is a work day (custom dataset) that is a
    custom holiday, the custom holiday Group will be chosen.

    You can define multiple Groups for dayset and custom. If you define more
    than one Group for any of the rest, only the first one found will be used.

    Now that you have the days defined, you must define a set of DateTime Items
    which hold the start times for each time of day state used on that day and
    add those Item to the given day's Group. For example, the start times for
    the weekend would be added to the Ephem_Weekend Group above.

    The values stored in the Items can come from anywhere but the two most
    common will be from Astro or statically defined. For Astro, just link the
    Item to the appropriate Astro Channel. For statically defined the time is
    defined and the Item initialized through metadata.

    The expected metadata is : Tod="init"[start_time="HH:MM:SS:MS", tod_state="EXAMPLE"]
    where
        - HH is hours
        - MM is minutes
        - SS is seconds and optional
        - MS is milliseconds and optional
        - EXAMPLE is the State String

    All members of tod_group are required to have a tod_state metadata entry.
    Only static Items require the start_time metadata entry.

    This Rule triggeres at system started, one minute after midnight, and if any
    member of tod_group_ephem changes. When the Rule triggers, it creates timers
    to go off at the indicated times.

    For example, they full set of Groups and Items for a weekend, weekday and
    standard bank holiday system might be something like the following:

        Group Ephem_TimeOfDay_StartTimes
        Group Ephem_Weekday (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="weekday"] }
        Group Ephem_Weekend (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="weekend"] }
        Group Ephem_Holiday (Ephem_TimeOfDay_StartTimes) { ToD="day"[type="holiday] }

        DateTime vMorning_Time "Morning [%1$tH:%1$tM]"
            <sunrise> (Ephem_Weekday)
            { ToD="init"[start_time="06:00",tod_state="MORNING"] }

        DateTime vDay_Time "Day1 [%1$tH:%1$tM]"
            <sun> (Ephem_Weekend, Ephem_Holiday)
            { channel="astro:sun:local:rise#start",
              ToD="init"[tod_state="DAY"] }

        DateTime vAfternoon_Time "Afternoon [ %1$tH:%1$tM]"
            <sunset> (Ephem_Weekday, Ephem_Weekend, Ephem_Holiday)
            { channel="astro:sun:set120:set#start",
              ToD="init"[tod_state="AFTERNOON"] }

        DateTime vEvening_Time "Evening [%1$tH:%1$tM]"
            <sunset> (Ephem_Weekday, Ephem_Weekend, Ephem_Holiday)
            { channel="astro:sun:local:set#start",
              ToD="init"[tod_state="EVENING"] }

        DateTime vNight_Time "Night [%1$tH:%1$tM]"
            <moon> (Ephem_Weekday)
            { ToD="init"[start_time="23:00", tod_state="NIGHT"] }

        DateTime vBed_Time "Bed [%1$tH:%1$tM]"
            <bedroom_blue> (Ephem_Weekend, Ephem_Holiday)
            { ToD="init"[start_time="00:02",tod_state="BED"] }

        DateTime vBed_Time_Weekend "Holiday Bed [%1$tH:%1$tM]"
            <bedroom_blue> (Ephem_Weekend, Ephem_Holiday)

    Take notice that one DateTime Item can be a member of more than one Group.
    Also take notice that each day has a different set of times of day defined.
    Finally notice that some of the times are static and others driven by Astro.
"""

    # Initialize ToD Items from their metadata. First check to see if this Rule
    # triggered because the Items are being updated by trying to actuire the
    # updating semiphore. If we succeed in grabbing the semiphore, make sure to
    # release it before calling the function to initiate the update.
    if not updating.acquire(False):
        ephem_tod.log.info("Items are updating, ignoring rule trigger")
        return
    updating.release()
    tod_update_items(ephem_tod.log)

    # Determine the type of day and select the right Group of Items
    start_times = get_start_times(ephem_tod.log)
    if start_times is None:
        ephem_tod.log.error("No start times were found for today, please check "
                            "your Time of Day Items and Groups configurations.")
        return

    # Create the Timers
    create_tod_timers(ephem_tod.log, start_times)

def scriptUnloaded():
    """ Clears out all existing timers on script unload."""
    clear_timers()

Theory of operation:

Configuration

First you should configure Ephemeris. The easiest way is through PaperUI where you can set the days of the week that are weekend days and set your location. A default set of bank holidays will be used based on the country and region (and potentially city) you choose. Note, if there is no drop down for city that means there are no bank holidays specific to any city in your region. Do not enter your city manually.

Create a Group and set the name of that Group to tod_group_ephem in configuration.py.
Create a String Item and set the name of that Item to tod_item_ephem in configuration.py.

For each day type that you have a unique set of times of day, create a Group with the proper metadata (see the docstrings) to identify that Group with that type of day. Create DateTime Items containing the start times for each time of day period and add them to that Group. Add the Group to the parent tod_group_ephem. As mentioned in the docstring, there should only be one Group for default, weekday, weekend, or holiday. The dayset and custom types can have more than one Group (e.g. one custom with a file listing bank holidays not included by default and another one with a file listing school half days).

Here is my simple but currently working config:

String vEphem_TimeOfDay "Current Ephem Time of Day [MAP(weather.map):%s]"
	<tod>

Group Ephem_TimeOfDay_StartTimes

Group Ephem_Default (Ephem_TimeOfDay_StartTimes)
    { ToD="day"[type="default"] }

Group Ephem_Weekend (Ephem_TimeOfDay_StartTimes)
    { ToD="day"[type="weekend"] }

Group Ephem_Holiday (Ephem_TimeOfDay_StartTimes)
    { ToD="day"[type="holiday"] }

String vTimeOfDay "Current Time of Day [MAP(weather.map):%s]"
	<tod>

Group TimeOfDay_StartTimes

DateTime vMorning_Time "Morning [%1$tH:%1$tM]"
    <sunrise> (TimeOfDay_StartTimes, Ephem_Default)
    { ToD="init"[start_time="06:00",tod_state="MORNING"] }

DateTime vDay_Time "Day1 [%1$tH:%1$tM]"
    <sun> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday)
    { channel="astro:sun:local:rise#start",
      ToD="init"[tod_state="DAY"] }

DateTime vAfternoon_Time "Afternoon [ %1$tH:%1$tM]"
    <sunset> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday)
    { channel="astro:sun:set120:set#start",
      ToD="init"[tod_state="AFTERNOON"] }

DateTime vEvening_Time "Evening [%1$tH:%1$tM]"
    <sunset> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday)
    { channel="astro:sun:local:set#start",
      ToD="init"[tod_state="EVENING"] }

DateTime vNight_Time "Night [%1$tH:%1$tM]"
    <moon> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday)
    { ToD="init"[start_time="23:00", tod_state="NIGHT"] }

DateTime vBed_Time "Bed [%1$tH:%1$tM]"
    <bedroom_blue> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday)
    { ToD="init"[start_time="00:02",tod_state="BED"] }

NOTE: I still have the Time of Day Rule in the DP running in parallel.

It might be helpful for you to visualize your times of day in a table:

ToD State Time default weekend holiday
MORNING 06:00 X
DAY Astro Sunrise X X X
AFTERNOON Astro Sunset - 120 mins X X X
EVENING Astro Sunset X X X
NIGHT 23:00 X X X
BED 00:02 X X X

As you can see, it’s a pretty simple config. I just skip the MORNING state on weekends and holidays. I fully expect most users to have a far more complicated table than this.

How it Works

The Rule triggers when any of the DateTime Items are changed, at System started, and a little after midnight (to give Astro a chance to calculate the times for the new day).

First is will loop through all of the Items and updates them with the metadata for those with a start_time defined in the ToD metadata (see the docstrings for details). A lock is used to prevent the Rule from running while the Items are being initialized/updated.

Next the type of day it is today is determined and the Group that has the time of day start times for that type is calculated and returned. Finally, Timers are created for each of the DateTime Items in the Group that haven’t already passed to update the tod_item_ephem with the state name (defined in ToD metadata), causing the tod_item_ephem to be updated with the time of day state at the proper time.

Please let me know if you have any questions, comments, or problems. I will be submitting this as a PR probably later this week.

5 Likes

Oh yessir I will be testing this when I have time!

I’ve been through weekends and holidays and it seems pretty stable. I’ve submitted it as a PR.

3 Likes

I’m trying to use this DP but i have got following errors after setting items/groups, adding and saving: (copy/paste from this post)

automation/jsr223/python/community/time-of-day/tod_ephem.py

and adding following to configuration.py:

tod_group_ephem = "TimeOfDay_StartTimes"
tod_item_ephem = "vTimeOfDay"

I included complete logs, errors are at the end - something related to logging and after failed to execute rule…

2019-11-12 21:13:48.709 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/community/time-of-day/tod_ephem.py'

==> /var/log/openhab2/openhab.log <==

2019-11-12 21:13:51.234 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[System started], target_type=System, trigger_target=started, trigger_type=None, old_state=None, new_state=None

2019-11-12 21:13:51.301 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[Time cron 0 1 0 * * ? *], target_type=Time, trigger_target=cron, trigger_type=0 1 0 * * ? *, old_state=None, new_state=None

2019-11-12 21:13:51.403 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[Descendent of TimeOfDay_StartTimes changed], target_type=Descendent of, trigger_target=TimeOfDay_StartTimes, trigger_type=changed, old_state=None, new_state=None

2019-11-12 21:13:51.471 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vDay_Time-changed_a6f2ae70055611eaaff9b827eb65d8fa]

2019-11-12 21:13:51.504 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vAfternoon_Time-changed_a6f7b780055611ea9e43b827eb65d8fa]

2019-11-12 21:13:51.528 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created cron_trigger: [Time_cron_0_1_0_a6ddc6de055611eabd3cb827eb65d8fa]

2019-11-12 21:13:51.550 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created system_trigger: [System_started_a6d3dbcf055611ea92a8b827eb65d8fa]

2019-11-12 21:13:51.566 [DEBUG] [jsr223.jython.core.rules            ] - Added rule [Time of Day with Ephemeris]

==> /var/log/openhab2/events.log <==

2019-11-12 21:13:51.585 [thome.event.RuleAddedEvent] - Rule '93bdd84f-af94-4b9d-95a2-38cd82d20e0a' has been added.

2019-11-12 21:13:51.592 [.event.RuleStatusInfoEvent] - 93bdd84f-af94-4b9d-95a2-38cd82d20e0a updated: UNINITIALIZED

2019-11-12 21:13:51.598 [.event.RuleStatusInfoEvent] - 93bdd84f-af94-4b9d-95a2-38cd82d20e0a updated: INITIALIZING

2019-11-12 21:13:51.731 [.event.RuleStatusInfoEvent] - 93bdd84f-af94-4b9d-95a2-38cd82d20e0a updated: IDLE

2019-11-12 21:13:52.801 [.event.RuleStatusInfoEvent] - 93bdd84f-af94-4b9d-95a2-38cd82d20e0a updated: RUNNING

==> /var/log/openhab2/openhab.log <==

2019-11-12 21:13:52.807 [INFO ] [23.jython.Time of Day with Ephemeris] - Updating ToD Items from metadata

2019-11-12 21:13:52.824 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('start_time',)]

2019-11-12 21:13:52.850 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]

2019-11-12 21:13:52.887 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('start_time',)]

2019-11-12 21:13:52.901 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]

2019-11-12 21:13:52.916 [INFO ] [23.jython.Time of Day with Ephemeris] - Done updating ToD Items

2019-11-12 21:13:52.937 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for default:

2019-11-12 21:13:52.955 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:52.969 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]

2019-11-12 21:13:52.987 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.002 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]

2019-11-12 21:13:53.015 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.033 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]

2019-11-12 21:13:53.046 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.066 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]

2019-11-12 21:13:53.080 [WARN ] [23.jython.Time of Day with Ephemeris] - There is more than one default Group! Only using the first one vDay_Time

2019-11-12 21:13:53.094 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for weekday:

2019-11-12 21:13:53.106 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.118 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]

2019-11-12 21:13:53.132 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.144 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]

2019-11-12 21:13:53.157 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for dayset:

2019-11-12 21:13:53.168 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.178 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]

2019-11-12 21:13:53.190 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('type',)]

2019-11-12 21:13:53.199 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]

2019-11-12 21:13:53.527 [ERROR] [23.jython.Time of Day with Ephemeris] - Traceback (most recent call last):

  File "/etc/openhab2/automation/lib/python/core/log.py", line 52, in wrapper

    return fn(*args, **kwargs)

  File "<script>", line 401, in ephem_tod

  File "<script>", line 159, in get_start_times

IllegalStateException: java.lang.IllegalStateException: Cannot instantiate configuration from URL 'null'.

2019-11-12 21:13:53.545 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule '93bdd84f-af94-4b9d-95a2-38cd82d20e0a': Fail to execute action: 1

==> /var/log/openhab2/events.log <==

2019-11-12 21:13:53.554 [.event.RuleStatusInfoEvent] - 93bdd84f-af94-4b9d-95a2-38cd82d20e0a updated: IDLE

Any clue what could be wrong from my side?

Edit: maybe just need wait for midnight, going sleep now and check it before work tomorrow.

Show me your Item definitions. I may need to do some more pre-checking of the configs to generate better error messages.

Also, can you switch to the version in the PR? There are a few very minor changes between the two but it will ensure I fully understand what is going on.

Did you configure Ephemeris with your location so that your local bank holidays are available? That might be a cause, assuming line 159 is the start_times = get_group("holiday" line.

No, it should work the first time.

items definition:

Group Ephem_TimeOfDay_StartTimes

DateTime vMorning_Time "Morning [%1$tH:%1$tM]" <sunrise> (TimeOfDay_StartTimes) 
    { ToD="init"[start_time="06:00",tod_state="MORNING"] }

DateTime vDay_Time "Day [%1$tH:%1$tM]" <sun> (TimeOfDay_StartTimes)
    { channel="astro:sun:local:rise#start", ToD="init"[tod_state="DAY"] }

DateTime vAfternoon_Time "Afternoon [ %1$tH:%1$tM]" <sunset> (TimeOfDay_StartTimes)
    { channel="astro:sun:4ee9ff3e:set#start", ToD="init"[tod_state="AFTERNOON"] }

DateTime vEvening_Time "Evening [%1$tH:%1$tM]" <sunset> (TimeOfDay_StartTimes)
    { channel="astro:sun:local:set#start", ToD="init"[tod_state="EVENING"] }
    
DateTime vNight_Time "Night [%1$tH:%1$tM]" <moon> (TimeOfDay_StartTimes)
    { ToD="init"[start_time="23:00", tod_state="NIGHT"] } 

DateTime vBed_Time "Bed [%1$tH:%1$tM]" <bedroom_blue> (TimeOfDay_StartTimes)
    { ToD="init"[start_time="00:02",tod_state="BED"] }

Ephemeris config in paperrUI:

I’ll look how to get PR version tomorrow, but I believe that probably Thailand holidays are not well defined (as they are not fixed)

Did you enter the city or was it in a drop down list?

Looking at Jollyday (the source for the bank holidays) I notice there is not one for Holidays_th.xml. The fact that the holidays are not fixed may not be a deal killer. Many many holidays in the Jewish, Muslim, and Christian tradition are implemented and Jollyday handles those. There is even a file for Hindu holidays though it’s pretty empty. I suspect no one has provided the Thai holidays to Jollyday.

This is something I need to consider how to handle if this is indeed the source of the problem, or there might need to be a PR to the Ephemeris code to not generate an exception.

But, first I see there is a problem with your Items and Groups. You need a structure like the following:

            Ephem_TimeOfDay_StartTimes                  (Group)
            /            |           \
  Ephem_Weekend    Ephem_Weekday  Ephem_Holiday         (Groups)
        |                |              |
vWeekend_Morning   vWeekday_Morning  vHoliday_Morning   (DateTime Items)
        |                |             |
     and so on

You need a separate Group for each day type. This Group is a member of Ephem_TimeOfDay_StartTimes. And it is these Groups that have as members the DateTime Items. You are missing that second row of Groups.

If you don’t want to have a different set of times of day for different day types, just create a Group along the lines of:

Ephem_Defaults { ToD="day"[type="default"] }

and add all your DateTime Items to that. This was supposed to be explained in the post above, with an example. It’s also explained in the docstrings. Was there something unclear in the text in the OP? I need to correct it if so.