[Scripted Automation] Multi-day Time of Day Library

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.

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

There is no drop down, city appears “automagically” probably from region location settings. If I do it manually it will be Bang Lamung.

Most of issue was my big failure to read the docs properly.
I set/updated all groups and items same as you have it. And it seems to update correct date-time for each ToD item but at the end it finish with same error:

2019-11-13 12:17:42.526 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/community/time-of-day/tod_ephem.py'                            
2019-11-13 12:17:44.028 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[System started], target_type=System, trigger_target=started, trigger_typ
e=None, old_state=None, new_state=None                                                                                                                         
2019-11-13 12:17:44.089 [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-13 12:17:44.185 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[Descendent of Ephem_TimeOfDay_StartTimes changed], target_type=Descenden
t of, trigger_target=Ephem_TimeOfDay_StartTimes, trigger_type=changed, old_state=None, new_state=None                                                          
2019-11-13 12:17:44.259 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vDay_Time-changed_ec35409e05d411ea8a89b827eb65d8fa] 
2019-11-13 12:17:44.293 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vBed_Time-changed_ec3a70c005d411eaaea9b827eb65d8fa] 
2019-11-13 12:17:44.328 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vEvening_Time-changed_ec3fa0e105d411ea9627b827eb65d8
fa]                                                                                                                                                            
2019-11-13 12:17:44.364 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vMorning_Time-changed_ec451f2105d411ea9b39b827eb65d8
fa]                                                                                                                                                            
2019-11-13 12:17:44.407 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vNight_Time-changed_ec4a9d6105d411eab33fb827eb65d8fa
]                                                                                                                                                              
2019-11-13 12:17:44.451 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vAfternoon_Time-changed_ec512d0f05d411eaa798b827eb65
d8fa]                                                                                                                                                          
2019-11-13 12:17:44.474 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created cron_trigger: [Time_cron_0_1_0_ec20320005d411ea8d95b827eb65d8fa]        
2019-11-13 12:17:44.516 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created system_trigger: [System_started_ec170a4005d411eaa5bab827eb65d8fa]       
2019-11-13 12:17:44.538 [DEBUG] [jsr223.jython.core.rules            ] - Added rule [Time of Day with Ephemeris]                                               
2019-11-13 12:17:45.986 [INFO ] [me.core.service.AbstractWatchService] - Loading script 'python/community/time-of-day/tod_ephem.py'                            
2019-11-13 12:17:46.624 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[System started], target_type=System, trigger_target=started, trigger_typ
e=None, old_state=None, new_state=None                                                                                                                         
2019-11-13 12:17:46.658 [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-13 12:17:46.713 [DEBUG] [jsr223.jython.core.triggers         ] - when: target=[Descendent of Ephem_TimeOfDay_StartTimes changed], target_type=Descenden
t of, trigger_target=Ephem_TimeOfDay_StartTimes, trigger_type=changed, old_state=None, new_state=None                                                          
2019-11-13 12:17:46.869 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vDay_Time-changed_edc2be7005d411ea8245b827eb65d8fa] 
2019-11-13 12:17:46.902 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vBed_Time-changed_edc88ad105d411eaa497b827eb65d8fa] 
2019-11-13 12:17:46.933 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vEvening_Time-changed_edcdbaf005d411eabfb8b827eb65d8
fa]                                                                                                                                                            
2019-11-13 12:17:46.962 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vMorning_Time-changed_edd227c005d411ea88b1b827eb65d8
fa]                                                                                                                                                            
2019-11-13 12:17:46.989 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vNight_Time-changed_edd6bba105d411ea814eb827eb65d8fa
]                                                                                                                                                              
2019-11-13 12:17:47.014 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created item_trigger: [Item-vAfternoon_Time-changed_eddab34005d411ea90abb827eb65
d8fa]                                                                                                                                                          
2019-11-13 12:17:47.030 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created cron_trigger: [Time_cron_0_1_0_eda8318f05d411eab7f1b827eb65d8fa]        
2019-11-13 12:17:47.047 [DEBUG] [jsr223.jython.core.triggers         ] - when: Created system_trigger: [System_started_eda3017005d411eaa2f8b827eb65d8fa]       
2019-11-13 12:17:47.077 [DEBUG] [jsr223.jython.core.rules            ] - Added rule [Time of Day with Ephemeris]                                               
2019-11-13 12:17:48.200 [INFO ] [23.jython.Time of Day with Ephemeris] - Updating ToD Items from metadata                                                      
2019-11-13 12:17:48.222 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vDay_Time], namespace [ToD], args [('start_time',)]              
2019-11-13 12:17:48.233 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vDay_Time], namespace [ToD]                                       
2019-11-13 12:17:48.249 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vBed_Time], namespace [ToD], args [('start_time',)]              
2019-11-13 12:17:48.259 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vBed_Time], namespace [ToD]                                       
2019-11-13 12:17:48.272 [DEBUG] [23.jython.Time of Day with Ephemeris] - Handling vBed_Time (Type=DateTimeItem, State=2019-11-13T00:00:00.000+0700, Label=Bed, 
Category=bedroom_blue, Groups=[TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday])                                                             
2019-11-13 12:17:48.317 [DEBUG] [jsr223.jython.core.utils            ] - New postUpdate value for [vBed_Time] is [2019-11-13T00:02:00.000+07:00]               
2019-11-13 12:17:48.345 [INFO ] [23.jython.Time of Day with Ephemeris] - Updated vBed_Time to 2019-11-13T00:02:00.000+07:00                                    
2019-11-13 12:17:48.358 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vEvening_Time], namespace [ToD], args [('start_time',)]          
2019-11-13 12:17:48.369 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vEvening_Time], namespace [ToD]                                   
2019-11-13 12:17:48.381 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vMorning_Time], namespace [ToD], args [('start_time',)]          
2019-11-13 12:17:48.393 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vMorning_Time], namespace [ToD]                                   
2019-11-13 12:17:48.405 [DEBUG] [23.jython.Time of Day with Ephemeris] - Handling vMorning_Time (Type=DateTimeItem, State=2019-11-13T06:00:00.000+0700, Label=M
orning, Category=sunrise, Groups=[TimeOfDay_StartTimes, Ephem_Default])                                                                                        
2019-11-13 12:17:48.424 [DEBUG] [jsr223.jython.core.utils            ] - Not posting update 2019-11-13T06:00:00.000+07:00 to vMorning_Time since it is the same
 as the current state                                                                                                                                          
