mqtt-homie-homeassistant 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ea47bfb9fc5d373702f2edf0bd746412d12cd884132d87f098fdd29a5b552b51
4
+ data.tar.gz: a229e5225e2a77daadb9caca7edb547da464211dc0f66aa3840429015970e39a
5
+ SHA512:
6
+ metadata.gz: b4df1f4dff3da9e5becc45c539720383b7f0f9fed40125412a03b668b5048fff2a9206a01f831f140e986c0a5844d4ae016b5586228ae0d4b1a1710dd49097b9
7
+ data.tar.gz: 73d1824ec2f29d95414368a1fbbc360c0a250ce54e0a0fecfc93c2c4dd1d2086fcca7f71c2c7b3e33e545c331d69d31da237ff1a8ec15b3f0b0d548218175e5b
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Homie
5
+ module HomeAssistant
6
+ module Device
7
+ def self.included(klass)
8
+ super
9
+ klass.attr_accessor :home_assistant_device, :home_assistant_discovery_prefix
10
+ end
11
+
12
+ # @!visibility private
13
+ def base_hass_config(config)
14
+ config[:availability] = [{
15
+ topic: "#{topic}/$state",
16
+ payload_available: "ready",
17
+ payload_not_available: "lost"
18
+ }]
19
+ config[:device] ||= {}
20
+ config[:device][:name] ||= name
21
+ config[:device][:identifiers] ||= id
22
+ config[:device][:sw_version] ||= MQTT::Homie::Device::VERSION
23
+ config[:node_id] = id
24
+ config[:qos] = 1
25
+ end
26
+ end
27
+ end
28
+ Device.include(HomeAssistant::Device)
29
+ end
30
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Homie
5
+ module HomeAssistant
6
+ module Node
7
+ def hass_climate(action_property: nil,
8
+ current_humidity_property: nil,
9
+ current_temperature_property: nil,
10
+ fan_mode_property: nil,
11
+ mode_property: nil,
12
+ power_property: nil,
13
+ preset_mode_property: nil,
14
+ swing_mode_property: nil,
15
+ target_humidity_property: nil,
16
+ temperature_property: nil,
17
+ temperature_high_property: nil,
18
+ temperature_low_property: nil,
19
+ **kwargs)
20
+ if power_property && power_property.datatype != :boolean
21
+ raise ArgumentError, "Power property must be a boolean"
22
+ end
23
+
24
+ temperature_property = resolve_property(temperature_property)
25
+ temperature_high_property = resolve_property(temperature_high_property)
26
+ temperature_low_property = resolve_property(temperature_low_property)
27
+ temp_properties = [
28
+ temperature_property,
29
+ temperature_high_property,
30
+ temperature_low_property
31
+ ].compact
32
+ unless (temp_ranges = temp_properties.map(&:range).compact).empty?
33
+ min = temp_ranges.map(&:begin).min
34
+ max = temp_ranges.map(&:end).max
35
+ kwargs[:temp_range] = min..max
36
+ end
37
+ kwargs[:temperature_unit] = temp_properties.map(&:unit).compact.first
38
+ if power_property
39
+ kwargs[:payload_off] = "false"
40
+ kwargs[:payload_on] = "true"
41
+ end
42
+
43
+ hass_property(kwargs, action_property, :action, read_only: true)
44
+ hass_property(kwargs, current_humidity_property, :current_humidity, read_only: true)
45
+ hass_property(kwargs, current_temperature_property, :current_temperature, read_only: true)
46
+ hass_enum(kwargs, fan_mode_property, :fan_mode)
47
+ hass_enum(kwargs, mode_property, :mode, MQTT::HomeAssistant::DEFAULTS[:climate][:modes])
48
+ hass_property(kwargs, power_property, :power)
49
+ hass_enum(kwargs, preset_mode_property, :preset_mode)
50
+ hass_enum(kwargs, swing_mode_property, :swing_mode)
51
+ hass_property(kwargs, target_humidity_property, :target_humidity)
52
+ hass_property(kwargs, temperature_property, :temperature)
53
+ hass_property(kwargs, temperature_high_property, :temperature_high)
54
+ hass_property(kwargs, temperature_low_property, :temperature_low)
55
+ publish_hass_component(platform: :climate, **kwargs)
56
+ end
57
+
58
+ def hass_fan(property,
59
+ oscillation_property: nil,
60
+ preset_mode_property: nil,
61
+ **kwargs)
62
+ hass_property(kwargs, property)
63
+ hass_property(kwargs, oscillation_property, :oscillation)
64
+ hass_enum(kwargs, preset_mode_property, :preset_mode)
65
+ publish_hass_component(platform: :fan, **kwargs)
66
+ end
67
+
68
+ def hass_humidifier(property,
69
+ target_humidity_property: nil,
70
+ mode_property: nil,
71
+ **kwargs)
72
+ hass_property(kwargs, property)
73
+ hass_property(kwargs, target_humidity_property, :target_humidity)
74
+ hass_property(kwargs, mode_property, :mode)
75
+ publish_hass_component(platform: :humidifier,
76
+ payload_off: "false",
77
+ payload_on: "true",
78
+ **kwargs)
79
+ end
80
+
81
+ def hass_light(property = nil,
82
+ brightness_property: nil,
83
+ color_mode_property: nil,
84
+ color_temp_property: nil,
85
+ effect_property: nil,
86
+ hs_property: nil,
87
+ rgb_property: nil,
88
+ white_property: nil,
89
+ xy_property: nil,
90
+ **kwargs)
91
+ # automatically infer a brightness-only light and adjust config
92
+ if brightness_property && property.nil?
93
+ property = brightness_property
94
+ kwargs[:on_command_type] = :brightness
95
+ end
96
+ case property.datatype
97
+ when :boolean
98
+ kwargs[:payload_off] = "false"
99
+ kwargs[:payload_on] = "true"
100
+ when :integer
101
+ kwargs[:payload_off] = "0"
102
+ when :float
103
+ kwargs[:payload_off] = "0.0"
104
+ end
105
+ kwargs[:brightness_scale] = brightness_property.range.end if brightness_property&.range
106
+ kwargs[:effect_list] = effect_property.range if effect_property&.datatype == :enum
107
+ kwargs[:mireds_range] = color_temp_property.range if color_temp_property.unit == "mired"
108
+ kwargs[:white_scale] = white_property.range.end if white_property&.range
109
+
110
+ hass_property(kwargs, property)
111
+ hass_property(kwargs, brightness_property, :brightness)
112
+ hass_property(kwargs, color_mode_property, :color_mode)
113
+ hass_property(kwargs, color_temp_property, :color_temp)
114
+ hass_property(kwargs, effect_property, :effect)
115
+ hass_property(kwargs, hs_property, :hs)
116
+ hass_property(kwargs, rgb_property, :rgb)
117
+ hass_property(kwargs, white_property, :white)
118
+ hass_property(kwargs, xy_property, :xy)
119
+ publish_hass_component(platform: :light, **kwargs)
120
+ end
121
+
122
+ def hass_water_heater(
123
+ current_temperature_property: nil,
124
+ mode_property: nil,
125
+ power_property: nil,
126
+ temperature_property: nil,
127
+ **kwargs)
128
+ temperature_property = resolve_property(temperature_property)
129
+ current_temperature_property = resolve_property(current_temperature_property)
130
+ temp_properties = [
131
+ temperature_property,
132
+ current_temperature_property
133
+ ].compact
134
+ kwargs[:range] = temperature_property&.range
135
+ kwargs[:temperature_unit] = temp_properties.map(&:unit).compact.first
136
+ if power_property
137
+ kwargs[:payload_off] = "false"
138
+ kwargs[:payload_on] = "true"
139
+ end
140
+ hass_property(kwargs, current_temperature_property, :current_temperature, read_only: true)
141
+ hass_enum(kwargs, mode_property, :mode, MQTT::HomeAssistant::DEFAULTS[:water_heater][:modes])
142
+ hass_property(kwargs, power_property, :power)
143
+ hass_property(kwargs, temperature_property, :temperature)
144
+ publish_hass_component(platform: :water_heater, **kwargs)
145
+ end
146
+
147
+ def publish
148
+ super.tap do
149
+ @pending_hass_registrations&.each do |(object_id, kwargs)|
150
+ device.mqtt.publish_hass_component(object_id, **kwargs)
151
+ end
152
+ @pending_hass_registrations = nil
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def resolve_property(property)
159
+ return if property.nil?
160
+
161
+ orig_property = property
162
+ property = self[property] if property.is_a?(String)
163
+ raise ArgumentError, "Unknown property #{orig_property}" if property.nil?
164
+
165
+ property
166
+ end
167
+
168
+ def hass_property(config, property, prefix = nil, read_only: false, templates: {})
169
+ resolve_property(property)&.hass_property(config, prefix, read_only: read_only, templates: templates)
170
+ end
171
+
172
+ def hass_enum(config, property, prefix = nil, valid_set = nil)
173
+ return if property.nil?
174
+
175
+ property = resolve_property(property)
176
+ hass_property(config, property, prefix)
177
+
178
+ return unless property.datatype == :enum
179
+
180
+ values = property.range
181
+ values &= valid_set if valid_set
182
+ config[:"#{prefix}s"] = values
183
+ end
184
+
185
+ def publish_hass_component(device: nil, discovery_prefix: nil, object_id: nil, **kwargs)
186
+ discovery_prefix ||= self.device.home_assistant_discovery_prefix
187
+ device = self.device.home_assistant_device.merge(device || {}) if self.device.home_assistant_device
188
+
189
+ object_id ||= id
190
+ kwargs[:name] ||= name
191
+ kwargs[:device] = device
192
+ kwargs[:discovery_prefix] ||= discovery_prefix
193
+ kwargs[:unique_id] ||= "#{self.device.id}_#{object_id}"
194
+ self.device.base_hass_config(kwargs)
195
+ if published?
196
+ self.device.mqtt.publish_hass_component(object_id, **kwargs)
197
+ else
198
+ pending_hass_registrations << [object_id, kwargs]
199
+ end
200
+ end
201
+
202
+ def pending_hass_registrations
203
+ @pending_hass_registrations ||= []
204
+ end
205
+ end
206
+ end
207
+ Node.prepend(HomeAssistant::Node)
208
+ end
209
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Homie
5
+ module HomeAssistant
6
+ module Property
7
+ def initialize(*args, hass: nil, **kwargs)
8
+ super(*args, **kwargs)
9
+
10
+ return unless hass
11
+
12
+ case hass
13
+ when Symbol
14
+ public_send("hass_#{hass}")
15
+ when Hash
16
+ raise ArgumentError, "hass must only contain one item" unless hass.length == 1
17
+
18
+ public_send("hass_#{hass.first.first}", **hass.first.last)
19
+ else
20
+ raise ArgumentError, "hass must be a Symbol or a Hash of HASS device type to additional HASS options"
21
+ end
22
+ end
23
+
24
+ def hass_binary_sensor(**kwargs)
25
+ raise ArgumentError, "Property must be a boolean" unless datatype == :boolean
26
+ raise ArgumentError, "Property must not be settable" if settable?
27
+
28
+ hass_property(kwargs)
29
+ publish_hass_component(platform: :binary_sensor,
30
+ payload_off: "false",
31
+ payload_on: "true",
32
+ **kwargs)
33
+ end
34
+
35
+ def hass_fan(**kwargs)
36
+ raise ArgumentError, "Property must be a boolean" unless datatype == :boolean
37
+ raise ArgumentError, "Property must be settable" unless settable?
38
+
39
+ hass_property(kwargs)
40
+ publish_hass_component(platform: :fan,
41
+ payload_off: "false",
42
+ payload_on: "true",
43
+ **kwargs)
44
+ end
45
+
46
+ def hass_light(**kwargs)
47
+ case datatype
48
+ when :boolean
49
+ kwargs[:payload_off] = "false"
50
+ kwargs[:payload_on] = "true"
51
+ when :integer
52
+ kwargs[:payload_off] = "0"
53
+ when :float
54
+ kwargs[:payload_off] = "0.0"
55
+ end
56
+
57
+ hass_property(kwargs)
58
+ publish_hass_component(platform: :light, **kwargs)
59
+ end
60
+
61
+ def hass_number(**kwargs)
62
+ raise ArgumentError, "Property must be an integer or a float" unless %i[integer float].include?(datatype)
63
+
64
+ hass_property(kwargs)
65
+ kwargs[:range] = range if range
66
+ kwargs[:unit_of_measurement] = unit if unit
67
+
68
+ publish_hass_component(platform: :number, **kwargs)
69
+ end
70
+
71
+ def hass_scene(**kwargs)
72
+ unless datatype == :enum && range.length == 1
73
+ raise ArgumentError, "Property must be an enum with a single value"
74
+ end
75
+ raise ArgumentError, "Property must be settable" unless settable?
76
+
77
+ publish_hass_component(platform: :scene,
78
+ command_topic: "#{topic}/set",
79
+ payload_on: range.first,
80
+ **kwargs)
81
+ end
82
+
83
+ def hass_select(**kwargs)
84
+ raise ArgumentError, "Property must be an enum" unless datatype == :enum
85
+ raise ArgumentError, "Property must be settable" unless settable?
86
+
87
+ hass_property(kwargs)
88
+ publish_hass_component(platform: :select, options: range, **kwargs)
89
+ end
90
+
91
+ def hass_sensor(**kwargs)
92
+ if datatype == :enum
93
+ kwargs[:device_class] = :enum
94
+ kwargs[:options] = range
95
+ end
96
+
97
+ publish_hass_component(platform: :sensor,
98
+ state_topic: topic,
99
+ **kwargs)
100
+ end
101
+
102
+ def hass_switch(**kwargs)
103
+ raise ArgumentError, "Property must be a boolean" unless datatype == :boolean
104
+
105
+ hass_property(kwargs)
106
+ publish_hass_component(platform: :switch,
107
+ payload_off: "false",
108
+ payload_on: "true",
109
+ **kwargs)
110
+ end
111
+
112
+ def publish
113
+ super.tap do
114
+ @pending_hass_registrations&.each do |(object_id, kwargs)|
115
+ device.mqtt.publish_hass_component(object_id, **kwargs)
116
+ end
117
+ @pending_hass_registrations = nil
118
+ end
119
+ end
120
+
121
+ # @!visibility private
122
+ def hass_property(config, prefix = nil, read_only: false, templates: {})
123
+ prefix = "#{prefix}_" if prefix
124
+ state_prefix = "state_" unless read_only
125
+ config[:"#{prefix}#{state_prefix}topic"] = topic if retained?
126
+ if !read_only && settable?
127
+ config[:"#{prefix}command_topic"] = "#{topic}/set"
128
+ config[:"#{prefix}command_template"] = "{{ value | round(0) }}" if datatype == :integer
129
+ end
130
+ config.merge!(templates.slice(:"#{prefix}template", :"#{prefix}command_template"))
131
+ end
132
+
133
+ # @!visibility private
134
+ def hass_enum(config, prefix = nil, valid_set = nil)
135
+ prefix = "#{prefix}_" if prefix
136
+
137
+ return unless datatype == :enum
138
+
139
+ modes = range
140
+ modes &= valid_set if valid_set
141
+ config[:"#{prefix}modes"] = modes
142
+ end
143
+
144
+ private
145
+
146
+ def publish_hass_component(device: nil, discovery_prefix: nil, object_id: nil, **kwargs)
147
+ discovery_prefix ||= self.device.home_assistant_discovery_prefix
148
+ device = self.device.home_assistant_device.merge(device || {}) if self.device.home_assistant_device
149
+
150
+ object_id ||= "#{node.id}_#{id}"
151
+ kwargs[:name] ||= "#{node.name} #{name}"
152
+ kwargs[:device] = device
153
+ kwargs[:discovery_prefix] ||= discovery_prefix
154
+ kwargs[:unique_id] ||= "#{self.device.id}_#{object_id}"
155
+ self.device.base_hass_config(kwargs)
156
+ if published?
157
+ self.device.mqtt.publish_hass_component(object_id, **kwargs)
158
+ else
159
+ pending_hass_registrations << [object_id, kwargs]
160
+ end
161
+ end
162
+
163
+ def pending_hass_registrations
164
+ @pending_hass_registrations ||= []
165
+ end
166
+ end
167
+ end
168
+ Property.prepend(HomeAssistant::Property)
169
+ end
170
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Homie
5
+ module HomeAssistant
6
+ VERSION = "1.0.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mqtt/home_assistant"
4
+ require "mqtt/homie"
5
+ require "mqtt/homie/home_assistant/device"
6
+ require "mqtt/homie/home_assistant/node"
7
+ require "mqtt/homie/home_assistant/property"
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mqtt-homie-homeassistant
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cody Cutrer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: homie-mqtt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mqtt-homeassistant
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ description:
56
+ email: cody@cutrer.com'
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/mqtt-homie-homeassistant.rb
62
+ - lib/mqtt/homie/home_assistant/device.rb
63
+ - lib/mqtt/homie/home_assistant/node.rb
64
+ - lib/mqtt/homie/home_assistant/property.rb
65
+ - lib/mqtt/homie/home_assistant/version.rb
66
+ homepage: https://github.com/ccutrer/ruby-mqtt-homie-homeassistant
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ rubygems_mfa_required: 'true'
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '2.5'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.5.11
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Library for publishing device auto-discovery configuration for Homie devices
90
+ to Home Assistant as well.
91
+ test_files: []