mqtt-homie-homeassistant 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []