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