From 140aac78959c87be54a86c4c18c17efd7461a9c6 Mon Sep 17 00:00:00 2001 From: Jimmy Tanagra Date: Sun, 31 Dec 2023 17:16:11 +1000 Subject: [PATCH] Fix condition handling for colors and visibility Signed-off-by: Jimmy Tanagra --- lib/openhab/core/sitemaps/provider.rb | 46 ++++-- lib/openhab/dsl/sitemaps/builder.rb | 78 +++++----- spec/openhab/dsl/sitemaps/builder_spec.rb | 176 ++++++++++++++++++---- 3 files changed, 220 insertions(+), 80 deletions(-) diff --git a/lib/openhab/core/sitemaps/provider.rb b/lib/openhab/core/sitemaps/provider.rb index a44b586a2b..5a92c403a3 100644 --- a/lib/openhab/core/sitemaps/provider.rb +++ b/lib/openhab/core/sitemaps/provider.rb @@ -52,8 +52,6 @@ def unregister @registration.unregister end - # rubocop:disable Layout/LineLength - # # Enter the Sitemap Builder DSL. # @@ -69,18 +67,45 @@ def unregister # frame label: "Control" do # text label: "Climate", icon: "if:mdi:home-thermometer-outline" do # frame label: "Main Floor" do - # text item: MainFloor_AmbTemp # # colors are set with a hash, with key being condition, and value being the color - # switch item: MainFloorThermostat_TargetMode, label: "Mode", mappings: %w[off auto cool heat], label_color: { "==heat" => "red", "" => "black" } - # # an array of conditions are OR'd together - # switch item: MainFloorThermostat_TargetMode, label: "Mode", mappings: %w[off auto cool heat], label_color: { ["==heat", "==cool"], => "green" } - # setpoint item: MainFloorThermostat_SetPoint, label: "Set Point", visibility: "MainFloorThermostat_TargetMode!=off" + # # The :default key is used when no other condition matches + # text item: MainFloor_AmbTemp, + # label_color: "purple", # A simple string can be used when no conditions are needed + # value_color: { ">=90" => "red", "<=70" => "blue", :default => "black" } + # + # # If item name is not provided in the condition, it will default to the widget's Item + # # The operator will default to == if not specified + # switch item: MainFloorThermostat_TargetMode, label: "Mode", + # mappings: %w[off auto cool heat], + # value_color: { "cool" => "blue", "heat" => "red", :default => "black" } + # + # # an array of conditions are AND'd together + # setpoint item: MainFloorThermostat_SetPoint, label: "Set Point", + # value_color: { + # ["MainFloorThermostat_TargetMode!=off", ">80"] => "red", # red if mode!=off AND setpoint > 80 + # ["MainFloorThermostat_TargetMode!=off", ">74"] => "yellow", + # ["MainFloorThermostat_TargetMode!=off", ">70"] => "green", + # "MainFloorThermostat_TargetMode!=off" => "blue", + # :default => "gray" + # } # end # frame label: "Basement" do # text item: Basement_AmbTemp - # switch item: BasementThermostat_TargetMode, label: "Mode", mappings: { OFF: "off", COOL: "cool", HEAT: "heat" } - # # nested arrays are conditions that are AND'd together, instead of OR'd (requires openHAB 4.1) - # setpoint item: BasementThermostat_SetPoint, label: "Set Point", visibility: [["BasementThermostat_TargetMode!=off", "Vacation_Switch!=OFF"]] + # switch item: BasementThermostat_TargetMode, label: "Mode", + # mappings: { OFF: "off", COOL: "cool", HEAT: "heat" } + # + # # Conditions within a nested array are AND'd together (requires openHAB 4.1) + # setpoint item: BasementThermostat_SetPoint, label: "Set Point", + # visibility: [["BasementThermostat_TargetMode!=off", "Vacation_Switch==OFF"]] + # + # # Additional elements are OR'd + # # The following visibility conditions are evaluated as: + # # (BasementThermostat_TargetMode!=off AND Vacation_Switch==OFF) OR Verbose_Mode==ON + # setpoint item: BasementThermostat_SetPoint, label: "Set Point", + # visibility: [ + # ["BasementThermostat_TargetMode!=off", "Vacation_Switch==OFF"], + # "Verbose_Mode==ON" + # ] # end # end # end @@ -90,7 +115,6 @@ def unregister def build(update: true, &block) DSL::Sitemaps::Builder.new(self, update: update).instance_eval(&block) end - # rubocop:enable Layout/LineLength # For use in specs # @!visibility private diff --git a/lib/openhab/dsl/sitemaps/builder.rb b/lib/openhab/dsl/sitemaps/builder.rb index 699ed0af0f..55834063c3 100644 --- a/lib/openhab/dsl/sitemaps/builder.rb +++ b/lib/openhab/dsl/sitemaps/builder.rb @@ -36,8 +36,14 @@ def sitemap(name, label = nil, icon: nil, &block) # Base class for all widgets # @see org.openhab.core.model.sitemap.sitemap.Widget class WidgetBuilder - # This is copied directly out of UIComponentSitemapProvider.java - CONDITION_PATTERN = /(?[A-Za-z]\w*)?\s*(?==|!=|<=|>=|<|>)?\s*(?\+|-)?(?.+)/.freeze + include Core::EntityLookup + + # This is copied out of UIComponentSitemapProvider.java + # The original pattern will match plain state e.g. "ON" as item="O" and state="N" + # this pattern is modified so it matches as item=nil and state="ON" by using atomic grouping `(?>subexpression)` + # rubocop:disable Layout/LineLength + CONDITION_PATTERN = /(?>(?[A-Za-z]\w*)?\s*(?==|!=|<=|>=|<|>))?\s*(?\+|-)?(?.+)/.freeze + # rubocop:enable Layout/LineLength private_constant :CONDITION_PATTERN # @return [String, nil] @@ -49,15 +55,15 @@ class WidgetBuilder # @see https://www.openhab.org/docs/ui/sitemaps.html#icons attr_accessor :icon # Label color rules - # @return [Hash] + # @return [Hash, Hash, String>] # @see https://www.openhab.org/docs/ui/sitemaps.html#label-value-and-icon-colors attr_reader :label_colors # Value color rules - # @return [Hash] + # @return [Hash, Hash, String>] # @see https://www.openhab.org/docs/ui/sitemaps.html#label-value-and-icon-colors attr_reader :value_colors # Icon color rules - # @return [Hash] + # @return [Hash, Hash, String>] # @see https://www.openhab.org/docs/ui/sitemaps.html#label-value-and-icon-colors attr_reader :icon_colors # Visibility rules @@ -68,10 +74,14 @@ class WidgetBuilder # @param item [String, Core::Items::Item, nil] The item whose state to show (see {#item}) # @param label [String, nil] (see {#label}) # @param icon [String, nil] (see {#icon}) - # @param label_color [String, Array, nil] One or more label color rules (see {#label_color}) - # @param value_color [String, Array, nil] One or more value color rules (see {#value_color}) - # @param icon_color [String, Array, nil] One or more icon color rules (see {#icon_color}) - # @param visibility [String, Array, nil] One or more visibility rules (see {#visibility}) + # @param label_color [String, Hash, Hash, String>, nil] + # One or more label color rules (see {#label_color}) + # @param value_color [String, Hash, Hash, String>, nil] + # One or more value color rules (see {#value_color}) + # @param icon_color [String, Hash, Hash, String>, nil] + # One or more icon color rules (see {#icon_color}) + # @param visibility [String, Array, Array>, nil] + # One or more visibility rules (see {#visibility}) # @!visibility private def initialize(type, item: nil, @@ -104,18 +114,21 @@ def initialize(type, # Adds one or more new rules for setting the label color # @return [Hash] the current rules def label_color(rules) + rules = { default: rules } if rules.is_a?(String) @label_colors.merge!(rules) end # Adds one or more new rules for setting the value color # @return [Hash] the current rules def value_color(rules) + rules = { default: rules } if rules.is_a?(String) @value_colors.merge!(rules) end # Adds one or more new rules for setting the icon color # @return [Hash] the current rules def icon_color(rules) + rules = { default: rules } if rules.is_a?(String) @icon_colors.merge!(rules) end @@ -138,25 +151,7 @@ def build add_colors(widget, :value_color, value_colors) add_colors(widget, :icon_color, icon_colors) - # @deprecated OH 4.1 - if SitemapBuilder.factory.respond_to?(:create_condition) - add_conditions(widget, :visibility, visibilities, :create_visibility_rule) - else - visibilities.each do |v| - raise ArgumentError, "AND conditions not supported prior to openHAB 4.1" if v.is_a?(Array) - - unless (match = CONDITION_PATTERN.match(v)) - raise ArgumentError, "Syntax error in visibility rule #{v.inspect}" - end - - rule = SitemapBuilder.factory.create_visibility_rule - rule.item = match["item"] - rule.condition = match["condition"] - rule.sign = match["sign"] - rule.state = match["state"] - widget.visibility.add(rule) - end - end + add_conditions(widget, :visibility, visibilities, :create_visibility_rule) widget end @@ -173,38 +168,37 @@ def inspect private - def add_colors(widget, method, conditions) - conditions.each do |condition, color| - condition = [condition] unless condition.is_a?(Array) - add_conditions(widget, method, condition, :create_color_array) do |color_array| - color_array.arg = color - end + def add_colors(widget, method, colors) + # ensure that the default color is at the end, and make the conditions nil (no conditions) + colors.delete(:default)&.tap { |default_color| colors.merge!(nil => default_color) } + + add_conditions(widget, method, colors.keys, :create_color_array) do |color_array, key| + color_array.arg = colors[key] end end def add_conditions(widget, method, conditions, container_method) return if conditions.empty? + puts conditions.inspect + object = widget.send(method) - has_and_conditions = conditions.any?(Array) - # @deprecated OH 4.1 - if !SitemapBuilder.factory.respond_to?(:create_condition) && has_and_conditions + # @deprecated OH 4.0 + if conditions.any?(Array) && !SitemapBuilder.factory.respond_to?(:create_condition) raise ArgumentError, "AND conditions not supported prior to openHAB 4.1" end - conditions = [conditions] unless has_and_conditions - conditions.each do |sub_conditions| container = SitemapBuilder.factory.send(container_method) add_conditions_to_container(container, sub_conditions) - yield container if block_given? + yield container, sub_conditions if block_given? object.add(container) end end def add_conditions_to_container(container, conditions) - # @deprecated OH 4.1 + # @deprecated OH 4.0 supports_and_conditions = SitemapBuilder.factory.respond_to?(:create_condition) Array.wrap(conditions).each do |c| @@ -591,8 +585,6 @@ def build # Parent class for builders of widgets that can contain other widgets. # @see org.openhab.core.model.sitemap.sitemap.LinkableWidget class LinkableWidgetBuilder < WidgetBuilder - include Core::EntityLookup - # allow referring to items that don't exist yet self.create_dummy_items = true diff --git a/spec/openhab/dsl/sitemaps/builder_spec.rb b/spec/openhab/dsl/sitemaps/builder_spec.rb index 9474692268..8b5f220d8b 100644 --- a/spec/openhab/dsl/sitemaps/builder_spec.rb +++ b/spec/openhab/dsl/sitemaps/builder_spec.rb @@ -25,37 +25,148 @@ end end - it "supports visibility" do - s = sitemaps.build do - sitemap "default", "My Residence" do - switch item: "Switch1", visibility: "Switch1 == ON" + describe "#visibility" do + it "supports a simple condition" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", visibility: "Switch1 == ON" + end + end + + switch = s.children.first + cond = switch.visibility.first + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to eq "Switch1" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" + end + + it "supports a condition with just the state" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", visibility: "ON" + end + end + + switch = s.children.first + cond = switch.visibility.first + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to be_nil + expect(cond.condition).to be_nil + expect(cond.state).to eq "ON" + end + + it "supports a condition with operator and state" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", visibility: "==ON" + end end + + switch = s.children.first + cond = switch.visibility.first + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to be_nil + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" end - switch = s.children.first - cond = switch.visibility.first - # @deprecated OH 4.0 - cond = cond.conditions.first if cond.respond_to?(:conditions) - expect(cond.item).to eq "Switch1" - expect(cond.condition.to_s).to eq "==" - expect(cond.state).to eq "ON" + it "supports multiple conditions" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", visibility: ["Switch1 == ON", "Switch2 == ON"] + end + end + + switch = s.children.first + + expect(switch.visibility.size).to eq 2 + + cond = switch.visibility.first + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to eq "Switch1" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" + + cond = switch.visibility.last + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to eq "Switch2" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" + end end - it "supports colors" do - s = sitemaps.build do - sitemap "default", "My Residence" do - switch item: "Switch1", label_color: { "Switch1 == ON" => "green" } + context "with colors" do + it "supports conditions" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", label_color: { "Switch1 == ON" => "green" } + end end + + switch = s.children.first + cond = rule = switch.label_color.first + # @deprecated OH 4.0 + cond = cond.conditions.first if cond.respond_to?(:conditions) + expect(cond.item).to eq "Switch1" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" + expect(rule.arg).to eq "green" end - switch = s.children.first - cond = rule = switch.label_color.first - # @deprecated OH 4.0 - cond = cond.conditions.first if cond.respond_to?(:conditions) - expect(cond.item).to eq "Switch1" - expect(cond.condition.to_s).to eq "==" - expect(cond.state).to eq "ON" - expect(rule.arg).to eq "green" + it "supports conditions with default value" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", label_color: { "Switch1 == ON" => "green", :default => "red" } + end + end + + switch = s.children.first + rules = switch.label_color + expect(rules.size).to eq 2 + + default = rules.last + # @deprecated OH 4.0 + if default.respond_to?(:conditions) + expect(default.conditions).to be_empty + else + expect(default.condition.to_s).to be_empty + end + expect(default.arg).to eq "red" + end + + it "supports non-conditional string value" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1", label_color: "red" + end + end + + switch = s.children.first + expect(switch.label_color.size).to eq 1 + rule = switch.label_color.first + expect(rule.arg).to eq "red" + end + + it "supports simple color as the default in multiple calls" do + s = sitemaps.build do + sitemap "default", "My Residence" do + switch item: "Switch1" do + label_color "red" # The default doesn't have to be specified last + label_color "Switch1 == ON" => "green" + end + end + end + + switch = s.children.first + expect(switch.label_color.first.arg).to eq "green" + expect(switch.label_color.last.arg).to eq "red" + end end # @deprecated OH 4.0 @@ -70,11 +181,24 @@ end it "supports AND conditions on colors" do - sitemaps.build do + s = sitemaps.build do sitemap "default", "My Residence" do - switch item: "Switch1", label_color: { [["Switch1 == ON", "Switch2 == OFF"]] => "green" } + switch item: "Switch1", label_color: { ["Switch1 == ON", "Switch2 == OFF"] => "green" } end end + + switch = s.children.first + rule = switch.label_color.first + cond = rule.conditions.first + expect(cond.item).to eq "Switch1" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "ON" + + cond = cond.conditions.last + expect(cond.item).to eq "Switch2" + expect(cond.condition.to_s).to eq "==" + expect(cond.state).to eq "OFF" + expect(rule.arg).to eq "green" end else it "does not support AND conditions on visibility" do @@ -91,7 +215,7 @@ expect do sitemaps.build do sitemap "default", "My Residence" do - switch item: "Switch1", label_color: { [["Switch1 == ON", "Switch2 == OFF"]] => "green" } + switch item: "Switch1", label_color: { ["Switch1 == ON", "Switch2 == OFF"] => "green" } end end end.to raise_error(ArgumentError)