class MQTT::Homie::Property
def device
def device node.device end
def format=(format)
def format=(format) return if format == @format @format = format return unless @published device.init do mqtt.publish("#{topic}/$format", format.to_s, retain: true, qos: 1) end end
def full_name
def full_name "#{node.full_name} #{name}" end
def initialize(node, id, name, datatype, value = nil, format: nil, retained: true, unit: nil, &block)
def initialize(node, id, name, datatype, value = nil, format: nil, retained: true, unit: nil, &block) raise ArgumentError, "Invalid Homie datatype" unless %i[string integer float boolean enum color datetime duration].include?(datatype) raise ArgumentError, "retained must be boolean" unless [true, false].include?(retained) format = format.join(",") if format.is_a?(Array) && datatype == :enum if %i[integer float].include?(datatype) && format.is_a?(Range) raise ArgumentError "only inclusive ranges are supported" if format.exclude_end? format = "#{format.begin}:#{format.end}" end raise ArgumentError, "format must be nil or a string" unless format.nil? || format.is_a?(String) raise ArgumentError, "unit must be nil or a string" unless unit.nil? || unit.is_a?(String) raise ArgumentError, "format is required for enums" if datatype == :enum && format.nil? raise ArgumentError, "format is required for colors" if datatype == :color && format.nil? if datatype == :color && !%w[rgb hsv].include?(format.to_s) raise ArgumentError, "format must be either rgb or hsv for colors" end if !value.nil? && !retained raise ArgumentError, "an initial value cannot be provided for a non-retained property" end super(id, name) @node = node @datatype = datatype @format = format @retained = retained @unit = unit @value = value @published = false @block = block end
def inspect
def inspect result = +"#<MQTT::Homie::Property #{topic} name=#{full_name.inspect}, datatype=#{datatype.inspect}" result << ", format=#{format.inspect}" if format result << ", unit=#{unit.inspect}" if unit result << ", settable=true" if settable? result << if retained? ", value=#{value.inspect}" else ", retained=false" end result << ">" result.freeze end
def mqtt
def mqtt node.mqtt end
def publish
def publish return if @published mqtt.batch_publish do mqtt.publish("#{topic}/$name", name, retain: true, qos: 1) mqtt.publish("#{topic}/$datatype", datatype.to_s, retain: true, qos: 1) mqtt.publish("#{topic}/$format", format, retain: true, qos: 1) if format mqtt.publish("#{topic}/$settable", "true", retain: true, qos: 1) if settable? mqtt.publish("#{topic}/$retained", "false", retain: true, qos: 1) unless retained? mqtt.publish("#{topic}/$unit", unit, retain: true, qos: 1) if unit publish_value unless value.nil? subscribe end @published = true end
def publish_value
def publish_value serialized = value serialized = serialized&.iso8601 if %i[datetime duration].include?(datatype) serialized = serialized.to_s node.device.logger&.debug("publishing #{serialized.inspect} to #{topic}") mqtt.publish(topic, serialized, retain: retained?, qos: 1) end
def range
def range return nil unless format case datatype when :enum then format.split(",") when :integer then Range.new(*format.split(":").map(&:to_i)) when :float then Range.new(*format.split(":").map(&:to_f)) else; raise MethodNotImplemented end end
def retained?
def retained? @retained end
def set(value)
def set(value) case datatype when :boolean return unless %w[true false].include?(value) value = value == "true" when :integer return unless /^-?\d+$/.match?(value) value = value.to_i return if format && !range.include?(value) when :float return unless /^-?(?:\d+|\d+\.|\.\d+|\d+\.\d+)(?:[eE]-?\d+)?$/.match?(value) value = value.to_f return if format && !range.include?(value) when :enum return unless range.include?(value) when :color return unless /^\d{1,3},\d{1,3},\d{1,3}$/.match?(value) value = value.split(",").map(&:to_i) case format when "rgb" return if value.max > 255 when "hsv" return if value.first > 360 || value[1..2].max > 100 end when :datetime begin value = Time.parse(value) rescue ArgumentError return end when :duration begin value = ActiveSupport::Duration.parse(value) rescue ActiveSupport::Duration::ISO8601Parser::ParsingError return end end @block.arity == 2 ? @block.call(value, self) : @block.call(value) end
def settable?
def settable? !!@block end
def subscribe
def subscribe mqtt.subscribe("#{topic}/set") if settable? end
def topic
def topic "#{node.topic}/#{id}" end
def unit=(unit)
def unit=(unit) return if unit == @unit @unit = unit return unless @published device.init do mqtt.publish("#{topic}/$unit", unit.to_s, retain: true, qos: 1) end end
def unpublish
def unpublish return unless @published @published = false mqtt.publish("#{topic}/$name", retain: true, qos: 0) mqtt.publish("#{topic}/$datatype", retain: true, qos: 0) mqtt.publish("#{topic}/$format", retain: true, qos: 0) if format mqtt.publish("#{topic}/$settable", retain: true, qos: 0) if settable? mqtt.publish("#{topic}/$retained", retain: true, qos: 0) unless retained? mqtt.publish("#{topic}/$unit", retain: true, qos: 0) if unit mqtt.unsubscribe("#{topic}/set") if settable? mqtt.publish(topic, retain: retained?, qos: 0) if !value.nil? && retained? end
def value=(value)
def value=(value) return if @value == value @value = value if retained? publish_value if @published end