intesisbox 0.0.2
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 +7 -0
- data/bin/intesisbox_mqtt_bridge +201 -0
- data/lib/intesis_box.rb +2 -0
- data/lib/intesis_box/client.rb +86 -0
- data/lib/intesis_box/discovery.rb +69 -0
- data/lib/intesis_box/version.rb +3 -0
- data/lib/intesisbox.rb +1 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1fd75a5326195c3ac8d0b6d7dc51730e23d6f3ce6c26a09b7e7b2bb2dad45369
|
4
|
+
data.tar.gz: 3c130bac7dbbca03827c9e8835daf58cb8fb54a5236ee0f6d7473af05060eb96
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 15aee2ce1fb63c3dd5ef737eb6bb270fc6388f275c1cd28c79ded5a3e0678d2aa62490113ef98ccf3456cd7e8d51659317ec3f10e7c9301434585d04901600cc
|
7
|
+
data.tar.gz: 7104c211dbec8652c6b84216f509c3dbdca9695648e671ca2c67a3e0e8d4909f384e202ceb632caa1a5f737a989b531a3ee46143da835343724043c0c5ced49b
|
@@ -0,0 +1,201 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'intesisbox'
|
4
|
+
require 'homie-mqtt'
|
5
|
+
|
6
|
+
class MQTTBridge
|
7
|
+
def initialize(mqtt_uri, root_topic: nil)
|
8
|
+
@base_topic = "homie/intesisbox"
|
9
|
+
|
10
|
+
bridge, discovery_property, units_property = nil
|
11
|
+
@device = MQTT::Homie::Device.new("intesisbox", "IntesisBox", root_topic: root_topic, mqtt: mqtt_uri) do |topic, value|
|
12
|
+
if topic == discovery_property.topic
|
13
|
+
@device.mqtt.unsubscribe(topic)
|
14
|
+
discovery_property.set(value) unless @got_discovery
|
15
|
+
@got_discovery = true
|
16
|
+
elsif topic == units_property.topic
|
17
|
+
@device.mqtt.unsubscribe(topic)
|
18
|
+
units_property.set(value) unless @got_units
|
19
|
+
@got_units = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@wmps = {}
|
24
|
+
@discovery = 300
|
25
|
+
@units = :C
|
26
|
+
|
27
|
+
bridge = nil
|
28
|
+
@device.node("bridge", "Bridge", "Bridge") do |node|
|
29
|
+
(bridge = node).property("discovery", "Auto-Discovery Interval", :integer, format: "0:86400", unit: "s") do |prop, value|
|
30
|
+
next unless value =~ /^\d+$/
|
31
|
+
value = value.to_i
|
32
|
+
next if value > 86400
|
33
|
+
old_value = @discovery
|
34
|
+
prop.value = value
|
35
|
+
@discovery = value
|
36
|
+
if value < old_value || old_value == 0
|
37
|
+
@discovery_thread.kill
|
38
|
+
start_discovery_thread
|
39
|
+
end
|
40
|
+
end.
|
41
|
+
property("units", "Units", :enum, format: "C,F") do |prop, value|
|
42
|
+
next unless %w{C F}.include?(value)
|
43
|
+
if prop.value != value
|
44
|
+
prop.value = value
|
45
|
+
@units = value
|
46
|
+
next if @wmps.empty?
|
47
|
+
@device.init do
|
48
|
+
@wmps.each do |(mac, wmp)|
|
49
|
+
node = @device.nodes[mac]
|
50
|
+
setptemp = node.properties['setptemp']
|
51
|
+
setptemp.value = convert_units(wmp.setptemp)
|
52
|
+
setptemp.unit = "º#{@units}"
|
53
|
+
setptemp.format = wmp.limits[:setptemp].map { |lim| convert_units(lim) }.join(":")
|
54
|
+
if wmp.ambtemp
|
55
|
+
ambtemp = node.properties['ambtemp']
|
56
|
+
ambtemp.value = convert_units(wmp.ambtemp)
|
57
|
+
ambtemp.unit = "º#{@units}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
discovery_property = bridge.properties['discovery']
|
65
|
+
units_property = bridge.properties['units']
|
66
|
+
@device.publish
|
67
|
+
# use MQTT itself as our state store
|
68
|
+
@device.mqtt.subscribe("#{discovery_property.topic}")
|
69
|
+
@device.mqtt.subscribe("#{units_property.topic}")
|
70
|
+
|
71
|
+
setup_discovery
|
72
|
+
start_discovery_thread
|
73
|
+
|
74
|
+
@device.join
|
75
|
+
end
|
76
|
+
|
77
|
+
def start_discovery_thread
|
78
|
+
@discovery_thread = Thread.new do
|
79
|
+
@discovery_object.discover
|
80
|
+
loop do
|
81
|
+
break if @discovery == 0
|
82
|
+
sleep(@discovery)
|
83
|
+
@discovery_object.discover
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def setup_discovery
|
89
|
+
@discovery_object = IntesisBox::Discovery.new do |details|
|
90
|
+
# force a reconnect to WMPs that have moved
|
91
|
+
mac = MQTT::Homie.escape_id(details[:mac])
|
92
|
+
known_wmp = @wmps[mac]
|
93
|
+
if known_wmp && known_wmp.ip != details[:ip]
|
94
|
+
@wmps.delete(mac)
|
95
|
+
end
|
96
|
+
|
97
|
+
next true if @wmps.key?(mac)
|
98
|
+
|
99
|
+
wmp = IntesisBox::Client.new(details[:ip])
|
100
|
+
|
101
|
+
loop do
|
102
|
+
break if wmp.mac
|
103
|
+
unless wmp.poll(1)
|
104
|
+
puts "unable to talk to #{wmp.mac}"
|
105
|
+
break
|
106
|
+
end
|
107
|
+
end
|
108
|
+
next true unless wmp.mac
|
109
|
+
|
110
|
+
puts "Found new WMP #{wmp.mac} (#{wmp.devicename})"
|
111
|
+
@wmps[MQTT::Homie.escape_id(wmp.mac)] = wmp
|
112
|
+
node = publish_wmp(wmp)
|
113
|
+
|
114
|
+
Thread.new do
|
115
|
+
iter = 0
|
116
|
+
loop do
|
117
|
+
next wmp.ping unless wmp.poll
|
118
|
+
|
119
|
+
node.name = wmp.devicename
|
120
|
+
node.properties['ip'].value = wmp.ip
|
121
|
+
node.properties['onoff'].value = wmp.onoff
|
122
|
+
node.properties['mode'].value = wmp.mode if wmp.limits[:mode]&.length.to_i > 0
|
123
|
+
node.properties['fansp'].value = wmp.fansp if wmp.limits[:fansp]&.length.to_i > 0
|
124
|
+
node.properties['vaneud'].value = wmp.vaneud if wmp.limits[:vaneud]&.length.to_i > 0
|
125
|
+
node.properties['vanelr'].value = wmp.vanelr if wmp.limits[:vanelr]&.length.to_i > 0
|
126
|
+
node.properties['setptemp'].value = convert_units(wmp.setptemp) if wmp.setptemp
|
127
|
+
node.properties['ambtemp'].value = convert_units(wmp.ambtemp) if wmp.ambtemp
|
128
|
+
node.properties['errstatus'].value = wmp.errstatus
|
129
|
+
node.properties['errcode'].value = wmp.errcode
|
130
|
+
node.properties['devicename'].value = wmp.devicename
|
131
|
+
end
|
132
|
+
rescue => e
|
133
|
+
puts "Lost connection to #{wmp.mac} (#{wmp.devicename}): #{e}"
|
134
|
+
remove_wmp(wmp)
|
135
|
+
end
|
136
|
+
true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def publish_wmp(wmp)
|
141
|
+
generic_property = ->(prop, value) do
|
142
|
+
wmp = @wmps[prop.node.id]
|
143
|
+
wmp.send("#{prop.id}=", value)
|
144
|
+
end
|
145
|
+
|
146
|
+
node = nil
|
147
|
+
@device.node(MQTT::Homie.escape_id(wmp.mac), wmp.devicename, wmp.model) do |n|
|
148
|
+
node = n
|
149
|
+
node.property("devicename", "Device Name", :string, &generic_property).
|
150
|
+
property("ip", "IP Address", "string").
|
151
|
+
property("onoff", "AC unit On or Off", "boolean") do |prop, value|
|
152
|
+
wmp = @wmps[prop.node.id]
|
153
|
+
wmp.onoff = value == 'true'
|
154
|
+
end
|
155
|
+
|
156
|
+
if wmp.limits[:mode]&.length.to_i > 0
|
157
|
+
node.property("mode", "Mode (heat, cool, fan, dry or auto)", :enum, format: wmp.limits[:mode].join(","), &generic_property)
|
158
|
+
end
|
159
|
+
|
160
|
+
if wmp.limits[:fansp]&.length.to_i > 0
|
161
|
+
node.property("fansp", "Fan speed", :enum, format: wmp.limits[:fansp].join(","), &generic_property)
|
162
|
+
end
|
163
|
+
|
164
|
+
if wmp.limits[:vaneud]&.length.to_i > 0
|
165
|
+
node.property("vaneud", "Up/Down vane position", :enum, format: wmp.limits[:vaneud].join(","), &generic_property)
|
166
|
+
end
|
167
|
+
|
168
|
+
if wmp.limits[:vanelr]&.length.to_i > 0
|
169
|
+
node.property("vanelr", "Left/Right vane position", :enum, format: wmp.limits[:vanelr].join(","), &generic_property)
|
170
|
+
end
|
171
|
+
|
172
|
+
node.property("setptemp", "Set point temperature", :float, unit: "º#{@units}", format: wmp.limits[:setptemp].map { |lim| convert_units(lim) }.join(":")) do |prop, value|
|
173
|
+
wmp = @mps[prop.node.id]
|
174
|
+
wmp.setptemp = convert_units_set(value.to_f)
|
175
|
+
end.
|
176
|
+
property("ambtemp", "Ambient temperature", :float, unit: "º#{@units}").
|
177
|
+
property("errstatus", "Shows if any error occurs", :string).
|
178
|
+
property("errcode", "Error code", :integer)
|
179
|
+
end
|
180
|
+
node
|
181
|
+
end
|
182
|
+
|
183
|
+
def remove_wmp(wmp)
|
184
|
+
mac = MQTT::Homie.escape_id(wmp.mac)
|
185
|
+
@wmps.delete(mac)
|
186
|
+
@device.remove_node(mac)
|
187
|
+
end
|
188
|
+
|
189
|
+
def convert_units(value)
|
190
|
+
return value if @units == :C
|
191
|
+
value * 9/5.0 + 32
|
192
|
+
end
|
193
|
+
|
194
|
+
def convert_units_set(value)
|
195
|
+
return value if @units == :C
|
196
|
+
(value - 32) * 5/9.0
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
mqtt_uri = ARGV[0]
|
201
|
+
MQTTBridge.new(mqtt_uri)
|
data/lib/intesis_box.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module IntesisBox
|
4
|
+
class Client
|
5
|
+
attr_reader :ip, :model, :mac, :version, :rssi
|
6
|
+
attr_reader :limits
|
7
|
+
attr_reader :devicename
|
8
|
+
attr_reader :onoff, :mode, :fansp, :vaneud, :vanelr, :setptemp, :ambtemp, :errstatus, :errcode
|
9
|
+
|
10
|
+
def initialize(ip, port = 3310)
|
11
|
+
@limits = {}
|
12
|
+
@ip = ip
|
13
|
+
@io = TCPSocket.new(ip, port)
|
14
|
+
|
15
|
+
@io.puts("LIMITS:*")
|
16
|
+
poll(1)
|
17
|
+
@io.puts("CFG:DEVICENAME")
|
18
|
+
poll(1)
|
19
|
+
@io.puts("GET,1:*")
|
20
|
+
poll(1)
|
21
|
+
# this is purposely last, since mac is what we check for it being ready
|
22
|
+
@io.puts("ID")
|
23
|
+
poll(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
def poll(timeout = 30)
|
27
|
+
return false if @io.wait_readable(timeout).nil?
|
28
|
+
|
29
|
+
loop do
|
30
|
+
line = @io.readline.strip
|
31
|
+
cmd, args = line.split(':', 2)
|
32
|
+
case cmd
|
33
|
+
when "ID"
|
34
|
+
@model, @mac, _ip, _protocol, @version, @rssi = args.split(',')
|
35
|
+
when "LIMITS"
|
36
|
+
function, limits = args.split(",",2)
|
37
|
+
limits = limits[1...-1].split(",")
|
38
|
+
next if function == 'ONOFF'
|
39
|
+
limits.map! { |l| l.to_f / 10 } if %w{SETPTEMP AMBTEMP}.include?(function)
|
40
|
+
@limits[function.downcase.to_sym] = limits
|
41
|
+
when "CHN,1"
|
42
|
+
function, value = args.split(",")
|
43
|
+
value = value == 'ON' if function == 'ONOFF'
|
44
|
+
value = value.to_f / 10 if %w{SETPTEMP AMBTEMP}.include?(function)
|
45
|
+
value = value.to_i if function == 'ERRCODE'
|
46
|
+
value = nil if value == -3276.8
|
47
|
+
instance_variable_set(:"@#{function.downcase}", value)
|
48
|
+
when "CFG"
|
49
|
+
function, value = args.split(",", 2)
|
50
|
+
@devicename = value if function == 'DEVICENAME'
|
51
|
+
end
|
52
|
+
break unless @io.ready?
|
53
|
+
end
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
def ping
|
58
|
+
@io.puts("PING")
|
59
|
+
end
|
60
|
+
|
61
|
+
def onoff=(value)
|
62
|
+
@io.puts("SET,1:ONOFF,#{value ? 'ON' : 'OFF'}")
|
63
|
+
end
|
64
|
+
|
65
|
+
%w{mode fansp vaneud vanelr}.each do |function|
|
66
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
67
|
+
def #{function}=(value)
|
68
|
+
return if limits[:#{function}] && !limits[:#{function}].include?(value.to_s)
|
69
|
+
@io.puts("SET,1:#{function.upcase},\#{value}")
|
70
|
+
end
|
71
|
+
RUBY
|
72
|
+
end
|
73
|
+
|
74
|
+
def setptemp=(value)
|
75
|
+
value = value.round
|
76
|
+
return if limits[:setptemp] && (value < limits[:setptemp].first || value > limits[:setptemp].last)
|
77
|
+
@io.puts("SET,1:SETPTEMP,#{value * 10}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def devicename=(value)
|
81
|
+
@io.puts("CFG:DEVICENAME,#{value}")
|
82
|
+
# have to re-query to ensure it got the new value
|
83
|
+
@io.puts("CFG:DEVICENAME")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module IntesisBox
|
4
|
+
class Discovery
|
5
|
+
class << self
|
6
|
+
def discover(timeout: 1, expected_count: nil, expect: nil)
|
7
|
+
wmps = {}
|
8
|
+
discovery = new(timeout: timeout) do |wmp|
|
9
|
+
wmps[wmp[:mac]] = wmp
|
10
|
+
next false if wmps.length == expected_count
|
11
|
+
next false if expect && wmp[:mac] == expect
|
12
|
+
true
|
13
|
+
end
|
14
|
+
wmps
|
15
|
+
ensure
|
16
|
+
discovery&.close
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(timeout: nil)
|
21
|
+
@socket = UDPSocket.new
|
22
|
+
@socket.bind("0.0.0.0", 3310)
|
23
|
+
@socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
|
24
|
+
@found = []
|
25
|
+
|
26
|
+
receive_lambda = -> do
|
27
|
+
loop do
|
28
|
+
if IO.select([@socket], nil, nil, timeout)
|
29
|
+
@socket.wait_readable
|
30
|
+
msg, _ = @socket.recvfrom(128)
|
31
|
+
next unless msg.start_with?("DISCOVER:")
|
32
|
+
msg = msg[9..-1]
|
33
|
+
|
34
|
+
model, mac, ip, protocol, version, rssi, name, _, _ = msg.split(",")
|
35
|
+
wmp = { mac: mac, model: model, ip: ip, protocol: protocol, version: version, rssi: rssi, name: name }
|
36
|
+
if block_given?
|
37
|
+
break unless yield wmp
|
38
|
+
else
|
39
|
+
@found << wmp
|
40
|
+
end
|
41
|
+
else
|
42
|
+
break
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if timeout
|
48
|
+
discover
|
49
|
+
receive_lambda.call
|
50
|
+
else
|
51
|
+
@receive_thread = Thread.new(&receive_lambda)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def close
|
56
|
+
@receive_thread&.kill
|
57
|
+
@socket.close
|
58
|
+
end
|
59
|
+
|
60
|
+
def discover
|
61
|
+
@socket.sendmsg("DISCOVER\r\n", 0, Socket.sockaddr_in(3310, '255.255.255.255'))
|
62
|
+
end
|
63
|
+
|
64
|
+
def pending_discoveries
|
65
|
+
result, @found = @found, []
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/intesisbox.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'intesis_box'
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: intesisbox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Cody Cutrer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-12-27 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.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '9.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '9.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
|
+
- intesisbox_mqtt_bridge
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- bin/intesisbox_mqtt_bridge
|
63
|
+
- lib/intesis_box.rb
|
64
|
+
- lib/intesis_box/client.rb
|
65
|
+
- lib/intesis_box/discovery.rb
|
66
|
+
- lib/intesis_box/version.rb
|
67
|
+
- lib/intesisbox.rb
|
68
|
+
homepage: https://github.com/ccutrer/intesisbox
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubygems_version: 3.1.4
|
88
|
+
signing_key:
|
89
|
+
specification_version: 4
|
90
|
+
summary: Library for communication with IntesisBox
|
91
|
+
test_files: []
|