Skip to content

Commit

Permalink
Add support for TimeSeries (#236)
Browse files Browse the repository at this point in the history
- [x] add `TimeSeries` class and make it array-like
- [x] add `java.time.Instant` to the Object scope
- [x] document: `GenericItem#time_series=` 
- [x] rule trigger: `time_series_updated`
- [x] add: `ItemTimeSeriesUpdatedEvent` to document `event.time_series`
- [x] document profile callback method `send_time_series`
- [x] add `:time_series_from_handler` profile event
- [x] TimeSeries.new defaults to `:replace`

Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng authored Jan 30, 2024
1 parent dc8d665 commit e1124e2
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 10 deletions.
23 changes: 23 additions & 0 deletions lib/openhab/core/events/item_time_series_updated_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

# @deprecated OH4.0 guard not needed in OH 4.1
return unless OpenHAB::Core.version >= OpenHAB::Core::V4_1

module OpenHAB
module Core
module Events
java_import org.openhab.core.items.events.ItemTimeSeriesUpdatedEvent

#
# {AbstractEvent} sent when an item received a time series update.
#
# @!attribute [r] time_series
# @return [TimeSeries] The updated time series.
#
# @since openHAB 4.1
# @see DSL::Rules::BuilderDSL#time_series_updated #time_series_updated rule trigger
#
class ItemTimeSeriesUpdatedEvent < ItemEvent; end
end
end
end
12 changes: 12 additions & 0 deletions lib/openhab/core/items/generic_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ def format_type(type)
type.to_s
end

#
# @method time_series=(time_series)
# Set a new time series.
#
# This will trigger a {DSL::Rules::BuilderDSL#time_series_updated time_series_updated} event.
#
# @param [Core::Types::TimeSeries] time_series New time series to set.
# @return [void]
#
# @since openHAB 4.1
#

#
# Defers notifying openHAB of modifications to multiple attributes until the block is complete.
#
Expand Down
17 changes: 16 additions & 1 deletion lib/openhab/core/profile_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ class ProfileFactory
include Singleton

class Profile
include org.openhab.core.thing.profiles.StateProfile
# @deprecated OH 4.0 only include TimeSeriesProfile in OH 4.1, because it extends StateProfile
if OpenHAB::Core.version >= OpenHAB::Core::V4_1
include org.openhab.core.thing.profiles.TimeSeriesProfile
else
include org.openhab.core.thing.profiles.StateProfile
end

def initialize(callback, context, uid, thread_locals, block)
unless callback.class.ancestors.include?(Things::ProfileCallback)
Expand Down Expand Up @@ -64,6 +69,14 @@ def onStateUpdateFromItem(state)
process_event(:state_from_item, state: state)
end

# @deprecated OH 4.0 guard is only needed for < OH 4.1
if OpenHAB::Core.version >= OpenHAB::Core::V4_1
# @!visibility private
def onTimeSeriesFromHandler(time_series)
process_event(:time_series_from_handler, time_series: time_series)
end
end

private

def process_event(event, **params)
Expand All @@ -77,6 +90,8 @@ def process_event(event, **params)
params[:channel_uid] = @callback.link.linked_uid
params[:state] ||= nil
params[:command] ||= nil
# @deprecated OH 4.0 guard is only needed for < OH 4.1
params[:time_series] ||= nil if OpenHAB::Core.version >= OpenHAB::Core::V4_1

kwargs = {}
@block.parameters.each do |(param_type, name)|
Expand Down
5 changes: 5 additions & 0 deletions lib/openhab/core/things/profile_callback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def send_update(state)
state = link.item.format_update(state)
super(state)
end

# @!method send_time_series(time_series)
# Send a time series to the framework.
# @param [TimeSeries] time_series
# @since openHAB 4.1
end
end
end
Expand Down
121 changes: 121 additions & 0 deletions lib/openhab/core/types/time_series.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

# @deprecated OH4.0 this guard is not needed on OH4.1
return unless OpenHAB::Core.version >= OpenHAB::Core::V4_1

require "forwardable"

module OpenHAB
module Core
module Types
TimeSeries = org.openhab.core.types.TimeSeries

#
# {TimeSeries} is used to transport a set of states together with their timestamp.
#
# The states are sorted chronologically. The entries can be accessed like an array.
#
# @since openHAB 4.1
#
# @example
# time_series = TimeSeries.new # defaults to :add policy
# .add(Time.at(2), DecimalType.new(2))
# .add(Time.at(1), DecimalType.new(1))
# .add(Time.at(3), DecimalType.new(3))
# logger.info "first entry: #{time_series.first.state}" # => 1
# logger.info "last entry: #{time_series.last.state}" # => 3
# logger.info "second entry: #{time_series[1].state}" # => 2
# logger.info "sum: #{time_series.sum(&:state)}" # => 6
#
# @see DSL::Rules::BuilderDSL#time_series_updated #time_series_updated rule trigger
#
class TimeSeries
extend Forwardable

# @!attribute [r] policy
# Returns the persistence policy of this series.
# @see org.openhab.core.types.TimeSeries#getPolicy()
# @return [org.openhab.core.types.TimeSeries.Policy]

# @!attribute [r] begin
# Returns the timestamp of the first element in this series.
# @return [Instant]

# @!attribute [r] end
# Returns the timestamp of the last element in this series.
# @return [Instant]

# @!attribute [r] size
# Returns the number of elements in this series.
# @return [Integer]

#
# Create a new instance of TimeSeries
#
# @param [:add, :replace, org.openhab.core.types.TimeSeries.Policy] policy
# The persistence policy of this series.
#
def initialize(policy = :replace)
policy = Policy.value_of(policy.to_s.upcase) if policy.is_a?(Symbol)
super
end

# Returns true if the series' policy is `ADD`.
# @return [true,false]
def add?
policy == Policy::ADD
end

# Returns true if the series' policy is `REPLACE`.
# @return [true,false]
def replace?
policy == Policy::REPLACE
end

# @!visibility private
def inspect
"#<OpenHAB::Core::Types::TimeSeries " \
"policy=#{policy} " \
"begin=#{self.begin} " \
"end=#{self.end} " \
"size=#{size}>"
end

#
# Returns the content of this series.
# @return [Array<org.openhab.core.types.TimeSeries.Entry>]
#
def states
get_states.to_array.to_a.freeze
end

# rename raw methods so we can overwrite them
# @!visibility private
alias_method :add_instant, :add

#
# Adds a new element to this series.
#
# Elements can be added in an arbitrary order and are sorted chronologically.
#
# @note This method returns self so it can be chained, unlike the Java version.
#
# @param [Instant, #to_zoned_date_time, #to_instant] instant An instant for the given state.
# @param [State] state The State at the given timestamp.
# @return [self]
#
def add(instant, state)
instant = instant.to_zoned_date_time if instant.respond_to?(:to_zoned_date_time)
instant = instant.to_instant if instant.respond_to?(:to_instant)
add_instant(instant, state)
self
end

# any method that exists on Array gets forwarded to states
delegate (Array.instance_methods - instance_methods) => :states
end
end
end
end

TimeSeries = OpenHAB::Core::Types::TimeSeries unless Object.const_defined?(:TimeSeries)
14 changes: 14 additions & 0 deletions lib/openhab/core_ext/java/instant.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

module OpenHAB
module CoreExt
module Java
java_import java.time.Instant

# Extensions to {java.time.Instant}
class Instant < java.lang.Object; end
end
end
end

Instant = OpenHAB::CoreExt::Java::Instant unless Object.const_defined?(:Instant)
8 changes: 6 additions & 2 deletions lib/openhab/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,17 @@ def script(name = nil, description: nil, id: nil, tag: nil, tags: nil, **kwargs,
# @param [String, nil] label The label for the profile. When nil, the profile will not be visible in the UI.
# @param [org.openhab.core.config.core.ConfigDescription, nil] config_description
# The configuration description for the profile so that it can be configured in the UI.
# @yield [event, command: nil, state: nil, callback:, link:, item:, channel_uid:, configuration:, context:]
# @yield [event, command: nil, state: nil, time_series: nil, callback:, link:, item:, channel_uid:, configuration:, context:]
# All keyword params are optional. Any that aren't defined won't be passed.
# @yieldparam [:command_from_item, :state_from_item, :command_from_handler, :state_from_handler] event
# @yieldparam [:command_from_item, :state_from_item, :command_from_handler, :state_from_handler, :time_series_from_handler] event
# The event that needs to be processed.
# @yieldparam [Command, nil] command
# The command being sent for `:command_from_item` and `:command_from_handler` events.
# @yieldparam [State, nil] state
# The state being sent for `:state_from_item` and `:state_from_handler` events.
# @yieldparam [TimeSeries] time_series
# The time series for `:time_series_from_handler` events.
# Only available since openHAB 4.1.
# @yieldparam [Core::Things::ProfileCallback] callback
# The callback to be used to customize the action taken.
# @yieldparam [Core::Things::ItemChannelLink] link
Expand All @@ -105,6 +108,7 @@ def script(name = nil, description: nil, id: nil, tag: nil, tags: nil, **kwargs,
#
# @see org.openhab.core.thing.profiles.Profile
# @see org.openhab.core.thing.profiles.StateProfile
# @see org.openhab.core.thing.profiles.TimeSeriesProfile
#
# @example Vetoing a command
# profile(:veto_closing_shades) do |event, item:, command:|
Expand Down
49 changes: 42 additions & 7 deletions lib/openhab/dsl/rules/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@ def changed(*items, to: nil, from: nil, for: nil, attach: nil)
raise ArgumentError, "items must be an Item, GroupItem::Members, Thing, or ThingUID"
end

logger.trace("Creating changed trigger for entity(#{item}), to(#{to.inspect}), from(#{from.inspect})")
logger.trace { "Creating changed trigger for entity(#{item}), to(#{to.inspect}), from(#{from.inspect})" }

Array.wrap(from).each do |from_state|
Array.wrap(to).each do |to_state|
Expand Down Expand Up @@ -1381,7 +1381,7 @@ def on_start(at_level: nil, at_levels: nil, attach: nil)
levels.each do |level|
logger.warn "Rule engine doesn't start until start level 40" if level < 40
config = { startlevel: level }
logger.trace("Creating a SystemStartlevelTrigger with startlevel=#{level}")
logger.trace { "Creating a SystemStartlevelTrigger with startlevel=#{level}" }
Triggers::Trigger.new(rule_triggers: @rule_triggers)
.append_trigger(type: "core.SystemStartlevelTrigger", config: config, attach: attach)
end
Expand Down Expand Up @@ -1480,7 +1480,7 @@ def received_command(*items, command: nil, commands: nil, attach: nil)
raise ArgumentError, "items must be an Item or GroupItem::Members"
end
commands.each do |cmd|
logger.trace "Creating received command trigger for items #{item.inspect} and commands #{cmd.inspect}"
logger.trace { "Creating received command trigger for items #{item.inspect} and commands #{cmd.inspect}" }

command_trigger.trigger(item: item, command: cmd, attach: attach)
end
Expand Down Expand Up @@ -1699,7 +1699,7 @@ def at(item)
# end
#
def trigger(type, attach: nil, **configuration)
logger.trace("Creating trigger (#{type}) with configuration(#{configuration})")
logger.trace { "Creating trigger (#{type}) with configuration(#{configuration})" }
Triggers::Trigger.new(rule_triggers: @rule_triggers)
.append_trigger(type: type, config: configuration, attach: attach)
end
Expand Down Expand Up @@ -1802,13 +1802,48 @@ def updated(*items, to: nil, attach: nil)
raise ArgumentError, "items must be an Item, GroupItem::Members, Thing, or ThingUID"
end

logger.trace("Creating updated trigger for item(#{item}) to(#{to})")
logger.trace { "Creating updated trigger for item(#{item}) to(#{to})" }
[to].flatten.map do |to_state|
updated.trigger(item: item, to: to_state, attach: attach)
end
end.flatten
end

#
# Creates a time series updated trigger
#
# The `event` passed to run blocks will be a {OpenHAB::Core::Events::ItemTimeSeriesUpdatedEvent}
#
# @param [Item] items Items to create trigger for.
# @param [Object] attach Object to be attached to the trigger.
# @return [void]
#
# @since openHAB 4.1
# @see Core::Types::TimeSeries TimeSeries
# @see Core::Items::GenericItem#time_series= GenericItem#time_series=
#
# @example
# rule 'Execute rule when item time series is updated' do
# time_series_updated MyItem
# run do |event|
# logger.info("Item time series updated: #{event.item.name}.")
# logger.info(" TimeSeries size: #{event.time_series.size}, policy: #{event.time_series.policy}")
# event.time_series.each do |entry|
# timestamp = entry.timestamp.to_time.strftime("%Y-%m-%d %H:%M:%S")
# logger.info(" Entry: #{timestamp}: State: #{entry.state}")
# end
# end
# end
#
def time_series_updated(*items, attach: nil)
@ruby_triggers << [:time_series_updated, items]
items.map do |item|
raise ArgumentError, "items must be an Item or GroupItem::Members" unless item.is_a?(Core::Items::Item)

event("openhab/items/#{item.name}/timeseriesupdated", types: "ItemTimeSeriesUpdatedEvent", attach: attach)
end
end

#
# Create a trigger to watch a path
#
Expand Down Expand Up @@ -1987,7 +2022,7 @@ def create_rule?
elsif !execution_blocks?
logger.warn "Rule '#{uid}' has no execution blocks, not creating rule"
elsif !enabled
logger.trace "Rule '#{uid}' marked as disabled, not creating rule."
logger.trace { "Rule '#{uid}' marked as disabled, not creating rule." }
else
return true
end
Expand Down Expand Up @@ -2024,7 +2059,7 @@ def add_rule(provider, rule)
duplicate_index += 1
rule.uid = "#{base_uid} (#{duplicate_index})"
end
logger.trace("Adding rule: #{rule}")
logger.trace { "Adding rule: #{rule}" }
unmanaged_rule = Core.automation_manager.add_unmanaged_rule(rule)
provider.add(unmanaged_rule)
unmanaged_rule
Expand Down
7 changes: 7 additions & 0 deletions lib/openhab/dsl/rules/name_inference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def infer_rule_name_from_trigger(trigger, items = nil, kwargs = {})
infer_rule_name_from_item_registry_trigger(trigger)
when :thing_added, :thing_removed, :thing_updated
infer_rule_name_from_thing_trigger(trigger)
when :time_series_updated
infer_rule_name_from_time_series_trigger(items)
when :on_start
infer_rule_name_from_on_start_trigger(items)
end
Expand Down Expand Up @@ -136,6 +138,11 @@ def infer_rule_name_from_thing_trigger(trigger)
}[trigger]
end

# formulate a readable rule name from a time series updated trigger
def infer_rule_name_from_time_series_trigger(items)
"#{format_array(items.map(&:name))} time series updated"
end

# formulate a readable rule name from an on_start trigger
def infer_rule_name_from_on_start_trigger(levels)
levels = levels.map { |level| "#{level} (#{start_level_description(level)})" }
Expand Down
4 changes: 4 additions & 0 deletions lib/openhab/rspec/mocks/thing_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def handle_command(channel_uid, command)
callback&.state_updated(channel_uid, command) if command.is_a?(Core::Types::State)
end

def send_time_series(channel_uid, time_series)
@callback&.send_time_series(channel_uid, time_series)
end

def set_callback(callback)
@callback = callback
end
Expand Down
Loading

0 comments on commit e1124e2

Please sign in to comment.