2019-11-13 12:17:48.435 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vNight_Time], namespace [ToD], args [('start_time',)]            
2019-11-13 12:17:48.447 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vNight_Time], namespace [ToD]                                     
2019-11-13 12:17:48.460 [DEBUG] [23.jython.Time of Day with Ephemeris] - Handling vNight_Time (Type=DateTimeItem, State=2019-11-13T23:00:00.000+0700, Label=Nig
ht, Category=moon, Groups=[TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday])                                                                 
2019-11-13 12:17:48.477 [DEBUG] [jsr223.jython.core.utils            ] - Not posting update 2019-11-13T23:00:00.000+07:00 to vNight_Time since it is the same a
s the current state                                                                                                                                            
2019-11-13 12:17:48.489 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [vAfternoon_Time], namespace [ToD], args [('start_time',)]        
2019-11-13 12:17:48.499 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [vAfternoon_Time], namespace [ToD]                                 
2019-11-13 12:17:48.811 [INFO ] [23.jython.Time of Day with Ephemeris] - Done updating ToD Items                                                               
2019-11-13 12:17:48.824 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for default:                                                                 
2019-11-13 12:17:48.836 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Weekend], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.848 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Weekend], namespace [ToD]                                   
2019-11-13 12:17:48.862 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Weekend], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.874 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Weekend], namespace [ToD]                                   
2019-11-13 12:17:48.891 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Holiday], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.903 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Holiday], namespace [ToD]                                   
2019-11-13 12:17:48.919 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Holiday], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.931 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Holiday], namespace [ToD]                                   
2019-11-13 12:17:48.946 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Default], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.958 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Default], namespace [ToD]                                   
2019-11-13 12:17:48.974 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for weekday:                                                                 
2019-11-13 12:17:48.984 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Weekend], namespace [ToD], args [('type',)]                
2019-11-13 12:17:48.995 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Weekend], namespace [ToD]                                   
2019-11-13 12:17:49.007 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Holiday], namespace [ToD], args [('type',)]                
2019-11-13 12:17:49.018 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Holiday], namespace [ToD]                                   
2019-11-13 12:17:49.030 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Default], namespace [ToD], args [('type',)]                
2019-11-13 12:17:49.040 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Default], namespace [ToD]                                   
2019-11-13 12:17:49.052 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for dayset:                                                                  
2019-11-13 12:17:49.063 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Weekend], namespace [ToD], args [('type',)]                
2019-11-13 12:17:49.073 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Weekend], namespace [ToD]                                   
2019-11-13 12:17:49.086 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Holiday], namespace [ToD], args [('type',)]                
2019-11-13 12:17:49.096 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Holiday], namespace [ToD]                                   
2019-11-13 12:17:49.111 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Default], namespace [ToD], args [('type',)]                
2019-11-13 12:17:49.121 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Default], namespace [ToD]                                   
2019-11-13 12:17:49.323 [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-13 12:17:49.342 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule '043c5aa9-2974-41c7-b986-dae11587df3e': Fail to execute action:
 1                                                                                                                                                             

Not really, when I read it again today it was clear (I should not do such things late night)
But your graphical representation in last post is much straightforward to understand.

I deleted City in System/Ephemeris settings, save and reload tod_ephem.py
Error is still there:

2019-11-13 13:15:54.592 [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-13 13:15:54.598 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule '516e756d-fdcf-4393-b945-17d6ab9f8d6d': Fail to execute action:
 1                                                                                                                                                             

Finally changed System/Ephemeris country from Thailand to Czech Republic - and all works :slight_smile:

One more test - I removed country (select blank field from list) and saved - result is same error like in case of Thailand

I have not reviewed the PR yet, and I’m not sure if this will help, but I spotted an issue based on the error message you are getting. Change line 156 to…

if value == {}:

thank you for a help Scott, unfortunately, error is still the same.

but i try to fix it as following add try/except to code throwing error on line 181- seems to work, not sure if it is proper way to do it:

    try:                                                                                                                                                       
        holiday = Ephemeris.getBankHolidayName()                                                                                                               
    except:
        ephem_tod.log.debug("Error getting holiday ephemeris, skipping them!")
        holiday = None                                                                                                                                         
1 Like

Probably won’t help either, but there is another one at line 234

I change it also, with same result.

I’m pretty sure the error is that there is no holiday file for Thailand, though it has to be getting the city from somewhere.

It seems to be failing on the call to Ephemeris.getBankHolidayName(). This makes me think that the root of the problem is actually in Ephemeris. It should just return None if there is no country/region/or city file to read.

This would be exactly how I would handle it in the Python code, but I think the proper fix will be in Ephemeris itself.

get_key_value only returns one result when it exists. Why return an empty dict instead of None when the key value doesn’t exist? I just assumed it would be None since I don’t a dict when it’s there.

I’ve posted fixes to the PR for this.

The short answer is because…

None.get("Another_Subkey", {}).get("Yet_Another_Subkey", {})

… returns…

AttributeError. 'NoneType' object has no attribute 'get'

But…

{}.get("Another_Subkey", {}).get("Yet_Another_Subkey", {})

… returns {}.

Originally, get_key_value did return None, but I found it was easier when it did not return None, so that the dictionary get method could be used. This came about when I put in the handling of nested hierarchies in metadata. For example…

my_variable = get_key_value("Item_Name", "Namespace_Name", "Key", "Subkey", "Subsubkey")

… can be used like…

another_variable = my_variable.get("Another_Subkey", {}).get("Yet_Another_Subkey", {})

If get_key_value returned None, you couldn’t do this until you had first checked the result for None…

another_variable = None if my_variable is None else my_variable.get("Another_Subkey", {}).get("Yet_Another_Subkey")

And this repeats when using another_variable. Clear as mud?

Going through Ephemeris documentation - last section describe custom bank holidays
I think it would be helpful to have this possibility also in ToD library.
Something like to add full path to custom xml to variable in configuration.py and if this variable exists use it to pull data
but it is far above my knowledge how to do this.

Side note why I need custom holidays (and it is probably reason why there is not Thailand file in Jollyday):
There is list of holidays (around 30) which are either religious, Royal family events, government decided and few other types. Some of them are fixed (or moving to Monday after in case they occured during weekend). Unfortunately there is rule on top that companies have to give 13 days off during year - where royal and main religious events can not be skipped or moved but the others can be moved anywhere in year - and are marked as substitution for each chosen event.

This leads to situation that each company has different bank holiday rules and I’ll need to update xml every year with correct dates.

Kind of. It leads to non-intuitive behavior. At a minimum it isn’t clear what it will return because it doesn’t always return a dict, unless there is some magic I don’t know going on (i.e. a dict with one value can be used the same as that value by itself).

IIUC:

  • you get a String if there is just the one key/value
  • you get a dict if there are nested key/values
  • you get an empty dict if there is no key/value

Assuming I’m correct, then shouldn’t one get a dict for the first one too? Or is this already happening and Python magic is hiding that fact?

Depending on when this change was made, I may have started using this method back when it used to return None.

It’s there. That’s what the “custom” type is for. You need to supply a “file” value to the metadata on the Group with the full path to your custom holiday.xml file.

So first create your xml file like the Ephemeris docs say. Then create a Group:

Group Ephem_Custom (Ephem_TimeOfDay_StartTimes)
    { ToD="day"[type="custom", file="/etc/openhab2/services/holiday.xml"] }

NOTE: I wrote the Ephemeris docs based on the work I did on the this code.

Thanks for direction, going to test it now.

update - I created custom file :

/etc/openhab2/services/Holidays-th.xml

with following rights:

-rw-rw-r--   1 openhab openhab  804 Nov 14 05:34 Holidays-th.xml

Created group

Group Ephem_Custom1 (Ephem_TimeOfDay_StartTimes)
    { ToD="day"[type="custom", file="/etc/openhab2/services/Holidays-th.xml"] }

and added to items

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 "Day [%1$tH:%1$tM]" <sun> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday, Ephem_Custom1)
    { 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, Ephem_Custom1)
    { channel="astro:sun:4ee9ff3e:set#start", ToD="init"[tod_state="AFTERNOON"] }

DateTime vEvening_Time "Evening [%1$tH:%1$tM]" <sunset> (TimeOfDay_StartTimes, Ephem_Default, Ephem_Weekend, Ephem_Holiday, Ephem_Custom1)
    { 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, Ephem_Custom1)
    { 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, Ephem_Custom1)
    { ToD="init"[start_time="00:02",tod_state="BED"] }

and getting following error

2019-11-14 06:00:03.209 [DEBUG] [23.jython.Time of Day with Ephemeris] - Checking for custom:

2019-11-14 06:00:03.222 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Weekend], namespace [ToD], args [('type',)]

