ansible4ozw 0.0.1
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.
- data/lib/ansible.rb +47 -0
- data/lib/ansible/ansible_callback.rb +142 -0
- data/lib/ansible/ansible_device.rb +68 -0
- data/lib/ansible/ansible_value.rb +179 -0
- data/lib/ansible/config.rb +92 -0
- data/lib/ansible/devices/ansible_dimmer.rb +80 -0
- data/lib/ansible/devices/ansible_switch.rb +66 -0
- data/lib/ansible/knx/EIBConnection.rb +2371 -0
- data/lib/ansible/knx/dpt/canonical_1bit.rb +54 -0
- data/lib/ansible/knx/dpt/dpt1.rb +224 -0
- data/lib/ansible/knx/dpt/dpt10.rb +85 -0
- data/lib/ansible/knx/dpt/dpt11.rb +72 -0
- data/lib/ansible/knx/dpt/dpt12.rb +61 -0
- data/lib/ansible/knx/dpt/dpt13.rb +100 -0
- data/lib/ansible/knx/dpt/dpt14.rb +87 -0
- data/lib/ansible/knx/dpt/dpt15.rb +72 -0
- data/lib/ansible/knx/dpt/dpt16.rb +67 -0
- data/lib/ansible/knx/dpt/dpt17.rb +65 -0
- data/lib/ansible/knx/dpt/dpt18.rb +66 -0
- data/lib/ansible/knx/dpt/dpt19.rb +100 -0
- data/lib/ansible/knx/dpt/dpt2.rb +156 -0
- data/lib/ansible/knx/dpt/dpt3.rb +104 -0
- data/lib/ansible/knx/dpt/dpt4.rb +75 -0
- data/lib/ansible/knx/dpt/dpt5.rb +124 -0
- data/lib/ansible/knx/dpt/dpt6.rb +73 -0
- data/lib/ansible/knx/dpt/dpt7.rb +146 -0
- data/lib/ansible/knx/dpt/dpt8.rb +118 -0
- data/lib/ansible/knx/dpt/dpt9.rb +204 -0
- data/lib/ansible/knx/dpt/tests/test_dpt10.rb +45 -0
- data/lib/ansible/knx/dpt/tests/test_dpt9.rb +60 -0
- data/lib/ansible/knx/hexdump.rb +113 -0
- data/lib/ansible/knx/knx_dpt.rb +58 -0
- data/lib/ansible/knx/knx_dpt_scalar.rb +62 -0
- data/lib/ansible/knx/knx_eistypes.rb +76 -0
- data/lib/ansible/knx/knx_protocol.rb +99 -0
- data/lib/ansible/knx/knx_scene.rb +48 -0
- data/lib/ansible/knx/knx_tools.rb +76 -0
- data/lib/ansible/knx/knx_transceiver.rb +237 -0
- data/lib/ansible/knx/knx_value.rb +327 -0
- data/lib/ansible/openzwave/ozw_constants.rb +11 -0
- data/lib/ansible/openzwave/ozw_headers.rb +80 -0
- data/lib/ansible/openzwave/ozw_remote_manager.rb +7615 -0
- data/lib/ansible/openzwave/ozw_types.rb +406 -0
- data/lib/ansible/orbiter_proxy.rb +12 -0
- data/lib/ansible/transceiver.rb +63 -0
- data/lib/ansible/zwave/types/valuetype_bool.rb +74 -0
- data/lib/ansible/zwave/types/valuetype_button.rb +63 -0
- data/lib/ansible/zwave/types/valuetype_byte.rb +78 -0
- data/lib/ansible/zwave/types/valuetype_decimal.rb +64 -0
- data/lib/ansible/zwave/types/valuetype_int.rb +63 -0
- data/lib/ansible/zwave/types/valuetype_list.rb +64 -0
- data/lib/ansible/zwave/types/valuetype_short.rb +63 -0
- data/lib/ansible/zwave/types/valuetype_string.rb +61 -0
- data/lib/ansible/zwave/zwave_command_classes.rb +113 -0
- data/lib/ansible/zwave/zwave_node.rb +5 -0
- data/lib/ansible/zwave/zwave_protocol.rb +52 -0
- data/lib/ansible/zwave/zwave_transceiver.rb +435 -0
- data/lib/ansible/zwave/zwave_value.rb +193 -0
- metadata +108 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
=begin
|
2
|
+
Project Ansible - An extensible home automation scripting framework
|
3
|
+
----------------------------------------------------
|
4
|
+
Copyright (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
|
5
|
+
|
6
|
+
SOFTWARE NOTICE AND LICENSE
|
7
|
+
|
8
|
+
Project Ansible is free software: you can redistribute it and/or modify
|
9
|
+
it under the terms of the GNU Lesser General Public License as published
|
10
|
+
by the Free Software Foundation, either version 3 of the License,
|
11
|
+
or (at your option) any later version.
|
12
|
+
|
13
|
+
Project Ansible is distributed in the hope that it will be useful,
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
GNU Lesser General Public License for more details.
|
17
|
+
|
18
|
+
You should have received a copy of the GNU Lesser General Public License
|
19
|
+
along with Project Ansible. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
|
21
|
+
for more information on the LGPL, see:
|
22
|
+
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
|
23
|
+
=end
|
24
|
+
|
25
|
+
module Ansible
|
26
|
+
|
27
|
+
module KNX
|
28
|
+
|
29
|
+
# KNX Scene control module
|
30
|
+
module Scene
|
31
|
+
|
32
|
+
@scene_values = {}
|
33
|
+
|
34
|
+
def store(scene_id)
|
35
|
+
@scene_values[scene_id] = @current_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def recall(scene_id)
|
39
|
+
if val = @scene_values[scene_id] then
|
40
|
+
set(scene_id)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
=begin
|
2
|
+
Project Ansible - An extensible home automation scripting framework
|
3
|
+
----------------------------------------------------
|
4
|
+
Copyright (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
|
5
|
+
|
6
|
+
SOFTWARE NOTICE AND LICENSE
|
7
|
+
|
8
|
+
Project Ansible is free software: you can redistribute it and/or modify
|
9
|
+
it under the terms of the GNU Lesser General Public License as published
|
10
|
+
by the Free Software Foundation, either version 3 of the License,
|
11
|
+
or (at your option) any later version.
|
12
|
+
|
13
|
+
Project Ansible is distributed in the hope that it will be useful,
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
GNU Lesser General Public License for more details.
|
17
|
+
|
18
|
+
You should have received a copy of the GNU Lesser General Public License
|
19
|
+
along with Project Ansible. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
|
21
|
+
for more information on the LGPL, see:
|
22
|
+
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
|
23
|
+
=end
|
24
|
+
|
25
|
+
# various KNX tool functions
|
26
|
+
|
27
|
+
# addr2str: Convert an integer to an EIB address string, in the form "1/2/3" or "1.2.3"
|
28
|
+
def addr2str(a, ga=false)
|
29
|
+
str=""
|
30
|
+
if a.is_a?Array then
|
31
|
+
#~ a = a[0]*256+a[1]
|
32
|
+
a = a.pack('c*').unpack('n')[0]
|
33
|
+
end
|
34
|
+
if (ga)
|
35
|
+
str = "#{(a >> 11) & 0xf}/#{(a >> 8) & 0x7}/#{a & 0xff}"
|
36
|
+
else
|
37
|
+
str = "#{a >> 12}.#{(a >> 8) & 0xf}.#{a & 0xff}"
|
38
|
+
end
|
39
|
+
return(str)
|
40
|
+
end
|
41
|
+
|
42
|
+
# str2addr: Parse an address string into an unsigned 16-bit integer
|
43
|
+
def str2addr(s)
|
44
|
+
if m = s.match(/(\d*)\/(\d*)\/(\d*)/)
|
45
|
+
a, b, c = m[1].to_i, m[2].to_i, m[3].to_i
|
46
|
+
return ((a & 0x01f) << 11) | ((b & 0x07) << 8) | ((c & 0xff))
|
47
|
+
end
|
48
|
+
if m = s.match(/(\d*)\/(\d*)/)
|
49
|
+
a,b = m[1].to_i, m[2].to_i
|
50
|
+
return ((a & 0x01f) << 11) | ((b & 0x7FF))
|
51
|
+
end
|
52
|
+
if s.start_with?("0x")
|
53
|
+
return (s.to_i & 0xffff)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def decode_framedata(data)
|
58
|
+
case data
|
59
|
+
when Fixnum then "0x"+data.to_s(16).upcase
|
60
|
+
when Array then data.collect{|b| "0x"+b.to_s(16).upcase}
|
61
|
+
when String then data.unpack('C*').collect{|b| "0x"+b.to_s(16).upcase}
|
62
|
+
else data
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def frame_inspect(frame)
|
67
|
+
return "#{Time.now.strftime('%a %d/%m/%Y %H:%M:%S %Z')}: #{Ansible::KNX::APCICODES[frame.apci]}" +
|
68
|
+
" prio=#{Ansible::KNX::PRIOCLASSES[frame.prio_class]}" + " len=#{frame.datalength}" +
|
69
|
+
" from #{addr2str(frame.src_addr)} to #{addr2str(frame.dst_addr, frame.daf)}" +
|
70
|
+
" data=" + decode_framedata(frame.datalength > 1 ? frame.data : frame.apci_data).inspect
|
71
|
+
end
|
72
|
+
|
73
|
+
#ga = 17*256 + 200
|
74
|
+
#~ ga = [17, 200]
|
75
|
+
#~ puts(addr2str(ga))
|
76
|
+
#~ dest= str2addr("1/2/0")
|
@@ -0,0 +1,237 @@
|
|
1
|
+
=begin
|
2
|
+
Project Ansible - An extensible home automation scripting framework
|
3
|
+
----------------------------------------------------
|
4
|
+
Copyright (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
|
5
|
+
|
6
|
+
SOFTWARE NOTICE AND LICENSE
|
7
|
+
|
8
|
+
Project Ansible is free software: you can redistribute it and/or modify
|
9
|
+
it under the terms of the GNU Lesser General Public License as published
|
10
|
+
by the Free Software Foundation, either version 3 of the License,
|
11
|
+
or (at your option) any later version.
|
12
|
+
|
13
|
+
Project Ansible is distributed in the hope that it will be useful,
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
GNU Lesser General Public License for more details.
|
17
|
+
|
18
|
+
You should have received a copy of the GNU Lesser General Public License
|
19
|
+
along with Project Ansible. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
|
21
|
+
for more information on the LGPL, see:
|
22
|
+
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
|
23
|
+
=end
|
24
|
+
|
25
|
+
require 'cgi'
|
26
|
+
|
27
|
+
require 'config'
|
28
|
+
require 'transceiver'
|
29
|
+
require 'EIBConnection'
|
30
|
+
require 'ansible_callback'
|
31
|
+
require 'knx_protocol'
|
32
|
+
require 'knx_value'
|
33
|
+
require 'knx_tools'
|
34
|
+
|
35
|
+
module Ansible
|
36
|
+
|
37
|
+
module KNX
|
38
|
+
|
39
|
+
#
|
40
|
+
# The KNX Transceiver is an object responsible for i/o with the KNX bus.
|
41
|
+
# It does so using eibd, part of BCU-SDK the open-source libary for KNX.
|
42
|
+
class KNX_Transceiver < Transceiver
|
43
|
+
include AnsibleCallback
|
44
|
+
|
45
|
+
# a special exception to break the knx tranceiver loop
|
46
|
+
class NormalExit < Exception; end
|
47
|
+
|
48
|
+
attr_reader :stomp
|
49
|
+
|
50
|
+
# initialize a KNXTranceiver
|
51
|
+
#
|
52
|
+
# *params:
|
53
|
+
# [connURL] an eibd connection URL. see eibd --help for acceptable values
|
54
|
+
def initialize(connURL=KNX_URL)
|
55
|
+
raise "Already initialized!" unless Ansible::KNX::KNXValue.transceiver.nil?
|
56
|
+
@connURL = connURL
|
57
|
+
@monitor_conn_ok, @send_conn_ok = false, false
|
58
|
+
@send_mutex = Mutex.new()
|
59
|
+
@knxbuf = EIBBuffer.new()
|
60
|
+
#
|
61
|
+
super()
|
62
|
+
# store reference to ourselves to the classes that use us
|
63
|
+
Ansible::KNX::KNXValue.transceiver = self
|
64
|
+
# register default handler for KNX frames
|
65
|
+
add_callback(:onKNXtelegram) { | sender, cb, frame |
|
66
|
+
puts(frame_inspect(frame)) if $DEBUG
|
67
|
+
case frame.apci.value
|
68
|
+
when 0 then # A_GroupValue_Read
|
69
|
+
puts "read request for knx address #{addr2str(frame.dst_addr, frame.daf)}"
|
70
|
+
AnsibleValue[:groups => [frame.dst_addr]].each { |v|
|
71
|
+
unless v.current_value.nil? then
|
72
|
+
puts "==> responding with value #{v}"
|
73
|
+
send_apdu_raw(frame.dst_addr, v.to_apdu(0x40))
|
74
|
+
end
|
75
|
+
}
|
76
|
+
when 1 then # A_GroupValue_Response
|
77
|
+
puts "response frame by #{addr2str(frame.src_addr)} for knx address #{addr2str(frame.dst_addr, frame.daf)}"
|
78
|
+
AnsibleValue[:groups => [frame.dst_addr]].each { |v|
|
79
|
+
v.update_from_frame(frame)
|
80
|
+
puts "synchronized knx value #{v} from frame #{frame.inspect}" if $DEBUG
|
81
|
+
}
|
82
|
+
when 2 then # A_GroupValue_Write
|
83
|
+
AnsibleValue[:groups => [frame.dst_addr]].each { |v|
|
84
|
+
v.update_from_frame(frame)
|
85
|
+
puts "updated knx value #{v} from frame #{frame.inspect}" if $DEBUG
|
86
|
+
}
|
87
|
+
end
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# initialize eibd connection
|
92
|
+
def init_eibd(conn_symbol, conn_ok_symbol)
|
93
|
+
unless instance_variable_get(conn_ok_symbol)
|
94
|
+
begin
|
95
|
+
puts("KNX: init #{conn_symbol} to #{@connURL}")
|
96
|
+
conn = EIBConnection.new()
|
97
|
+
conn.EIBSocketURL(@connURL)
|
98
|
+
instance_variable_set(conn_symbol, conn)
|
99
|
+
instance_variable_set(conn_ok_symbol, true)
|
100
|
+
return(conn)
|
101
|
+
rescue Errno::ECONNRESET => e
|
102
|
+
conn.EIBClose
|
103
|
+
instance_variable_set(conn_ok_symbol, false)
|
104
|
+
puts "init_eibd: Disconnected, retrying in 10 seconds..."
|
105
|
+
sleep(10)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# get handle to eibd monitor connection
|
111
|
+
def eibd_connection(conn_symbol, conn_ok_symbol)
|
112
|
+
if instance_variable_get(conn_ok_symbol) then
|
113
|
+
return(instance_variable_get(conn_symbol))
|
114
|
+
else
|
115
|
+
init_eibd(conn_symbol, conn_ok_symbol)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# get handle to KNX monitoring connection, reconnecting if necessary
|
120
|
+
def monitor_conn; return(eibd_connection(:@monitor_conn, :@monitor_conn_ok)); end
|
121
|
+
|
122
|
+
# get handle to KNX sending connection, reconnecting if necessary
|
123
|
+
def send_conn; return(eibd_connection(:@send_conn, :@send_conn_ok)); end
|
124
|
+
|
125
|
+
# the main KNX transceiver thread
|
126
|
+
def run()
|
127
|
+
puts("KNX Transceiver thread is running!")
|
128
|
+
@stomp = nil
|
129
|
+
begin
|
130
|
+
#### part 1: connect to STOMP broker
|
131
|
+
@stomp = OnStomp.connect(STOMP_URL)
|
132
|
+
#### part 2: subscribe to command channel, listen for messages and pass them to KNX
|
133
|
+
# @stomp.subscribe KNX_COMMAND_TOPIC do |msg|
|
134
|
+
# dest = msg.headers['dest_addr'].to_i
|
135
|
+
# #TODO: check address limits
|
136
|
+
# apdu = Marshal.load(CGI.unescape(msg.body))
|
137
|
+
# send_apdu_raw(dest, apdu)
|
138
|
+
# end
|
139
|
+
##### part 3: monitor KNX bus, post all activity to /knx/monitor
|
140
|
+
vbm = monitor_conn.EIBOpenVBusmonitor()
|
141
|
+
loop do
|
142
|
+
len = monitor_conn.EIBGetBusmonitorPacket(@knxbuf)
|
143
|
+
#puts "knxbuffer=="+@knxbuf.buffer.inspect
|
144
|
+
frame = L_DATA_Frame.read(@knxbuf.buffer.pack('c*'))
|
145
|
+
#puts "frame:\n\t"
|
146
|
+
headers = {}
|
147
|
+
frame.field_names.each { |fieldname|
|
148
|
+
field = frame.send(fieldname)
|
149
|
+
#puts "\t#{fieldname} == #{field.value}"
|
150
|
+
headers[fieldname] = CGI.escape(field.value.to_s)
|
151
|
+
}
|
152
|
+
@stomp.send(KNX_MONITOR_TOPIC, "KNX Activity", headers)
|
153
|
+
#puts Ansible::KNX::APCICODES[frame.apci] + " packet from " +
|
154
|
+
# addr2str(frame.src_addr) + " to " + addr2str(frame.dst_addr, frame.daf) +
|
155
|
+
# " priority=" + Ansible::KNX::PRIOCLASSES[frame.prio_class]
|
156
|
+
fire_callback(:onKNXtelegram, frame.dst_addr, frame)
|
157
|
+
#
|
158
|
+
end
|
159
|
+
rescue Errno::ECONNRESET => e
|
160
|
+
@monitor_conn_ok = false
|
161
|
+
puts("EIBD disconnected! retrying in 10 seconds..")
|
162
|
+
sleep(10)
|
163
|
+
retry
|
164
|
+
rescue NormalExit => e
|
165
|
+
puts("KNX transceiver terminating gracefully...")
|
166
|
+
rescue Exception => e
|
167
|
+
puts("Exception in KNX server thread: #{e}")
|
168
|
+
puts("backtrace:\n " << e.backtrace.join("\n "))
|
169
|
+
sleep(3)
|
170
|
+
retry
|
171
|
+
# ensure
|
172
|
+
#puts "Closing EIB connection..."
|
173
|
+
#@monitor_conn.EIBClose() if @monitor_conn
|
174
|
+
#puts "Closing STOMP connection..."
|
175
|
+
#@stomp.disconnect if @stomp
|
176
|
+
end
|
177
|
+
end #def run()
|
178
|
+
|
179
|
+
# send a raw APDU to the KNX bus.
|
180
|
+
#
|
181
|
+
# * Arguments:
|
182
|
+
# [dest] destination (16-bit integer)
|
183
|
+
# [apdu] raw APDU (binary string)
|
184
|
+
def send_apdu_raw(dest, apdu)
|
185
|
+
@send_mutex.synchronize {
|
186
|
+
raise 'apdu must be a byte array!' unless apdu.is_a?Array
|
187
|
+
puts("KNX transceiver: sending to group address #{dest}, #{apdu.inspect}") if $DEBUG
|
188
|
+
if (send_conn.EIBOpenT_Group(dest, 1) == -1) then
|
189
|
+
raise("KNX client: error setting socket mode")
|
190
|
+
end
|
191
|
+
send_conn.EIBSendAPDU(apdu)
|
192
|
+
send_conn.EIBReset()
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
# (Try to) read a groupaddr from eibd cache.
|
197
|
+
#
|
198
|
+
# return it if found, otherwise query the bus. In the latter case,
|
199
|
+
# the main receiver thread (in run()) will act on the response.
|
200
|
+
#
|
201
|
+
# * Arguments:
|
202
|
+
# [ga] Fixnum: group address (0-65535)
|
203
|
+
# [cache_only] boolean: when true, do not query the bus
|
204
|
+
def read_eibd_cache(ga, cache_only=false)
|
205
|
+
src = EIBAddr.new()
|
206
|
+
buf = EIBBuffer.new()
|
207
|
+
result = nil
|
208
|
+
@send_mutex.synchronize {
|
209
|
+
# query eibd for a cached value
|
210
|
+
if (send_conn.EIB_Cache_Read_Sync(ga, src, buf, 0) == -1) then
|
211
|
+
# value not found in cache
|
212
|
+
puts "groupaddress #{addr2str(ga, true)} not found in cache."
|
213
|
+
unless cache_only then
|
214
|
+
puts ".. requesting value on bus .."
|
215
|
+
if (send_conn.EIBOpenT_Group(ga, 1) == -1) then
|
216
|
+
raise("KNX client: error setting socket mode")
|
217
|
+
end
|
218
|
+
# send a read request to the bus
|
219
|
+
send_conn.EIBSendAPDU([0,0x00])
|
220
|
+
end
|
221
|
+
send_conn.EIBReset()
|
222
|
+
else
|
223
|
+
send_conn.EIBReset()
|
224
|
+
# value found in cache..
|
225
|
+
puts "found in cache, last sender was #{addr2str(src.data)}"
|
226
|
+
result = buf.buffer
|
227
|
+
end
|
228
|
+
}
|
229
|
+
return result
|
230
|
+
end
|
231
|
+
|
232
|
+
end #class
|
233
|
+
|
234
|
+
end #module KNX
|
235
|
+
|
236
|
+
end #module Ansible
|
237
|
+
|
@@ -0,0 +1,327 @@
|
|
1
|
+
=begin
|
2
|
+
Project Ansible - An extensible home automation scripting framework
|
3
|
+
----------------------------------------------------
|
4
|
+
Copyright (c) 2011 Elias Karakoulakis <elias.karakoulakis@gmail.com>
|
5
|
+
|
6
|
+
SOFTWARE NOTICE AND LICENSE
|
7
|
+
|
8
|
+
Project Ansible is free software: you can redistribute it and/or modify
|
9
|
+
it under the terms of the GNU Lesser General Public License as published
|
10
|
+
by the Free Software Foundation, either version 3 of the License,
|
11
|
+
or (at your option) any later version.
|
12
|
+
|
13
|
+
Project Ansible is distributed in the hope that it will be useful,
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
16
|
+
GNU Lesser General Public License for more details.
|
17
|
+
|
18
|
+
You should have received a copy of the GNU Lesser General Public License
|
19
|
+
along with Project Ansible. If not, see <http://www.gnu.org/licenses/>.
|
20
|
+
|
21
|
+
for more information on the LGPL, see:
|
22
|
+
http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License
|
23
|
+
=end
|
24
|
+
|
25
|
+
require 'ansible_value'
|
26
|
+
|
27
|
+
require 'knx_transceiver'
|
28
|
+
require 'knx_protocol'
|
29
|
+
require 'knx_tools'
|
30
|
+
require 'knx_dpt'
|
31
|
+
|
32
|
+
module Ansible
|
33
|
+
|
34
|
+
module KNX
|
35
|
+
|
36
|
+
# a KNXValue is a device-dependant datapoint. It is initialized by a
|
37
|
+
# DPT type name (e.g. "1.001" for binary switch) and is extended
|
38
|
+
# by the initializer with the corresponding DPT module (e.g. KNX::DPT1)
|
39
|
+
# so as to handle DPT1 frames.
|
40
|
+
# Each KNXValue is linked to zero or more group addresses, the first
|
41
|
+
# of which will be the "update" value
|
42
|
+
class KNXValue
|
43
|
+
|
44
|
+
include AnsibleValue
|
45
|
+
|
46
|
+
#
|
47
|
+
# ------ CLASS VARIABLES & METHODS
|
48
|
+
#
|
49
|
+
@@ids = 0
|
50
|
+
def KNXValue.id_generator
|
51
|
+
@@ids = @@ids + 1
|
52
|
+
return @@ids
|
53
|
+
end
|
54
|
+
|
55
|
+
# a Hash containing all known group addresses
|
56
|
+
@@AllGroups = {}
|
57
|
+
|
58
|
+
# the transceiver responsible for all things KNX
|
59
|
+
@@transceiver = nil
|
60
|
+
def KNXValue.transceiver; return @@transceiver; end
|
61
|
+
def KNXValue.transceiver=(other);
|
62
|
+
@@transceiver = other if other.is_a? Ansible::KNX::KNX_Transceiver
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# ----- INSTANCE VARIABLES & METHODS
|
67
|
+
#
|
68
|
+
|
69
|
+
# equality checking
|
70
|
+
def == (other)
|
71
|
+
return (other.is_a?(KNXValue) and (@id == other.id))
|
72
|
+
end
|
73
|
+
|
74
|
+
# set flag: knxvalue.flags[:r] = true
|
75
|
+
# get flag: knxvalue.flags[:r] (evaluates to true, meaning the read flag is set)
|
76
|
+
attr_reader :flags
|
77
|
+
attr_reader :groups
|
78
|
+
attr_reader :dpt_basetype, :dpt_subtype, :id
|
79
|
+
attr_accessor :description
|
80
|
+
|
81
|
+
# initialize a KNXValue
|
82
|
+
# ===Arguments:
|
83
|
+
# [dpt]
|
84
|
+
# string representing the DPT (datapoint type) of the value
|
85
|
+
# e.g. "5.001" meaning DPT5 percentage value (8-bit unsigned)
|
86
|
+
# [groups]
|
87
|
+
# array of group addresses associated with this datapoint
|
88
|
+
# [flags]
|
89
|
+
# hash of symbol=>boolean flags regarding its behaviour
|
90
|
+
# e.g. {:r => true} the value can only respond to read requests on the KNX bus.
|
91
|
+
# default flags: READ and WRITE
|
92
|
+
# c => Communication
|
93
|
+
# r => Read
|
94
|
+
# w => Write
|
95
|
+
# t => Transmit
|
96
|
+
# u => Update
|
97
|
+
# i => read on Init
|
98
|
+
def initialize(dpt, groups=[], flags=nil)
|
99
|
+
|
100
|
+
# init DPT info
|
101
|
+
if md = /(\d*)\.(\d*)/.match(dpt) then
|
102
|
+
@dpt = dpt
|
103
|
+
@dpt_mod = Ansible::KNX.module_eval("DPT#{md[1]}")
|
104
|
+
raise "unknown/undeclared DPT module #{dpt}" unless @dpt_mod.is_a?Module
|
105
|
+
@parserclass = @dpt_mod.module_eval("DPT#{md[1]}_Frame")
|
106
|
+
raise "unknown/undeclared parser for DPT #{dpt}" unless @parserclass.ancestors.include?(DPTFrame)
|
107
|
+
@dpt_basetype = @dpt_mod::Basetype
|
108
|
+
raise "missing Basetype info for #{dpt}" unless @dpt_basetype.is_a?Hash
|
109
|
+
@dpt_subtype = @dpt_mod::Subtypes[md[2]]
|
110
|
+
raise "missing sybtype info for #{dpt}" unless @dpt_subtype.is_a?Hash
|
111
|
+
# extend this object with DPT-specific module
|
112
|
+
self.extend(@dpt_mod)
|
113
|
+
# print out some useful debug info
|
114
|
+
puts " dpt_basetype = #{@dpt_basetype}, dpt_subtype = #{@dpt_subtype}" if $DEBUG
|
115
|
+
else
|
116
|
+
raise "invalid datapoint type (#{dpt})"
|
117
|
+
end
|
118
|
+
|
119
|
+
# array of GroupAddress objects associated with this datapoint
|
120
|
+
# only the first address is used in a write operation (TODO: CHECKME)
|
121
|
+
@groups = case groups
|
122
|
+
when Fixnum then Array[group]
|
123
|
+
when String then Array[str2addr(groups)]
|
124
|
+
when Array then groups
|
125
|
+
end
|
126
|
+
|
127
|
+
# store DPT info about these group addresses
|
128
|
+
@groups.each { |grp|
|
129
|
+
# sanity check: is this groupaddr already decaled as a different basetype?
|
130
|
+
# FIXME: specs dont forbid it, only check required is datalength compatibility
|
131
|
+
if @@AllGroups[grp] and (old_dpt = @@AllGroups[grp][:dpt_basetype]) and not (old_dpt.eql?(@dpt_basetype))
|
132
|
+
raise "Group address #{addr2str(grp)} is already declared as DPT basetype #{old_dpt}!"
|
133
|
+
end
|
134
|
+
puts "adding groupaddr #{addr2str(grp,true) } (#{@dpt}: #{@dpt_subtype[:name]}), to global hash"
|
135
|
+
@@AllGroups[grp] = {:basetype => @dpt_basetype, :subtype => @dpt_subtype}
|
136
|
+
}
|
137
|
+
|
138
|
+
if flags.nil?
|
139
|
+
# default flags: READ and WRITE
|
140
|
+
@flags = {:r => true,:w => true}
|
141
|
+
else
|
142
|
+
raise "flags parameter must be a Hash!" unless flags.is_a?Hash
|
143
|
+
@flags = flags
|
144
|
+
end
|
145
|
+
|
146
|
+
# TODO: physical address: set only for remote nodes we are monitoring
|
147
|
+
# when left to nil, it/ means a datapoint on this KNXTransceiver
|
148
|
+
@physaddr = nil
|
149
|
+
|
150
|
+
# id of datapoint
|
151
|
+
# initialized by class method KNXValue.id_generator
|
152
|
+
@id = KNXValue.id_generator()
|
153
|
+
|
154
|
+
@description = ''
|
155
|
+
|
156
|
+
# store this KNXValue in the Ansible database
|
157
|
+
AnsibleValue.insert(self)
|
158
|
+
end
|
159
|
+
|
160
|
+
# is this KNX datapoint read only?
|
161
|
+
def read_only?
|
162
|
+
return((defined? @flags) and (@flags[:r] and not @flags[:w]))
|
163
|
+
end
|
164
|
+
|
165
|
+
# is this KNX datapoint write only?
|
166
|
+
def write_only?
|
167
|
+
return((defined? @flags) and (@flags[:w] and not @flags[:r]))
|
168
|
+
end
|
169
|
+
|
170
|
+
# read value from eibd's group cache, or issue a read request,
|
171
|
+
# hoping that someone will respond with the last known status
|
172
|
+
# for this value
|
173
|
+
def read_value()
|
174
|
+
if (not @groups.nil?) and (group = @groups[0]) then
|
175
|
+
if (data = @@transceiver.read_eibd_cache(group)) then
|
176
|
+
fire_callback(:onReadCacheHit)
|
177
|
+
# update the value
|
178
|
+
update(@parserclass.read(data.pack('c*')))
|
179
|
+
else
|
180
|
+
# value not found in cache, maybe some other
|
181
|
+
# device on the bus will respond...
|
182
|
+
fire_callback(:onReadCacheMiss)
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
else
|
186
|
+
return false
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
#
|
191
|
+
# write value to the bus
|
192
|
+
# argument: new_val according to DPT
|
193
|
+
# if :basic, then new_val is plain data value
|
194
|
+
# else (:composite) new_val is a hash of
|
195
|
+
# field_name => field_value pairs
|
196
|
+
# all values get mapped using to_protocol_value() in AnsibleValue::update()
|
197
|
+
# return true if successful, false otherwise
|
198
|
+
def write_value(new_val)
|
199
|
+
#write value to primary group address
|
200
|
+
dest, telegram = nil, nil
|
201
|
+
if @groups.length > 0 then
|
202
|
+
dest = @groups[0]
|
203
|
+
else
|
204
|
+
raise "#{self}: primary group address not set!!!"
|
205
|
+
end
|
206
|
+
case @dpt_mod::Basetype[:valuetype]
|
207
|
+
when :basic then
|
208
|
+
# basic value: single 'data' field
|
209
|
+
telegram = @parserclass.new(:data => new_val)
|
210
|
+
when :composite then
|
211
|
+
if new_val.is_a?Hash then
|
212
|
+
telegram = @parserclass.new(new_val)
|
213
|
+
else
|
214
|
+
if @parserclass.methods.include?(:assign) then
|
215
|
+
puts "FIXME"
|
216
|
+
else
|
217
|
+
raise "#{self} has a composite DPT, set() expects a hash!"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
#
|
222
|
+
puts "#{self}: Writing new value (#{new_val}) to #{addr2str(dest, true)}"
|
223
|
+
#
|
224
|
+
if (@@transceiver.send_apdu_raw(dest, to_apdu(telegram, 0x80)) > -1) then
|
225
|
+
update(telegram)
|
226
|
+
return(true)
|
227
|
+
else
|
228
|
+
return(false)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# set primary group address
|
233
|
+
def group_primary=(grpaddr)
|
234
|
+
@groups.unshift(grpaddr)
|
235
|
+
end
|
236
|
+
|
237
|
+
# assign a new array of group addresses for this datapoint
|
238
|
+
def groups=(other)
|
239
|
+
raise "KNXValue.groups= requires an array of at least one group addresses" unless (other.is_a?Array) and (other.length > 0)
|
240
|
+
@groups.replace(other)
|
241
|
+
end
|
242
|
+
|
243
|
+
# make sure all frame fields are valid (within min,max range)
|
244
|
+
# see Ansible::AnsibleValue.update
|
245
|
+
def validate_ranges
|
246
|
+
if defined? @current_value then
|
247
|
+
@current_value.validate_ranges
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# create apdu for this KNXValue
|
252
|
+
# APDU types are:
|
253
|
+
# 0x00 => Read
|
254
|
+
# 0x40 => Response (default)
|
255
|
+
# 0x80 => Write
|
256
|
+
def to_apdu(frame, apci_code = 0x40)
|
257
|
+
apdu = if @dpt_mod::Basetype[:bitlength] <= 6 then
|
258
|
+
#[0, apci_code | @current_value]
|
259
|
+
[0, apci_code | frame.data]
|
260
|
+
else
|
261
|
+
#[0, apci_code] + @current_value.to_a
|
262
|
+
[0, apci_code] + frame.to_binary_s.unpack('C*')
|
263
|
+
end
|
264
|
+
return apdu
|
265
|
+
end
|
266
|
+
|
267
|
+
# update internal state from raw KNX frame
|
268
|
+
def update_from_frame(rawframe)
|
269
|
+
data = if @dpt_mod::Basetype[:bitlength] <= 6 then
|
270
|
+
# bindata always expects a binary string
|
271
|
+
[rawframe.apci_data].pack('c')
|
272
|
+
else
|
273
|
+
rawframe.data
|
274
|
+
end
|
275
|
+
foo = @parserclass.read(data)
|
276
|
+
puts "update_from_frame: #{foo.class} = #{foo.inspect}"
|
277
|
+
update(foo)
|
278
|
+
end
|
279
|
+
|
280
|
+
# human-readable representation of the value. Uses all field
|
281
|
+
# info from its DPT included module, if available.
|
282
|
+
def to_s
|
283
|
+
dpt_name = (@dpt_subtype.nil?) ? '' : @dpt_subtype[:name]
|
284
|
+
dpt_info = "KNXValue[#{@dpt} #{dpt_name}]"
|
285
|
+
# add field values explanation, if any
|
286
|
+
vstr = (defined?(@current_value) ? explain(@current_value) : '(value undefined)')
|
287
|
+
# return @dpt: values.explained
|
288
|
+
gaddrs = @groups.collect{|ga| addr2str(ga, true)}.join(', ')
|
289
|
+
return [@description, gaddrs, dpt_info].compact.join(' ') + " : #{vstr}"
|
290
|
+
end
|
291
|
+
|
292
|
+
# return a human-readable representation of a DPT frame
|
293
|
+
def explain(frame)
|
294
|
+
raise "explain() expects a DPTFrame, got a #{frame.class}" unless frame.is_a?DPTFrame
|
295
|
+
fielddata = []
|
296
|
+
# iterate over all available DPT fields
|
297
|
+
frame.field_names.each { |fieldname|
|
298
|
+
# skip padding fields
|
299
|
+
next if /pad/.match(fieldname)
|
300
|
+
field = frame.send(fieldname)
|
301
|
+
fval = field.value
|
302
|
+
# get value encoding hashmap, if any
|
303
|
+
vhash = getparam(:enc, field)
|
304
|
+
# get value units
|
305
|
+
units = getparam(:unit, field)
|
306
|
+
fval = as_canonical_value()
|
307
|
+
# add field value, according to encoding hashtable
|
308
|
+
fielddata << "#{(vhash.is_a?Hash) ? vhash[fval] : fval} #{units}"
|
309
|
+
}
|
310
|
+
return fielddata.join(', ')
|
311
|
+
end
|
312
|
+
|
313
|
+
# get a DPT parameter, trying to locate it in the following order:
|
314
|
+
# 1) in the DPTFrame field definition
|
315
|
+
# 2) in the DPT subtype definition
|
316
|
+
# 3) in the DPT basetype definition
|
317
|
+
def getparam(param, field=nil)
|
318
|
+
return ((field and field.get_parameter(param)) or
|
319
|
+
(@dpt_subtype and @dpt_subtype[param]) or
|
320
|
+
(@dpt_basetype and @dpt_basetype[param]))
|
321
|
+
end
|
322
|
+
|
323
|
+
end #class KNXValue
|
324
|
+
|
325
|
+
end #module KNX
|
326
|
+
|
327
|
+
end #module Ansible
|