Skip to content

Commit

Permalink
Support updating existing things/items/sitemaps
Browse files Browse the repository at this point in the history
Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng committed Sep 24, 2023
1 parent 47f311b commit c288419
Show file tree
Hide file tree
Showing 14 changed files with 442 additions and 25 deletions.
14 changes: 14 additions & 0 deletions lib/openhab/core/items/group_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ def #{type}_item? # def date_time_item?
RUBY
end

#
# Compares all attributes of the item with another item.
#
# @param other [Item] The item to compare with
# @return [true,false] true if all attributes are equal, false otherwise
#
def config_eql?(other)
return false unless super

%i[base_item function].all? do |method|
public_send(method).to_s == other.public_send(method).to_s
end
end

private

# Add base type and function details
Expand Down
62 changes: 59 additions & 3 deletions lib/openhab/core/items/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,19 @@ def all_groups
# # Item1's metadata after: {"namespace2"=>["value", {"config1"=>"foo", "config2"=>"bar"}], "namespace"=>["value", {"config1"=>"foo", "config2"=>"bar"}]}
#
def metadata
@metadata ||= Metadata::NamespaceHash.new(name)
@metadata ||= Metadata::NamespaceHash.new(provider ? name : nil)
end
# rubocop:enable Layout/LineLength

# Attaches the metadata when the item is added to the provider
# See Items::Provider#add and #update
# @!visibility private
def attach_metadata!
return unless @metadata && !@metadata.attached? && provider

@metadata = Metadata::NamespaceHash.new(name).replace(@metadata)
end

#
# Checks if this item has at least one of the given tags.
#
Expand Down Expand Up @@ -264,12 +273,17 @@ def thing
#
# @return [Array<Thing>] An array of things or an empty array
def things
registry = Things::Links::Provider.registry
channels = registry.get_bound_channels(name).to_a
channels.map(&:thing_uid).uniq.map { |tuid| EntityLookup.lookup_thing(tuid) }.compact
end
alias_method :all_linked_things, :things

# Returns all of the item's channels.
#
# @return [Array<ChannelUID>] An array of channel UIDs or an empty array
def channels
Things::Links::Provider.registry.get_bound_channels(name).to_a
end

# @return [String]
def inspect
s = "#<OpenHAB::Core::Items::#{type}Item#{type_details} #{name} #{label.inspect} state=#{raw_state.inspect}"
Expand All @@ -286,6 +300,48 @@ def provider
Provider.registry.provider_for(self)
end

#
# Compares all attributes of the item with another item.
#
# @param other [Item] The item to compare with
# @return [true,false] true if all attributes are equal, false otherwise
#
def config_eql?(other)
return false unless eql?(other)

properties_match = %i[
label
category
tags
group_names
].all? do |method|
public_send(method) == other.public_send(method)
end
return false unless properties_match

# Only compare unattached metadata and metadata from ManagedProvider
# and exclude those added by MetadataProviders (e.g. SemanticsMetadataProvider)
# because at item creation prior to registering the item and attaching the metadata,
# Metadata Providers haven't been notified to add their metadata yet.
managed_or_unattached = proc do |_, hash|
case hash
when ::Hash then true
when Metadata::Hash
next true unless hash.attached?

Metadata::Provider.registry.provider_for(hash.uid)
&.is_a?(org.openhab.core.common.registry.ManagedProvider)
end
end

our_metadata = metadata.select(&managed_or_unattached)
other_metadata = other.metadata.select(&managed_or_unattached)

our_metadata.keys.sort == other_metadata.keys.sort && our_metadata.all? do |key, value|
other_metadata[key].to_h == value.to_h
end
end

def_type_predicate(:color)
def_type_predicate(:contact)
def_type_predicate(:date_time)
Expand Down
7 changes: 7 additions & 0 deletions lib/openhab/core/items/number_item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ def format_type(command)
super
end

def config_eql?(other)
return false unless super

# super calls eql? to ensure that other is a NumberItem
dimension == other.dimension
end