2019-11-14 06:00:03.233 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Weekend], namespace [ToD]

2019-11-14 06:00:03.248 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Holiday], namespace [ToD], args [('type',)]

2019-11-14 06:00:03.260 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Holiday], namespace [ToD]

2019-11-14 06:00:03.274 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Default], namespace [ToD], args [('type',)]

2019-11-14 06:00:03.288 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Default], namespace [ToD]

2019-11-14 06:00:03.305 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Custom1], namespace [ToD], args [('type',)]

2019-11-14 06:00:03.317 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Custom1], namespace [ToD]

2019-11-14 06:00:03.332 [DEBUG] [jsr223.jython.core.metadata         ] - get_key_value: Item [Ephem_Custom1], namespace [ToD], args [('file',)]

2019-11-14 06:00:03.344 [DEBUG] [jsr223.jython.core.metadata         ] - get_metadata: Item [Ephem_Custom1], namespace [ToD]

2019-11-14 06:00:03.364 [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 429, in ephem_tod

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

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

  File "<script>", line 191, in <lambda>

NullPointerException: java.lang.NullPointerException

2019-11-14 06:00:03.372 [INFO ] [home.model.script.CabinetLight_OnOff] - CabinetLight switched OFF due to start of MORNING

2019-11-14 06:00:03.380 [ERROR] [e.automation.internal.RuleEngineImpl] - Failed to execute rule 'b9534d15-18cc-43f7-a4dd-35f9a40df190': Fail to execute action: 1

edit: content of Holidays-th.xml

<?xml version="1.0" encoding="UTF-8"?>
<tns:Configuration hierarchy="th" description="Thailand"
    xmlns:tns="http://www.example.org/Holiday" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.example.org/Holiday /Holiday.xsd">
    <tns:Holidays>
        <tns:Fixed month="November" day="14" descriptionPropertiesKey="ToD test customHolidays" />
    </tns:Holidays>
</tns:Configuration>

Whoa… hold up there a sec! I guess there may have been a shorter answer in my previous post! Metadata values, from a Python perspective, can be str, decimal, boolean, dict or None. In order to differentiate between an actual value of None and there not being a matching key/value pair, I return an empty dict. This may not be intuitive (it is to me!), but it’s functional :slightly_smiling_face:!

i see the docstring for set_key_value shows the correct arguments, but get_key_value only shows string for the return format (fixed).

Crystal?

That’s probably the source of my confusion. I think, when I last checked the docs for this method it returned None and the docstring still says return type is String (I assume that’s what you fixed and it hasn’t rolled out yet?). That and, for most practical purposes, there isn’t a whole lot of difference between having a key whose value is None and a key that isn’t present. But maybe there is a use case I’m not thinking about.

I’m just used to “not found” being returned as null/None in most of the libraries I’ve used I guess.