Skip to content

Commit

Permalink
Support all attributes of channel builder (#178)
Browse files Browse the repository at this point in the history
Note, some (all?) bindings provide default values for some channel
attributes, e.g. `acceptedItemType`, `label`, etc. when they are not
specified. This will cause thing builder to update/replace the created
thing instead of leaving it unchanged. The update will cause the Thing
status will change to OFFLINE, then back ONLINE.

By specifying those values during the channel creation, the Thing
builder will see that the new Thing being built is exactly the same as
the existing Thing, and will not update the existing thing, thereby
avoiding the change in Thing status.

Demonstration:

```ruby
things.build do
  thing "mqtt:topic:mything", broker: "mqtt:broker:mybroker" do
    channel "signal", "number", config: { stateTopic: "xxxx" }
  end
end
# Once built, the binding will set a default label and acceptedItemType

... 
# Try to recreate, Thing will be updated because the specified channel attributes are "different" from the existing thing
things.build do
  thing "mqtt:topic:mything", broker: "mqtt:broker:mybroker" do
    channel "signal", "number", config: { stateTopic: "xxxx" }
  end
end
```

MQTT will add a default label and acceptedItemType: "Number" to the
above channel, so when we try to recreate it again, the thing builder
noticed that the channels have different attributes, so the builder will
update the Thing instead of leaving it alone.

To avoid this problem:

```ruby
things.build do
  thing "mqtt:topic:mything", broker: "mqtt:broker:mybroker" do
    channel "signal", "number", "custom label", config: { stateTopic: "xxxx" }, accepted_item_type: "Number"
  end
end

... 
# Try to recreate, Thing will not need to be updated because now all attributes match
things.build do
  thing "mqtt:topic:mything", broker: "mqtt:broker:mybroker" do
    channel "signal", "number", "custom label", config: { stateTopic: "xxxx" }, accepted_item_type: "Number"
  end
end
```

Signed-off-by: Jimmy Tanagra <[email protected]>
  • Loading branch information
jimtng authored Oct 9, 2023
1 parent 13efdfc commit ce75a54
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 20 deletions.
28 changes: 28 additions & 0 deletions lib/openhab/core/things/channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,34 @@ def inspect
def to_s
uid.to_s
end

# @deprecated OH3.4 this whole section is not needed in OH4+. Also see Thing#config_eql?
if Gem::Version.new(Core::VERSION) < Gem::Version.new("4.0.0")
# @!visibility private
module ChannelComparable
# @!visibility private
# This is only needed in OH3 because it is implemented in OH4 core
def ==(other)
return true if equal?(other)
return false unless other.is_a?(Channel)

%i[class
uid
label
description
kind
channel_type_uid
configuration
properties
default_tags
auto_update_policy
accepted_item_type].all? do |attr|
send(attr) == other.send(attr)
end
end
end
org.openhab.core.thing.binding.builder.ChannelBuilder.const_get(:ChannelImpl).prepend(ChannelComparable)
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/openhab/core/things/thing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ def actions(scope = nil)
# @return [true,false] true if all attributes are equal, false otherwise
#
def config_eql?(other)
%i[uid label channels bridge_uid location configuration].all? { |method| send(method) == other.send(method) }
# @deprecated OH3.4 - in OH4, channels can be included in the array and do not need to be compared separately
channels.to_a == other.channels.to_a &&
%i[uid label bridge_uid location configuration].all? { |method| send(method) == other.send(method) }
end

#
Expand Down
47 changes: 43 additions & 4 deletions lib/openhab/dsl/things/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,14 @@ def thing(*args, **kwargs, &block)
# The ChannelBuilder DSL allows you to customize a channel
class ChannelBuilder
attr_accessor :label
attr_reader :uid, :config, :type
attr_reader :uid,
:config,
:type,
:default_tags,
:properties,
:description,
:auto_update_policy,
:accepted_item_type

#
# Constructor for ChannelBuilder
Expand All @@ -269,10 +276,27 @@ class ChannelBuilder
# @param [String] label The channel label.
# @param [thing] thing The thing associated with this channel.
# This parameter is not needed for the {ThingBuilder#channel} method.
# @param [String] description The channel description.
# @param [String] group The group name.
# @param [Hash] config Channel configuration. The keys can be strings or symbols.
# @param [Hash] properties The channel properties.
# @param [String,Symbol,Semantics::Tag,Array<String,Symbol,Semantics::Tag>] default_tags
# The default tags for this channel.
# @param [:default, :recommend, :veto, org.openhab.core.thing.type.AutoUpdatePolicy] auto_update_policy
# The channel's auto update policy.
# @param [String] accepted_item_type The accepted item type.
#
def initialize(uid, type, label = nil, thing:, group: nil, config: {})
def initialize(uid,
type,
label = nil,
thing:,
description: nil,
group: nil,
config: nil,
properties: nil,
default_tags: nil,
auto_update_policy: nil,
accepted_item_type: nil)
@thing = thing

uid = uid.to_s
Expand All @@ -292,15 +316,30 @@ def initialize(uid, type, label = nil, thing:, group: nil, config: {})
end
@type = type
@label = label
@config = config.transform_keys(&:to_s)
@config = config&.transform_keys(&:to_s)
@default_tags = Items::ItemBuilder.normalize_tags(*Array.wrap(default_tags))
@properties = properties&.transform_keys(&:to_s)
@description = description
@accepted_item_type = accepted_item_type
return unless auto_update_policy

@auto_update_policy = org.openhab.core.thing.type.AutoUpdatePolicy.value_of(auto_update_policy.to_s.upcase)
end

# @!visibility private
def build
org.openhab.core.thing.binding.builder.ChannelBuilder.create(uid)
.with_kind(kind)
.with_type(type)
.with_configuration(Core::Configuration.new(config))
.tap do |builder|
builder.with_label(label) if label
builder.with_configuration(Core::Configuration.new(config)) if config && !config.empty?
builder.with_default_tags(Set.new(default_tags).to_java) unless default_tags.empty?
builder.with_properties(properties) if properties
builder.with_description(description) if description
builder.with_auto_update_policy(auto_update_policy) if auto_update_policy
builder.with_accepted_item_type(accepted_item_type) if accepted_item_type
end
.build
end

Expand Down
165 changes: 150 additions & 15 deletions spec/openhab/dsl/things/builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,27 @@ def build_and_update(org_config, new_config, thing_to_keep: :new_thing, &block)
new_config = new_config.dup
default_uid = uid
org_thing = things.build do
thing(org_config.delete(:uid) || default_uid, org_config.delete(:label), **org_config)
channels = org_config.delete(:channels)
thing(org_config.delete(:uid) || default_uid, org_config.delete(:label), **org_config) do
channels&.each do |c|
c = c.dup
channel(c.delete(:uid), c.delete(:type), **c)
end
end
end
# Unwrap the thing object now before creating the new thing.
# See the comment in items/builder_spec.rb for more details.
org_thing = org_thing.__getobj__
yield :original, org_thing if block

new_thing = things.build do
thing(new_config.delete(:uid) || default_uid, new_config.delete(:label), **new_config)
channels = new_config.delete(:channels)
thing(new_config.delete(:uid) || default_uid, new_config.delete(:label), **new_config) do
channels&.each do |c|
c = c.dup
channel(c.delete(:uid), c.delete(:type), **c)
end
end
end
new_thing = new_thing.__getobj__
yield :updated, new_thing if block
Expand All @@ -111,6 +123,17 @@ def build_and_update(org_config, new_config, thing_to_keep: :new_thing, &block)
[org_thing, new_thing]
end

it "keeps the original thing when there are no changes" do
config = {
uid: "a:b:c",
label: "Label",
bridge: "a:b:bridge",
config: { ipAddress: "1" },
channels: [{ uid: "a", type: "string", config: { stateTopic: "a/b/c" } }]
}.freeze
build_and_update(config, config, thing_to_keep: :old_thing)
end

context "with changes" do
it "replaces the old thing when the label is different" do
build_and_update({ label: "Old Label" }, { label: "New Label" })
Expand All @@ -132,10 +155,6 @@ def build_and_update(org_config, new_config, thing_to_keep: :new_thing, &block)
expect(thing.configuration[:ipAddress]).to eq "2"
end

it "keeps the old thing when nothing is different" do
build_and_update({}, {}, thing_to_keep: :old_thing)
end

it "keeps the old thing but update the state" do
build_and_update({}, { enabled: false }, thing_to_keep: :old_thing)
expect(thing).not_to be_enabled
Expand All @@ -159,16 +178,132 @@ def build_and_update(org_config, new_config, thing_to_keep: :new_thing, &block)
expect(home.configuration.get("geolocation")).to eq "0,0"
end

it "can create channels" do
things.build do
thing "astro:sun:home" do
channel "channeltest", "string", config: { config1: "testconfig" }
context "with channels" do
it "works" do
things.build do
thing "astro:sun:home" do
channel "channeltest", "string"
end
end
expect(home = things["astro:sun:home"]).not_to be_nil
expect(home.channels["channeltest"].uid.to_s).to eql "astro:sun:home:channeltest"
end

context "with channel attributes" do
it "supports config" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", config: { config1: "testconfig" }
end
end
channel = thing.channels["channeltest"]
expect(channel.configuration).to have_key("config1")
expect(channel.configuration.get("config1")).to eq "testconfig"
end

context "with default_tags" do
it "accepts a string tag" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: "tag1"
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[tag1]
end

it "accepts a symbolic tag" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: :tag1
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[tag1]
end

it "accepts a Semantic tag constant" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: Semantics::Status
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[Status]
end

it "accepts an array of symbolic tag" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: %i[tag1]
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[tag1]
end

it "accepts an array of string tag" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: %w[tag1]
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[tag1]
end

it "accepts an array of Semantic tag constants" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest", "string", default_tags: [Semantics::Status]
end
end
expect(thing.channels["channeltest"].default_tags.to_a).to eq %w[Status]
end
end

context "with auto_update_policy" do
it "accepts symbolic policy" do
thing = things.build do
thing "astro:sun:home" do
org.openhab.core.thing.type.AutoUpdatePolicy.values.each do |policy| # rubocop:disable Style/HashEachMethods
channel policy.to_s.downcase, "string", auto_update_policy: policy.to_s.downcase.to_sym
end
end
end

org.openhab.core.thing.type.AutoUpdatePolicy.values.each do |policy| # rubocop:disable Style/HashEachMethods
expect(thing.channels[policy.to_s.downcase].auto_update_policy).to eq policy
end
end

it "accepts AutoUpdatePolicy enum" do
thing = things.build do
thing "astro:sun:home" do
org.openhab.core.thing.type.AutoUpdatePolicy.values.each do |policy| # rubocop:disable Style/HashEachMethods
channel policy.to_s.downcase, "string", auto_update_policy: policy
end
end
end

org.openhab.core.thing.type.AutoUpdatePolicy.values.each do |policy| # rubocop:disable Style/HashEachMethods
expect(thing.channels[policy.to_s.downcase].auto_update_policy).to eq policy
end
end
end

it "supports label, description, properties, and accepted_item_type attributes" do
thing = things.build do
thing "astro:sun:home" do
channel "channeltest",
"string",
"testlabel",
description: "testdescription",
properties: { property1: "testproperty" },
accepted_item_type: "Number"
end
end
channel = thing.channels["channeltest"]
expect(channel.label).to eq "testlabel"
expect(channel.description).to eq "testdescription"
expect(channel.properties).to eq("property1" => "testproperty")
expect(channel.accepted_item_type).to eq "Number"
end
end
expect(home = things["astro:sun:home"]).not_to be_nil
expect(home.channels.map { |c| c.uid.to_s }).to include("astro:sun:home:channeltest")
channel = home.channels.find { |c| c.uid.id == "channeltest" }
expect(channel.configuration.properties).to have_key("config1")
expect(channel.configuration.get("config1")).to eq "testconfig"
end
end

0 comments on commit ce75a54

Please sign in to comment.