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