protected

# Adds the unit dimension
Expand Down
20 changes: 20 additions & 0 deletions lib/openhab/core/items/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ def remove(item_name, recursive = false) # rubocop:disable Style/OptionalBoolean
item
end

#
# Add an item to this provider
#
# @param [Item] item
# @return [Item] The added item
#
def add(item)
super.tap { item.attach_metadata! }
end

#
# Update an item in this provider
#
# @param [Item] item The new item
# @return [Item] The old item
#
def update(item)
super.tap { item.attach_metadata! }
end

def initialize
super(unload_priority: 50)
end
Expand Down
6 changes: 4 additions & 2 deletions lib/openhab/core/items/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ def to_a
# Enter the Item Builder DSL.
#
# @param (see Core::Provider.current)
# @param update [true, false] Update existing items with the same name.
# @yield Block executed in the context of a {DSL::Items::Builder}
# @return [Object] The return value of the block.
#
# @see DSL::Items::Builder
#
def build(preferred_provider = nil, &block)
DSL::Items::BaseBuilderDSL.new(preferred_provider).instance_eval_with_dummy_items(&block)
def build(preferred_provider = nil, update: false, &block)
DSL::Items::BaseBuilderDSL.new(preferred_provider, update: update)
.instance_eval_with_dummy_items(&block)
end

#
Expand Down
11 changes: 7 additions & 4 deletions lib/openhab/core/sitemaps/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def unregister
#
# Enter the Sitemap Builder DSL.
#
# @param update [true, false] When true, existing sitemaps with the same name will be updated.
# @yield Block executed in the context of a {DSL::Sitemaps::Builder}
# @return [void]
#
Expand All @@ -82,8 +83,8 @@ def unregister
# end
# end
#
def build(&block)
DSL::Sitemaps::Builder.new(self).instance_eval(&block)
def build(update: false, &block)
DSL::Sitemaps::Builder.new(self, update: update).instance_eval(&block)
end
# rubocop:enable Layout/LineLength

Expand Down Expand Up @@ -123,8 +124,10 @@ def notify_listeners_about_removed_element(element)
@listeners.each { |l| l.model_changed(element.name, org.openhab.core.model.core.EventType::REMOVED) }
end

def notify_listeners_about_updated_element(element)
@listeners.each { |l| l.model_changed(element.name, org.openhab.core.model.core.EventType::MODIFIED) }
def notify_listeners_about_updated_element(_old_element, element)
@listeners.each do |l|
l.model_changed(element.name, org.openhab.core.model.core.EventType::MODIFIED)
end
end
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/openhab/core/things/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ def to_a
#
# Enter the Thing Builder DSL.
# @param (see Core::Provider.current)
# @param update [true, false]
# When true, existing items with the same name will be redefined if they're different.
# @yield Block executed in the context of a {DSL::Things::Builder}.
# @return [Object] The result of the block.
#
def build(preferred_provider = nil, &block)
DSL::Things::Builder.new(preferred_provider).instance_eval(&block)
def build(preferred_provider = nil, update: false, &block)
DSL::Things::Builder.new(preferred_provider, update: update).instance_eval(&block)
end

#
Expand Down
20 changes: 20 additions & 0 deletions lib/openhab/core/things/thing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,26 @@ def actions(scope = nil)
$actions.get(scope || uid.binding_id, uid.to_s)
end

#
# Compares all attributes of the thing with another thing.
#
# @param other [Thing] The thing to compare with
# @return [true,false] true if all attributes are equal, false otherwise
#
def config_eql?(other)
return false unless eql?(other)

%i[
uid
label
channels
bridgeuid
location
].all? do |method|
public_send(method) == other.public_send(method)
end
end

#
# Delegate missing methods to the thing's default actions scope.
#
Expand Down
50 changes: 43 additions & 7 deletions lib/openhab/dsl/items/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,60 @@ class BaseBuilderDSL
class ProviderWrapper
attr_reader :provider

def initialize(provider)
def initialize(provider, update:)
@provider = provider
@update = update
end

