From 9acceb63b5e70e4bb014ca9afecb4289c8d6c6f2 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 8 Nov 2024 13:10:02 -0700 Subject: [PATCH] Improve errors when an item no longer exists If a user keeps a ref to an item, and that item disappears, calling a method on that item will now raise a helpful StaleProxyError, instead of a NoMethodError on nil The proxy code was significantly refactored, and Thing proxies will also inherit the same stale proxy handling. I also removed the $things registry lookup on every access of a thing, using the same event subscriber mechanism as Item. I also moved several methods from GenericItem to Item, so that Proxy can detect that those methods should exist even for dummy/stale items. Signed-off-by: Cody Cutrer --- CHANGELOG.md | 2 +- lib/openhab/core/entity_lookup.rb | 2 +- lib/openhab/core/items/generic_item.rb | 151 ----------------------- lib/openhab/core/items/item.rb | 151 +++++++++++++++++++++++ lib/openhab/core/items/persistence.rb | 2 +- lib/openhab/core/items/proxy.rb | 127 +++++-------------- lib/openhab/core/items/semantics.rb | 2 +- lib/openhab/core/proxy.rb | 164 ++++++++++++++++++++++++- lib/openhab/core/things/proxy.rb | 59 ++------- lib/openhab/dsl/items/ensure.rb | 2 +- lib/openhab/dsl/items/timed_command.rb | 2 +- spec/openhab/core/items/proxy_spec.rb | 10 +- 12 files changed, 364 insertions(+), 310 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d643404de0..a7cf583e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -723,7 +723,7 @@ Here is a non-exhaustive list of significant departures from the original gem: - {OpenHAB::Core::Events::ItemStateEvent} and {OpenHAB::Core::Events::ItemStateChangedEvent} now have full sets of predicate methods. - {OpenHAB::DSL::Rules::Terse terse rules} now have an `on_load` parameter. - {Item#all_groups Item#all_groups}, {Enumerable#all_members}, {Enumerable#groups}, {Enumerable#all_groups}. -- {GenericItem#formatted_state GenericItem#formatted_state}. +- {Item#formatted_state Item#formatted_state}. - {OpenHAB::DSL.transform} now available at top-level, like Rules DSL. - {Item#member_of? Item#member_of?}, {Item#tagged? Item#tagged?}. - {OpenHAB::DSL::Rules::BuilderDSL.watch watch} can now be used to monitor subdirectories diff --git a/lib/openhab/core/entity_lookup.rb b/lib/openhab/core/entity_lookup.rb index 4bd11176be..b4a1dda2ca 100644 --- a/lib/openhab/core/entity_lookup.rb +++ b/lib/openhab/core/entity_lookup.rb @@ -160,7 +160,7 @@ class << self def lookup_entity(name, create_dummy_items: false) # make sure we have a nil return create_dummy_items = nil if create_dummy_items == false - lookup_item(name) || lookup_thing_const(name) || (create_dummy_items && Items::Proxy.new(name.to_sym)) + lookup_item(name) || lookup_thing_const(name) || (create_dummy_items && Items::Proxy.new(name.to_s)) end # diff --git a/lib/openhab/core/items/generic_item.rb b/lib/openhab/core/items/generic_item.rb index 29d2c91964..882a90ae73 100644 --- a/lib/openhab/core/items/generic_item.rb +++ b/lib/openhab/core/items/generic_item.rb @@ -38,24 +38,6 @@ def ACCEPTED_DATA_TYPES def ===(other) other.is_a?(self) end - - # @!visibility private - def item_states_event_builder - @item_states_event_builder ||= - OpenHAB::OSGi.service("org.openhab.core.io.rest.sse.internal.SseItemStatesEventBuilder")&.tap do |builder| - m = builder.class.java_class.get_declared_method("getDisplayState", Item, java.util.Locale) - m.accessible = true - builder.instance_variable_set(:@getDisplayState, m) - # Disable "singleton on non-persistent Java type" - original_verbose = $VERBOSE - $VERBOSE = nil - def builder.get_display_state(item) - @getDisplayState.invoke(self, item, nil) - end - ensure - $VERBOSE = original_verbose - end - end end # rubocop:enable Naming/MethodName @@ -89,21 +71,6 @@ def state? !raw_state.is_a?(Types::UnDefType) end - # @!attribute [r] formatted_state - # - # Format the item's state according to its state description - # - # This may include running a transformation. - # - # @return [String] The formatted state - # - # @example - # logger.info(Exterior_WindDirection.formatted_state) # => "NE (36°)" - # - def formatted_state - GenericItem.item_states_event_builder.get_display_state(self) - end - # # @!attribute [r] state # @return [State, nil] @@ -124,124 +91,6 @@ def state # Check if the item state == {UNDEF} # @return [true,false] - # - # Send a command to this item - # - # When this method is chained after the {OpenHAB::DSL::Items::Ensure::Ensurable#ensure ensure} - # method, or issued inside an {OpenHAB::DSL.ensure_states ensure_states} block, or after - # {OpenHAB::DSL.ensure_states! ensure_states!} have been called, - # the command will only be sent if the item is not already in the same state. - # - # The similar method `command!`, however, will always send the command regardless of the item's state. - # - # @param [Command, #to_s] command command to send to the item. - # When given a {Command} argument, it will be passed directly. - # Otherwise, the result of `#to_s` will be parsed into a {Command}. - # @param [String, nil] source Optional string to identify what sent the event. - # @return [self, nil] nil when `ensure` is in effect and the item was already in the same state, - # otherwise the item. - # - # @see DSL::Items::TimedCommand#command Timed Command - # @see OpenHAB::DSL.ensure_states ensure_states - # @see OpenHAB::DSL.ensure_states! ensure_states! - # @see DSL::Items::Ensure::Ensurable#ensure ensure - # - # @example Sending a {Command} to an item - # MySwitch.command(ON) # The preferred method is `MySwitch.on` - # Garage_Door.command(DOWN) # The preferred method is `Garage_Door.down` - # SetTemperature.command 20 | "°C" - # - # @example Sending a plain number to a {NumberItem} - # SetTemperature.command(22.5) # if it accepts a DecimalType - # - # @example Sending a string to a dimensioned {NumberItem} - # SetTemperature.command("22.5 °C") # The string will be parsed and converted to a QuantityType - # - def command(command, source: nil) - command = format_command(command) - logger.trace { "Sending Command #{command} to #{name}" } - if source - Events.publisher.post(Events::ItemEventFactory.create_command_event(name, command, source.to_s)) - else - $events.send_command(self, command) - end - Proxy.new(self) - end - alias_method :command!, :command - - # not an alias to allow easier stubbing and overriding - def <<(command) - command(command) - end - - # @!parse alias_method :<<, :command - - # @!method refresh - # Send the {REFRESH} command to the item - # @return [Item] `self` - - # - # Send an update to this item - # - # @param [State, #to_s, nil] state the state to update the item. - # When given a {State} argument, it will be passed directly. - # Otherwise, the result of `#to_s` will be parsed into a {State} first. - # If `nil` is passed, the item will be updated to {NULL}. - # @return [self, nil] nil when `ensure` is in effect and the item was already in the same state, - # otherwise the item. - # - # @example Updating to a {State} - # DoorStatus.update(OPEN) - # InsideTemperature.update 20 | "°C" - # - # @example Updating to {NULL}, the two following are equivalent: - # DoorStatus.update(nil) - # DoorStatus.update(NULL) - # - # @example Updating with a plain number - # PeopleCount.update(5) # A plain NumberItem - # - # @example Updating with a string to a dimensioned {NumberItem} - # InsideTemperature.update("22.5 °C") # The string will be parsed and converted to a QuantityType - # - def update(state) - state = format_update(state) - logger.trace { "Sending Update #{state} to #{name}" } - $events.post_update(self, state) - Proxy.new(self) - end - alias_method :update!, :update - - # @!visibility private - def format_command(command) - command = format_type(command) - return command if command.is_a?(Types::Command) - - command = command.to_s - org.openhab.core.types.TypeParser.parse_command(getAcceptedCommandTypes, command) || command - end - - # @!visibility private - def format_update(state) - state = format_type(state) - return state if state.is_a?(Types::State) - - state = state.to_s - org.openhab.core.types.TypeParser.parse_state(getAcceptedDataTypes, state) || StringType.new(state) - end - - # formats a {Types::Type} to send to the event bus - # @!visibility private - def format_type(type) - # actual Type types can be sent directly without conversion - # make sure to use Type, because this method is used for both - # #update and #command - return type if type.is_a?(Types::Type) - return NULL if type.nil? - - type.to_s - end - # # @method time_series=(time_series) # Set a new time series. diff --git a/lib/openhab/core/items/item.rb b/lib/openhab/core/items/item.rb index 581d442b24..c63b3f5b75 100644 --- a/lib/openhab/core/items/item.rb +++ b/lib/openhab/core/items/item.rb @@ -19,6 +19,24 @@ def ===(other) other.is_a?(self) end + # @!visibility private + def item_states_event_builder + @item_states_event_builder ||= + OpenHAB::OSGi.service("org.openhab.core.io.rest.sse.internal.SseItemStatesEventBuilder")&.tap do |builder| + m = builder.class.java_class.get_declared_method("getDisplayState", Item, java.util.Locale) + m.accessible = true + builder.instance_variable_set(:@getDisplayState, m) + # Disable "singleton on non-persistent Java type" + original_verbose = $VERBOSE + $VERBOSE = nil + def builder.get_display_state(item) + @getDisplayState.invoke(self, item, nil) + end + ensure + $VERBOSE = original_verbose + end + end + private # @!macro def_type_predicate @@ -55,6 +73,139 @@ def to_s label || name end + # @!attribute [r] formatted_state + # + # Format the item's state according to its state description + # + # This may include running a transformation. + # + # @return [String] The formatted state + # + # @example + # logger.info(Exterior_WindDirection.formatted_state) # => "NE (36°)" + # + def formatted_state + Item.item_states_event_builder.get_display_state(self) + end + + # + # Send a command to this item + # + # When this method is chained after the {OpenHAB::DSL::Items::Ensure::Ensurable#ensure ensure} + # method, or issued inside an {OpenHAB::DSL.ensure_states ensure_states} block, or after + # {OpenHAB::DSL.ensure_states! ensure_states!} have been called, + # the command will only be sent if the item is not already in the same state. + # + # The similar method `command!`, however, will always send the command regardless of the item's state. + # + # @param [Command, #to_s] command command to send to the item. + # When given a {Command} argument, it will be passed directly. + # Otherwise, the result of `#to_s` will be parsed into a {Command}. + # @param [String, nil] source Optional string to identify what sent the event. + # @return [self, nil] nil when `ensure` is in effect and the item was already in the same state, + # otherwise the item. + # + # @see DSL::Items::TimedCommand#command Timed Command + # @see OpenHAB::DSL.ensure_states ensure_states + # @see OpenHAB::DSL.ensure_states! ensure_states! + # @see DSL::Items::Ensure::Ensurable#ensure ensure + # + # @example Sending a {Command} to an item + # MySwitch.command(ON) # The preferred method is `MySwitch.on` + # Garage_Door.command(DOWN) # The preferred method is `Garage_Door.down` + # SetTemperature.command 20 | "°C" + # + # @example Sending a plain number to a {NumberItem} + # SetTemperature.command(22.5) # if it accepts a DecimalType + # + # @example Sending a string to a dimensioned {NumberItem} + # SetTemperature.command("22.5 °C") # The string will be parsed and converted to a QuantityType + # + def command(command, source: nil) + command = format_command(command) + logger.trace { "Sending Command #{command} to #{name}" } + if source + Events.publisher.post(Events::ItemEventFactory.create_command_event(name, command, source.to_s)) + else + $events.send_command(self, command) + end + Proxy.new(self) + end + alias_method :command!, :command + + # not an alias to allow easier stubbing and overriding + def <<(command) + command(command) + end + + # @!parse alias_method :<<, :command + + # @!method refresh + # Send the {REFRESH} command to the item + # @return [Item] `self` + + # + # Send an update to this item + # + # @param [State, #to_s, nil] state the state to update the item. + # When given a {State} argument, it will be passed directly. + # Otherwise, the result of `#to_s` will be parsed into a {State} first. + # If `nil` is passed, the item will be updated to {NULL}. + # @return [self, nil] nil when `ensure` is in effect and the item was already in the same state, + # otherwise the item. + # + # @example Updating to a {State} + # DoorStatus.update(OPEN) + # InsideTemperature.update 20 | "°C" + # + # @example Updating to {NULL}, the two following are equivalent: + # DoorStatus.update(nil) + # DoorStatus.update(NULL) + # + # @example Updating with a plain number + # PeopleCount.update(5) # A plain NumberItem + # + # @example Updating with a string to a dimensioned {NumberItem} + # InsideTemperature.update("22.5 °C") # The string will be parsed and converted to a QuantityType + # + def update(state) + state = format_update(state) + logger.trace { "Sending Update #{state} to #{name}" } + $events.post_update(self, state) + Proxy.new(self) + end + alias_method :update!, :update + + # @!visibility private + def format_command(command) + command = format_type(command) + return command if command.is_a?(Types::Command) + + command = command.to_s + org.openhab.core.types.TypeParser.parse_command(getAcceptedCommandTypes, command) || command + end + + # @!visibility private + def format_update(state) + state = format_type(state) + return state if state.is_a?(Types::State) + + state = state.to_s + org.openhab.core.types.TypeParser.parse_state(getAcceptedDataTypes, state) || StringType.new(state) + end + + # formats a {Types::Type} to send to the event bus + # @!visibility private + def format_type(type) + # actual Type types can be sent directly without conversion + # make sure to use Type, because this method is used for both + # #update and #command + return type if type.is_a?(Types::Type) + return NULL if type.nil? + + type.to_s + end + # # @!attribute [r] groups # diff --git a/lib/openhab/core/items/persistence.rb b/lib/openhab/core/items/persistence.rb index 97363edd0d..df4559fe96 100644 --- a/lib/openhab/core/items/persistence.rb +++ b/lib/openhab/core/items/persistence.rb @@ -62,7 +62,7 @@ module Items # logger.info("Max power usage today: #{max}, at: #{max.timestamp}) # module Persistence - GenericItem.prepend(self) + Item.prepend(self) # # A wrapper for {org.openhab.core.persistence.HistoricItem HistoricItem} that delegates to its state. diff --git a/lib/openhab/core/items/proxy.rb b/lib/openhab/core/items/proxy.rb index 84ba60a3b9..baafc9d088 100644 --- a/lib/openhab/core/items/proxy.rb +++ b/lib/openhab/core/items/proxy.rb @@ -13,88 +13,42 @@ class Proxy < Delegator # @!parse include Item # @!visibility private - EVENTS = [Events::ItemAddedEvent::TYPE, Events::ItemUpdatedEvent::TYPE, Events::ItemRemovedEvent::TYPE].freeze + EVENTS = [Events::ItemAddedEvent::TYPE, + Events::ItemUpdatedEvent::TYPE, + Events::ItemRemovedEvent::TYPE].freeze # @!visibility private UID_METHOD = :name + # @!visibility private + UID_TYPE = String include Core::Proxy # @return [String] attr_reader :name - # - # Set the proxy item (called by super) - # - def __setobj__(item) - @item = item.is_a?(Item) ? item : nil - @name ||= item.name if item - end - - # - # @return [Item, nil] - # - def __getobj__ - @item - end - # @return [Module] def class - return Item if __getobj__.nil? + target = __getobj__ + return Item if target.nil? - __getobj__.class + target.class end # @return [true, false] def is_a?(klass) - obj = __getobj__ + target = __getobj__ # only claim to be a Delegator if we're backed by an actual item at the moment - klass == Item || obj.is_a?(klass) || klass == Proxy || (!obj.nil? && super) + klass == Item || target.is_a?(klass) || klass == Proxy || (target.nil? && super) end alias_method :kind_of?, :is_a? - # - # Need to check if `self` _or_ the delegate is an instance of the - # given class - # - # So that {#==} can work - # - # @return [true, false] - # - # @!visibility private - def instance_of?(klass) - __getobj__.instance_of?(klass) || super - end - - # - # Check if delegates are equal for comparison - # - # Otherwise items can't be used in Java maps - # - # @return [true, false] - # - # @!visibility private - def ==(other) - return __getobj__ == other.__getobj__ if other.instance_of?(Proxy) - - super - end - - # - # Non equality comparison - # - # @return [true, false] - # - # @!visibility private - def !=(other) - !(self == other) # rubocop:disable Style/InverseMethods - end - # @return [GroupItem::Members] - # @raise [NoMethodError] if item is not a GroupItem, or a dummy. + # @raise [NoMethodError] if item is neither a GroupItem, nor a dummy. def members - return GroupItem::Members.new(self) if __getobj__.nil? + target = __getobj__ + return GroupItem::Members.new(self) if target.nil? - __getobj__.members + target.members end # Several methods can just return nil when it's a dummy item @@ -126,45 +80,30 @@ def #{m}(*args) # def equipment(*args) RUBY end - # @return [String] - def to_s - return name if __getobj__.nil? - - __getobj__.to_s - end - - # @return [String] - def inspect - return super unless __getobj__.nil? - - "#" + # needs to return `true` for dummies for #members, false + # for non-dummies that aren't actually groups + def respond_to?(method, include_private = false) # rubocop:disable Style/OptionalBooleanParameter + target = __getobj__ + if target.nil? + return true if Item.method_defined?(method) + elsif method.to_sym == :members + return target.respond_to?(method) + end + + target.respond_to?(method, include_private) || super end - # - # Supports inspect from IRB when we're a dummy item. - # - # @return [void] - # @!visibility private - def pretty_print(printer) - return super unless __getobj__.nil? + private - printer.text(inspect) - end - - # needs to return `false` if we know we're not a {GroupItem} - def respond_to?(method, *args) - obj = __getobj__ - return obj.respond_to?(method, *args) if method.to_sym == :members && !obj.nil? - - super - end + # ditto + def target_respond_to?(target, method, include_private) + if method.to_sym == :members + return true if target.nil? - # needs to return `false` if we know we're not a {GroupItem} - def respond_to_missing?(method, *args) - obj = __getobj__ - return obj.respond_to_missing?(method, *args) if method.to_sym == :members && !obj.nil? + return target.respond_to?(method, include_private) + end - super + target.respond_to?(method, include_private) end end end diff --git a/lib/openhab/core/items/semantics.rb b/lib/openhab/core/items/semantics.rb index e5c4ccf285..8e72ec6420 100644 --- a/lib/openhab/core/items/semantics.rb +++ b/lib/openhab/core/items/semantics.rb @@ -173,7 +173,7 @@ module Items # For more information, see {add} # module Semantics - GenericItem.include(self) + Item.include(self) GroupItem.extend(Forwardable) GroupItem.def_delegators :members, :equipments, :locations diff --git a/lib/openhab/core/proxy.rb b/lib/openhab/core/proxy.rb index ecd6a54523..4915f585d4 100644 --- a/lib/openhab/core/proxy.rb +++ b/lib/openhab/core/proxy.rb @@ -24,6 +24,14 @@ module Core # @!visibility private # module Proxy + # Error raised when an item is attempted to be accessed, but no longer + # exists + class StaleProxyError < RuntimeError + def initialize(type, uid) + super("#{type} #{uid} does not currently exist") + end + end + # # Registers and listens to openHAB bus events for objects getting # added/updated/removed, and updates references from proxy objects @@ -39,12 +47,15 @@ class EventSubscriber def initialize @proxies = java.util.concurrent.ConcurrentHashMap.new @parent_module = Object.const_get(self.class.name.split("::")[0..-3].join("::"), false) - @object_type = @parent_module.name.split("::").last.downcase[0..-2].to_sym + object_type = @parent_module.name.split("::").last[0...-1] + @object_type = object_type.downcase.to_sym + @type = @parent_module.const_get(object_type, false) @event_types = @parent_module::Proxy::EVENTS @uid_method = @parent_module::Proxy::UID_METHOD + @uid_type = @parent_module::Proxy::UID_TYPE @registry = @parent_module::Provider.registry - @registration = OSGi.register_service(self, "event.topics": "openhab/*") + @registration = OSGi.register_service(self) ScriptHandling.script_unloaded { @registration.unregister } end @@ -67,9 +78,10 @@ def event_filter # def receive(event) uid = event.__send__(@object_type).__send__(@uid_method) - object = @registry.get(uid) unless event.class.simple_name == @event_types.last + uid = @uid_type.new(uid) unless @uid_type == String @proxies.compute_if_present(uid) do |_, proxy_ref| + object = @registry.get(uid) unless event.class.simple_name == @event_types.last proxy = resolve_ref(proxy_ref) next nil unless proxy @@ -84,7 +96,12 @@ def receive(event) def fetch(object) result = nil - @proxies.compute(object.__send__(@uid_method)) do |_k, proxy_ref| + uid = if object.is_a?(@type) + object.__send__(@uid_method) + else + object + end + @proxies.compute(uid) do |_k, proxy_ref| result = resolve_ref(proxy_ref) proxy_ref = nil unless result result ||= yield @@ -119,12 +136,151 @@ def self.included(klass) klass.singleton_class.prepend(ClassMethods) # define a sub-class of EventSubscriber as a child class of the including class klass.const_set(:EventSubscriber, Class.new(EventSubscriber)) + parent_module = Object.const_get(klass.name.split("::")[0..-2].join("::"), false) + object_type = parent_module.name.split("::").last[0...-1].to_sym + klass.const_set(:Type, parent_module.const_get(object_type, false)) end # @!visibility private def to_java __getobj__ end + + KERNEL_CLASS = ::Kernel.instance_method(:class) + KERNEL_IVAR_SET = ::Kernel.instance_method(:instance_variable_set) + private_constant :KERNEL_CLASS, :KERNEL_IVAR_SET + + # @!visibility private + + def initialize(target) + @klass = KERNEL_CLASS.bind(self).call + + if target.is_a?(@klass::Type) + super + KERNEL_IVAR_SET.bind(self).call(:"@#{@klass::UID_METHOD}", target&.__send__(@klass::UID_METHOD)) + else + # dummy items; we were just passed the item name + super(nil) + KERNEL_IVAR_SET.bind(self).call(:"@#{@klass::UID_METHOD}", target) + end + end + + # @!visibility private + def __setobj__(target) + @target = target + end + + # @!visibility private + def __getobj__ + @target + end + + # overwrite these methods to handle "dummy" items: + # if it's a dummy item, and the method exists on Item, + # raise a StaleProxyError when you try to call it. + # if it doesn't exist on item, just let it raise NoMethodError + # as usual + # @!visibility private + ruby2_keywords def method_missing(method, *args, &block) + target = __getobj__ + if target.nil? && @klass::Type.method_defined?(method) + __raise__ StaleProxyError.new(@klass.name.split("::")[-2][0...-1], __send__(@klass::UID_METHOD)) + end + + if target_respond_to?(target, method, false) + target.__send__(method, *args, &block) + elsif ::Kernel.method_defined?(method) || ::Kernel.private_method_defined?(method) + ::Kernel.instance_method(method).bind(self).call(*args, &block) + else + super + end + end + + # @!visibility private + def respond_to_missing?(method, include_private) + target = __getobj__ + return true if target.nil? && @klass::Type.method_defined?(method) + + r = target_respond_to?(target, method, include_private) + if r && include_private && !target_respond_to?(target, method, false) + warn "delegator does not forward private method ##{method}", uplevel: 3 + return false + end + r + end + + # @!visibility private + def respond_to?(method, include_private = false) # rubocop:disable Style/OptionalBooleanParameter + target = __getobj__ + return true if target.nil? && @klass::Type.method_defined?(method) + + target_respond_to?(target, method, include_private) || super + end + + # + # Need to check if `self` _or_ the delegate is an instance of the + # given class + # + # So that {#==} can work + # + # @return [true, false] + # + # @!visibility private + def instance_of?(klass) + __getobj__.instance_of?(klass) || super + end + + # + # Check if delegates are equal for comparison + # + # Otherwise items can't be used in Java maps + # + # @return [true, false] + # + # @!visibility private + def ==(other) + return __getobj__ == other.__getobj__ if other.instance_of?(@klass) + + super + end + + # + # Non equality comparison + # + # @return [true, false] + # + # @!visibility private + def !=(other) + !(self == other) # rubocop:disable Style/InverseMethods + end + + # @return [String] + def to_s + target = __getobj__ + return __send__(@klass::UID_METHOD) if target.nil? + + target.to_s + end + + # @return [String] + def inspect + target = __getobj__ + return target.inspect unless target.nil? + + "#<#{self.class.name} #{__send__(@klass::UID_METHOD)}>" + end + + # + # Supports inspect from IRB when we're a dummy + # + # @return [void] + # @!visibility private + def pretty_print(printer) + target = __getobj__ + return target.pretty_print(printer) unless target.nil? + + printer.text(inspect) + end end end end diff --git a/lib/openhab/core/things/proxy.rb b/lib/openhab/core/things/proxy.rb index 82b246e26c..7d98dc56ec 100644 --- a/lib/openhab/core/things/proxy.rb +++ b/lib/openhab/core/things/proxy.rb @@ -3,6 +3,9 @@ require "delegate" require "forwardable" +require_relative "thing" +require_relative "thing_uid" + module OpenHAB module Core module Things @@ -18,9 +21,14 @@ class Proxy < Delegator Events::ThingRemovedEvent::TYPE].freeze # @!visibility private UID_METHOD = :uid + # @!visibility private + UID_TYPE = ThingUID include Core::Proxy + # @return [ThingUID] + attr_reader :uid + # Returns the list of channels associated with this Thing # # @note This is defined on this class, and not on {Thing}, because @@ -31,57 +39,6 @@ class Proxy < Delegator def channels Thing::ChannelsArray.new(self, super.to_a) end - - # - # Set the proxy item (called by super) - # - def __setobj__(thing) - @uid = thing.uid - end - - # - # Lookup thing from thing registry - # - def __getobj__ - $things.get(@uid) - end - - # - # Need to check if `self` _or_ the delegate is an instance of the - # given class - # - # So that {#==} can work - # - # @return [true, false] - # - # @!visibility private - def instance_of?(klass) - __getobj__.instance_of?(klass) || super - end - - # - # Check if delegates are equal for comparison - # - # Otherwise items can't be used in Java maps - # - # @return [true, false] - # - # @!visibility private - def ==(other) - return __getobj__ == other.__getobj__ if other.instance_of?(Proxy) - - super - end - - # - # Non equality comparison - # - # @return [true, false] - # - # @!visibility private - def !=(other) - !(self == other) # rubocop:disable Style/InverseMethods - end end end end diff --git a/lib/openhab/dsl/items/ensure.rb b/lib/openhab/dsl/items/ensure.rb index 2df42a7d5e..37f943e64b 100644 --- a/lib/openhab/dsl/items/ensure.rb +++ b/lib/openhab/dsl/items/ensure.rb @@ -29,7 +29,7 @@ def ensure module Item include Ensurable - Core::Items::GenericItem.prepend(self) + Core::Items::Item.prepend(self) # If `ensure_states` is active (by block or chained method), then # check if this item is in the command's state before actually diff --git a/lib/openhab/dsl/items/timed_command.rb b/lib/openhab/dsl/items/timed_command.rb index 20c7b0f3dd..6507ba2abf 100644 --- a/lib/openhab/dsl/items/timed_command.rb +++ b/lib/openhab/dsl/items/timed_command.rb @@ -100,7 +100,7 @@ class << self attr_reader :timed_commands end - Core::Items::GenericItem.prepend(self) + Core::Items::Item.prepend(self) Core::Items::GroupItem.prepend(self) # diff --git a/spec/openhab/core/items/proxy_spec.rb b/spec/openhab/core/items/proxy_spec.rb index 3790bb89b1..dafaa06cc9 100644 --- a/spec/openhab/core/items/proxy_spec.rb +++ b/spec/openhab/core/items/proxy_spec.rb @@ -80,10 +80,10 @@ # rubocop:enable Lint/UselessAssignment context "without a backing item" do - let(:item) { described_class.new(:MySwitch) } + let(:item) { described_class.new("MySwitch") } it "supports #name" do - expect(item.name).to eq "MySwitch" + expect(item.name).to eql "MySwitch" end it "pretends to be an item" do @@ -95,8 +95,10 @@ end it "doesn't respond to other Item methods" do - expect(item).not_to respond_to(:command) - expect { item.command }.to raise_error(NoMethodError) + expect(item).to respond_to(:command) + expect(item).not_to respond_to(:bogus) + expect { item.command }.to raise_error(OpenHAB::Core::Items::Proxy::StaleProxyError) + expect { item.bogus }.to raise_error(NoMethodError) end it "disappears when calling semantic predicates on an array" do