# @!visibility private
def add(builder)
item = builder.build
provider.add(item)
# make sure to add the item to the registry before linking it
builder.channels.each do |(channel, config)|
if @update && (old_item = provider[item.uid])
if item.config_eql?(old_item)
logger.debug { "Not replacing existing item #{item.uid} because it is identical" }
item = old_item
else
logger.debug { "Replacing existing item #{item.uid} because it is not identical" }
provider.update(item)
end
else
provider.add(item)
end
item.update(builder.state) unless builder.state.nil?

# fill in partial channel names from group's thing id
channels = builder.channels.map do |(channel, config)|
if !channel.include?(":") &&
(group = builder.groups.find { |g| g.is_a?(GroupItemBuilder) && g.thing })
thing = group.thing
channel = "#{thing}:#{channel}"
end
[channel, config]
end

# make sure to add the item to the registry before linking it
provider = Core::Things::Links::Provider.current
channels.each do |(channel, config)|
current_link = provider.all.find { |link| link.item_name == item.name && link.linked_uid == channel }
if current_link
next if current_link.configuration == config

provider.remove(current_link.uid)
end

Core::Things::Links::Provider.link(item, channel, config)
end

# remove links not in the new item
removed_links = provider.all.select do |link|
next unless link.item_name == item.name

channels.none? { |(channel, _config)| channel == link.linked_uid }
end
removed_links.each do |link|
provider.remove(link.uid)
end

item
end
end
Expand All @@ -143,8 +180,8 @@ def add(builder)
# @return [org.openhab.core.items.ItemProvider]
attr_reader :provider

def initialize(provider)
@provider = ProviderWrapper.new(Core::Items::Provider.current(provider))
def initialize(provider, update:)
@provider = ProviderWrapper.new(Core::Items::Provider.current(provider), update: update)
end
end

Expand Down Expand Up @@ -542,7 +579,6 @@ def build
item.metadata["expire"] = expire if expire
item.metadata["stateDescription"] = { "pattern" => format } if format
item.metadata["unit"] = unit if unit
item.state = item.format_update(state) unless state.nil?
item
end

Expand Down
11 changes: 8 additions & 3 deletions lib/openhab/dsl/sitemaps/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ module Sitemaps
# Base sitemap builder DSL
class Builder
# @!visibility private
def initialize(provider)
def initialize(provider, update:)
@provider = provider
@update = update
end

# (see SitemapBuilder#initialize)
Expand All @@ -23,8 +24,12 @@ def initialize(provider)
def sitemap(name, label = nil, icon: nil, &block)
sitemap = SitemapBuilder.new(name, label, icon: icon)
sitemap.instance_eval_with_dummy_items(&block) if block
@provider.add(sitemap.build)
sitemap
sitemap = sitemap.build
if @update && @provider[sitemap.uid]
@provider.update(sitemap)
else
@provider.add(sitemap)
end
end
end

Expand Down
15 changes: 13 additions & 2 deletions lib/openhab/dsl/things/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ class Builder
# @return [org.openhab.core.thing.ManagedThingProvider]
attr_reader :provider

def initialize(provider)
def initialize(provider, update: false)
@provider = Core::Things::Provider.current(provider)
@update = update
end

# Create a new Bridge
Expand All @@ -61,7 +62,17 @@ def thing(*args, **kwargs, &block)
def build(klass, *args, **kwargs, &block)
builder = klass.new(*args, **kwargs)
builder.instance_eval(&block) if block
thing = provider.add(builder.build)
thing = builder.build
if @update && (old_thing = provider[thing.uid])
if thing.config_eql?(old_thing)
logger.debug { "Not replacing existing thing #{thing.uid}" }
thing = old_thing
else
provider.update(thing)
end
else
provider.add(thing)
end
thing = Core::Things::Proxy.new(thing)
thing.enable(enabled: builder.enabled) unless builder.enabled.nil?
thing
Expand Down
Loading

0 comments on commit c288419

Please sign in to comment.