diff --git a/ansible_callback.rb b/ansible_callback.rb index 8752e9b..96f5512 100644 --- a/ansible_callback.rb +++ b/ansible_callback.rb @@ -41,12 +41,12 @@ module AnsibleCallback def declare_callback(cb, &cb_body) raise "declare_callback: 1st argument must be a Symbol" unless cb.is_a?Symbol raise "declare_callback: 2nd argument must be a Proc" unless cb_body.is_a?Proc - @callbacks = {} unless @callbacks.is_a?Hash + @callbacks = {} if @callbacks.nil? if (cb.to_s[0..1] == "on") then - puts "Registering callback (#{cb}) for #{self}" + puts "Registering callback (#{cb}) for #{self.inspect}" @callbacks[cb] = cb_body elsif (cb.to_s == "default") then - puts "Registering DEFAULT callback for #{self}" + puts "Registering DEFAULT callback for #{self.inspect}" @callbacks.default = cb_body end end @@ -62,7 +62,7 @@ def fire_callback(cb, *args) @callbacks = {} unless @callbacks.is_a?Hash default = @callbacks.has_key?(cb) if (cb_proc = @callbacks[cb]).is_a?Proc then - puts "firing callback(#{cb}) args: #{args.inspect}" + puts "firing callback(#{cb}) args: #{args.inspect}" if $DEBUG cb_proc .call(self, cb.to_s, *args) else #puts "WARNING: callback #{cb} not found for #{self}, iv=#{iv} cb_proc=#{cb_proc.inspect}" diff --git a/ansible_value.rb b/ansible_value.rb index c7978c3..98f805f 100644 --- a/ansible_value.rb +++ b/ansible_value.rb @@ -22,8 +22,12 @@ http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License =end -module AnsibleValue +require 'ansible_callback' +module AnsibleValue + + include AnsibleCallback + attr_reader :previous_value, :current_value attr_reader :last_update @@ -35,10 +39,12 @@ def matches?(hash) hash.each { |iv_symbol, filter| raise "#{self.class}: AnsibleValue.match?(hash)'s keys must be Symbols.." unless iv_symbol.is_a?Symbol if val = instance_eval('@'+iv_symbol.to_s) then - #puts "match.val(#{iv_symbol}) == #{val}" + #puts "match.val(#{iv_symbol}) == #{val.inspect}" result = result & case filter + # if the filter is a regular expression, use it to match the instance value when Regexp then filter.match(val.to_s) - when Array then filter.include?(val) + # if the filter is an array, use set intersectionfrom_frame + when Array then (filter & val).length > 0 else filter == val end else @@ -52,14 +58,14 @@ def matches?(hash) @@AllValues = [] # lookup an AnsibleValue by a filter hash + # returns an array of matching values def AnsibleValue.[](filter_hash) #puts "AnsibleValue[] called, filter_hash=#{filter_hash}" - result_set = nil + result_set = [] @@AllValues.each { |v| raise "ooops! @@AllValues contains a non-AnsibleValue!" unless v.is_a?(AnsibleValue) if v.matches?(filter_hash) then - puts "Found a matching value! #{v}" if $DEBUG - result_set = Array.new unless result_set.is_a?Array + #puts "Found a matching value! #{v}" if $DEBUG result_set << v end } @@ -72,7 +78,7 @@ def AnsibleValue.[](filter_hash) def AnsibleValue.insert(newvalue) if @@AllValues.include?(newvalue) then # newvalue is already stored in @@AllValues, find it and return it - return( @@AllValues.find{|val| val == newvalue} ) + return( @@AllValues.find{|val| newvalue == val} ) else puts "Adding a new value to @@AllValues (#{newvalue})" if $DEBUG @@AllValues << newvalue @@ -86,13 +92,14 @@ def AnsibleValue.insert(newvalue) def update(newval) unless @current_value == newval then @last_update = Time.now - puts "==> updating value #{self}, with #{newval.inspect}" + puts "==> updating value #{self}, with #{newval.class}:#{newval.inspect}" # previous value was different, update it and fire onUpdate handler @previous_value = @current_value @current_value = newval # trigger onUpdate callback, if any fire_callback(:onUpdate, @current_value) end + return(@current_value) end end #module \ No newline at end of file diff --git a/knx/knx_protocol.rb b/knx/knx_protocol.rb index 21ce0df..a1e9ea9 100644 --- a/knx/knx_protocol.rb +++ b/knx/knx_protocol.rb @@ -25,52 +25,59 @@ require 'rubygems' require 'bit-struct' -class KNX_TP_ControlField < BitStruct - unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req" - unsigned :rep_flag, 1, "Repeat flag" - unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame" - unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)" - unsigned :unused1, 2, "two unused bits (should be 00)" -end - -class KNX_L_DATA_Frame < BitStruct - # octet 0: TP1 control field - unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req" - unsigned :rep_flag, 1, "Repeat flag" - unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame" - unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)" - unsigned :unused1, 2, "two unused bits (should be 00)" - # octet 1+2: source - unsigned :src_addr, 16, "Source Address" - # octet 3+4: destination - unsigned :dst_addr, 16, "Destination Address" - # octet 5: control fields - unsigned :daf, 1, "Dest.Address flag 0=physical 1=group" - unsigned :ctrlfield, 3, "Network control field" - unsigned :datalength, 4, "Data length (bytes after octet #6)" - # octet 6 .. plus 2 bits from octet 7: TPCI+APCI - unsigned :tpci, 2, "TPCI control bits 8+7" - unsigned :seq, 4, "Packet sequence" - unsigned :apci, 4, "APCI control bits" - # octet 7 ... end - unsigned :apci_data, 6, "APCI/Data combined" - rest :data, "rest of frame" -end - -######################################################### - -APCICODES = "A_GroupValue_Read A_GroupValue_Response A_GroupValue_Write \ - A_PhysicalAddress_Write A_PhysicalAddress_Read A_PhysicalAddress_Response \ - A_ADC_Read A_ADC_Response A_Memory_Read A_Memory_Response A_Memory_Write \ - A_UserMemory A_DeviceDescriptor_Read A_DeviceDescriptor_Response A_Restart \ - A_OTHER".split() - -TPDUCODES = "T_DATA_XXX_REQ T_DATA_CONNECTED_REQ T_DISCONNECT_REQ T_ACK".split() - -PRIOCLASSES = "system alarm high low".split() - -######################################################### - +module Ansible + + module KNX + + class TP_ControlField < BitStruct + unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req" + unsigned :rep_flag, 1, "Repeat flag" + unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame" + unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)" + unsigned :unused1, 2, "two unused bits (should be 00)" + end + + class L_DATA_Frame < BitStruct + # octet 0: TP1 control field + unsigned :lpdu_code, 2, "LPDU (2bit) 2=L_DATA.req 3=L_Poll_data.req" + unsigned :rep_flag, 1, "Repeat flag" + unsigned :ack_not, 1, "0 = Acknowledge frame, 1 = standard frame" + unsigned :prio_class, 2, "Priority class (0=highest .. 3=lowest)" + unsigned :unused1, 2, "two unused bits (should be 00)" + # octet 1+2: source + unsigned :src_addr, 16, "Source Address" + # octet 3+4: destination + unsigned :dst_addr, 16, "Destination Address" + # octet 5: control fields + unsigned :daf, 1, "Dest.Address flag 0=physical 1=group" + unsigned :ctrlfield, 3, "Network control field" + unsigned :datalength, 4, "Data length (bytes after octet #6)" + # octet 6 .. plus 2 bits from octet 7: TPCI+APCI + unsigned :tpci, 2, "TPCI control bits 8+7" + unsigned :seq, 4, "Packet sequence" + unsigned :apci, 4, "APCI control bits" + # octet 7 ... end + unsigned :apci_data, 6, "APCI/Data combined" + rest :data, "rest of frame" + end + + ######################################################### + + APCICODES = "A_GroupValue_Read A_GroupValue_Response A_GroupValue_Write \ + A_PhysicalAddress_Write A_PhysicalAddress_Read A_PhysicalAddress_Response \ + A_ADC_Read A_ADC_Response A_Memory_Read A_Memory_Response A_Memory_Write \ + A_UserMemory A_DeviceDescriptor_Read A_DeviceDescriptor_Response A_Restart \ + A_OTHER".split() + + TPDUCODES = "T_DATA_XXX_REQ T_DATA_CONNECTED_REQ T_DISCONNECT_REQ T_ACK".split() + + PRIOCLASSES = "system alarm high low".split() + + ######################################################### + + end #module KNX + +end #module #~ data = [188, 17, 200, 18, 1, 242, 0, 128, 80, 171] .pack("c*") #~ knxpacket = KNX_L_DATA_Frame.new(data) diff --git a/knx/knx_transceiver.rb b/knx/knx_transceiver.rb index 9957e0c..d937dae 100644 --- a/knx/knx_transceiver.rb +++ b/knx/knx_transceiver.rb @@ -1,5 +1,26 @@ -#~ Project Ansible -#~ (c) 2011 Elias Karakoulakis +=begin +Project Ansible - An extensible home automation scripting framework +---------------------------------------------------- +Copyright (c) 2011 Elias Karakoulakis + +SOFTWARE NOTICE AND LICENSE + +Project Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +Project Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Project Ansible. If not, see . + +for more information on the LGPL, see: +http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License +=end require 'cgi' @@ -25,78 +46,92 @@ module Ansible - class KNX_Transceiver < Transceiver - include AnsibleCallback - - attr_reader :stomp + module KNX - def initialize(connURL) - begin - puts("KNX: init connection to #{connURL}") - @monitor_conn = EIBConnection.new() - @monitor_conn.EIBSocketURL(connURL) - @send_conn = EIBConnection.new() - @send_conn.EIBSocketURL(connURL) - @knxbuf = EIBBuffer.new() - super() - rescue Exception => e - puts "Initializing #{self}: " + e + "\n\t" + e.backtrace.join("\n\t") + class KNX_Transceiver < Transceiver + include AnsibleCallback + + # a special exception to break the knx tranceiver loop + class NormalExit < Exception; end + + attr_reader :stomp + + def initialize(connURL) + begin + puts("KNX: init connection to #{connURL}") + @monitor_conn = EIBConnection.new() + @monitor_conn.EIBSocketURL(connURL) + @send_conn = EIBConnection.new() + @send_conn.EIBSocketURL(connURL) + @send_mutex = Mutex.new() + @knxbuf = EIBBuffer.new() + super() + rescue Exception => e + puts "#{self}.initialize() EXCEPTION: #{e}\n\t" + e.backtrace.join("\n\t") + end end - end - - # the main KNX transceiver thread - def run() - puts("KNX Transceiver thread is running!") - begin - #### part 1: connect to STOMP broker - @stomp = OnStomp.connect "stomp://localhost" - #### part 2: subscribe to command channel, listen for messages and pass them to KNX - # @stomp.subscribe KNX_COMMAND_TOPIC do |msg| - # dest = msg.headers['dest_addr'].to_i - # #TODO: check address limits - # apdu = Marshal.load(CGI.unescape(msg.body)) - # send_apdu_raw(dest, apdu) - # end - ##### part 3: monitor KNX bus, post all activity to /knx/monitor - @monitor_thread = Thread.new { + + # the main KNX transceiver thread + def run() + puts("KNX Transceiver thread is running!") + @stomp = nil + begin + #### part 1: connect to STOMP broker + @stomp = OnStomp.connect "stomp://localhost" + #### part 2: subscribe to command channel, listen for messages and pass them to KNX + # @stomp.subscribe KNX_COMMAND_TOPIC do |msg| + # dest = msg.headers['dest_addr'].to_i + # #TODO: check address limits + # apdu = Marshal.load(CGI.unescape(msg.body)) + # send_apdu_raw(dest, apdu) + # end + ##### part 3: monitor KNX bus, post all activity to /knx/monitor vbm = @monitor_conn.EIBOpenVBusmonitor() loop do + src, dest ="", "" len = @monitor_conn.EIBGetBusmonitorPacket(@knxbuf) @monitor_conn.EIBGetGroup_Src(@buf, src, dest) - frame = KNX_L_DATA_Frame.new(@knxbuf.buffer.pack('c*')) + #puts "knxbuffer=="+@knxbuf.buffer.inspect + frame = L_DATA_Frame.new(@knxbuf.buffer.pack('c*')) + #puts "frame:\n\t" headers = {} frame.fields.each { |fld| - headers[fld.name] = CGI.escape(fld.inspect_in_object(frame, :default)) + fldvalue = fld.inspect_in_object(frame, :default) + #puts "\t#{fld.name} == #{fldvalue}" + headers[fld.name] = CGI.escape(fldvalue) } - message = "KNX transceiver: #{APCICODES[headers.apci]} packet from #{addr2str(frame.src_addr)} to #{addr2str(frame.dst_addr, frame.daf)}, priority:#{PRIOCLASSES[headers.prio]}" - @stomp.send(KNX_MONITOR_TOPIC, message, headers) - fire_callback(:onKNXactivity, frame) + @stomp.send(KNX_MONITOR_TOPIC, "KNX Activity", headers) + fire_callback(:onKNXtelegram, frame) + # + end + rescue NormalExit => e + puts("KNX transceiver terminating gracefully...") + rescue Exception => e + puts("Exception in KNX server thread: #{e}") + puts("backtrace:\n " << e.backtrace.join("\n ")) + retry + ensure + @monitor_conn.EIBClose() if @monitor_conn + @stomp.disconnect if @stomp + end + end #def run() + + def send_apdu_raw(dest, apdu) + @send_mutex.synchronize { + puts("KNX transceiver: sending to group address #{dest}, #{apdu.inspect}") + if (@send_conn.EIBOpenT_Group(dest, 1) == -1) then + raise("KNX client: error setting socket mode") end + @send_conn.EIBSendAPDU(apdu) + @send_conn.EIBReset() } - @monitor_thread.join - rescue NormalExit => e - puts("KNX transceiver terminating gracefully...") - rescue Exception => e - puts("Exception in KNX server thread: #{e}") - puts("backtrace:\n " << e.backtrace.join("\n ")) - ensure - @monitor_conn.EIBClose() - @stomp.disconnect end - end #def Thread.run() - - def send_apdu_raw(dest, apdu) - puts("KNX transceiver: sending to group address #{dest}, #{apdu.inspect}") - if (@send_conn.EIBOpenT_Group(dest, 1) == -1) then - raise("KNX client: error setting socket mode") - end - @send_conn.EIBSendAPDU(apdu) - @send_conn.EIBReset() - end - - end #class + + end #class -end #module + end #module KNX + +end #module Ansible #KNX = Ansible::KNX_Transceiver.new("ip:192.168.0.10") #KNX.thread.join diff --git a/knx/knx_value.rb b/knx/knx_value.rb index d987e59..fa1799c 100644 --- a/knx/knx_value.rb +++ b/knx/knx_value.rb @@ -30,6 +30,8 @@ module Ansible + module KNX + # definition: a KNXValue is the device-dependant datapoint, having a # well defined data type (EIS type): EIS1 (boolean), EIS5 (float) # linked to zero or more group addresses, @@ -55,21 +57,24 @@ def == (other) return (@id == other.id) end - attr_reader :groups, :eistype + attr_reader :groups, :dpt_type, :id attr_accessor :description - # initialize KNXValue by its EIS type - def initialize(transceiver, eistype, groups) + # initialize KNXValue + def initialize(transceiver, groups=[], flags=nil) + # the transceiver responsible for all things KNX @transceiver = transceiver - + # array of group addresses associated with this datapoint # only the first address is used in a write operation (TODO: CHECKME) - @groups = [] - @groups.replace(groups) if groups.is_a? Array + @groups = case groups + when String then Array[str2addr(groups)] + when Array then groups + end # set flag: knxvalue.flags[:r] = true # test flag: knxvalue.flags[:r] (evaluates to true, meaning the read flag is set) - @flags = {} + @flags = flags or {} # c => Communication # r => Read # w => Write @@ -86,7 +91,7 @@ def initialize(transceiver, eistype, groups) # id of datapoint # initialized by class method KNXValue.id_generator - @id = nil + @id = KNXValue.id_generator() end # get a value from eibd @@ -96,33 +101,38 @@ def get() # set (write) a value to eibd def set(new_val) - #write value to 1/2/0 - dest= str2addr("1/2/0") - puts "Writing (dest)" - if (conn.EIBOpenT_Group(dest, 1) == -1) - puts("KNX client: error setting socket mode") - puts(conn.inspect) - exit(1) + #write value to primary group address + dest = nil + if @groups.length > 0 then + dest = @groups[0] + else + raise "#{self}: primary group address not set!!!" end - data = create_apdu - puts ("data length=#{data.length}") - conn.EIBSendAPDU(data) - conn.EIBReset() - + apdu = create_apdu() + puts "#{self}: Writing value to #{addr2str(dest)}" + # + @transceiver.send_apdu_raw(dest, apdu) end + def group_primary=(grpaddr) + @groups.unshift(grpaddr) + end def groups=(other) - raise "KNXValue.groups= requires an array of group addresses" unless other.is_a?Array + raise "KNXValue.groups= requires an array of at least one group addresses" unless (other.is_a?Array) and (other.length > 0) @groups.replace(other) end def create_apdu - - val = 0 # 0=Off, 1=On - data = [0, 0x80| val] + raise "must override create_apdu!!!" end - end # class + end #class KNXValue + + # + # now we can load all known KNXValue_DPT** classes + Dir["knx/dpt/*.rb"].each { |f| load f } + + end #module KNX -end #module +end #module Ansible diff --git a/test1.rb b/test1.rb index 0f67b0d..9a8cf55 100644 --- a/test1.rb +++ b/test1.rb @@ -1,3 +1,27 @@ +=begin +Project Ansible - An extensible home automation scripting framework +---------------------------------------------------- +Copyright (c) 2011 Elias Karakoulakis + +SOFTWARE NOTICE AND LICENSE + +Project Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +Project Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Project Ansible. If not, see . + +for more information on the LGPL, see: +http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License +=end + $:.push(Dir.getwd) $:.push(File.join(Dir.getwd, 'knx')) $:.push(File.join(Dir.getwd, 'zwave')) @@ -14,18 +38,21 @@ sleep(3) Tree = AnsibleValue[ :_nodeId => 2, - :_commandClassId => 32 + :_genre => OpenZWave::RemoteValueGenre::ValueGenre_Basic ][0] - + +Tree.declare_callback(:onUpdate) { | val, event| + puts "-------- ZWAVE NODE #{val._nodeId} #{event}! CURRENT VALUE==#{val.current_value} ------------" +} + if Dimmer = AnsibleValue[ - :_nodeId => 5, - :_commandClassId => OpenZWave::CommandClassesByName[:COMMAND_CLASS_SWITCH_MULTILEVEL], + :_nodeId => 5, :_type => OpenZWave::RemoteValueType::ValueType_Byte, - :_valueIndex => 0 + :_genre => OpenZWave::RemoteValueGenre::ValueGenre_Basic ] then Dimmer[0].declare_callback(:onUpdate) { | val, event| - puts "ZWAVE EVENT #{val}.#{event}! CURRENT VALUE==#{val.current_value} --------------------" - Tree.set(val.current_value>0) + puts "-------- ZWAVE NODE #{val._nodeId} #{event}! CURRENT VALUE==#{val.current_value} ------------" +# Tree.set(val.current_value>0) } else puts "valueid not found!" diff --git a/test2.rb b/test2.rb new file mode 100644 index 0000000..bdb37b9 --- /dev/null +++ b/test2.rb @@ -0,0 +1,40 @@ +=begin +Project Ansible - An extensible home automation scripting framework +---------------------------------------------------- +Copyright (c) 2011 Elias Karakoulakis + +SOFTWARE NOTICE AND LICENSE + +Project Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +Project Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Project Ansible. If not, see . + +for more information on the LGPL, see: +http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License +=end + +$:.push(Dir.getwd) +$:.push(File.join(Dir.getwd, 'knx')) +$:.push(File.join(Dir.getwd, 'zwave')) + +load 'knx_transceiver.rb' +load 'knx_tools.rb' + +KNX = Ansible::KNX::KNX_Transceiver.new("ip:192.168.0.10") +# monitor all KNX activity +KNX.declare_callback(:onKNXtelegram) { | sender, cb, frame | + if frame.dst_addr < 4048 then + puts "frame==#{frame.inspect}" + puts "data ==#{frame.data}" + puts Ansible::KNX::APCICODES[frame.apci] + " packet from " + addr2str(frame.src_addr) + " to " + addr2str(frame.dst_addr, frame.daf) + ", priority=" + Ansible::KNX::PRIOCLASSES[frame.prio_class]\ + end +} \ No newline at end of file diff --git a/test3.rb b/test3.rb new file mode 100644 index 0000000..2936c5f --- /dev/null +++ b/test3.rb @@ -0,0 +1,101 @@ +=begin +Project Ansible - An extensible home automation scripting framework +---------------------------------------------------- +Copyright (c) 2011 Elias Karakoulakis + +SOFTWARE NOTICE AND LICENSE + +Project Ansible is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +Project Ansible is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with Project Ansible. If not, see . + +for more information on the LGPL, see: +http://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License +=end + +$:.push(Dir.getwd) +$:.push(File.join(Dir.getwd, 'knx')) +$:.push(File.join(Dir.getwd, 'zwave')) + +load 'transceiver.rb' +load 'zwave_transceiver.rb' +load 'zwave_command_classes.rb' + +load 'knx_transceiver.rb' +load 'knx_tools.rb' +load 'knx_value.rb' + +stomp_url = 'stomp://localhost' +thrift_url = 'thrift://localhost' + +ZWT = Ansible::ZWave_Transceiver.new(stomp_url, thrift_url) +ZWT.manager.SendAllValues +sleep(2) + +Switch = AnsibleValue[ + :_nodeId => 2, + :_genre => OpenZWave::RemoteValueGenre::ValueGenre_Basic][0] +Dimmer = AnsibleValue[ + :_nodeId => 5, + :_genre => OpenZWave::RemoteValueGenre::ValueGenre_Basic][0] +DimmerAbsolute = AnsibleValue[ + :_nodeId => 5, + :_genre => OpenZWave::RemoteValueGenre::ValueGenre_User, + :_commandClassId => 38, #SWITCH_MULTILEVEL + :_valueIndex => 0][0] + +KNX = Ansible::KNX::KNX_Transceiver.new("ip:192.168.0.10") +KNX.declare_callback(:onKNXtelegram) { | sender, cb, frame | + puts Ansible::KNX::APCICODES[frame.apci] + " packet from " + + addr2str(frame.src_addr) + " to " + addr2str(frame.dst_addr, frame.daf) + + " priority=" + Ansible::KNX::PRIOCLASSES[frame.prio_class] + if (frame.apci == 2) then # A_GroupValue_Write + AnsibleValue[:groups => [frame.dst_addr]].each { |v| + #puts "updating knx value #{v} from frame #{frame.inspect}" + v.update_from_frame(frame) + } + end +} + + +AnsibleValue.insert( Ansible::KNX::KNXValue_DPT1.new(KNX, "1/0/20") ).declare_callback(:onUpdate) { |sender, cb, args| + puts "KNX value 1/0/20 updated! args=#{args}" + Tree.set(args) # FIXME convert value domains +} +Switch.declare_callback(:onUpdate) { | sender, cb, args| + puts "ZWave Switch HAS CHANGED #{args}" + knxval = sender.current_value == 0 ? 0 : 1 + KNX.send_apdu_raw(str2addr("1/0/21"), [0, 0x80 | knxval]) +} + + +AnsibleValue.insert( Ansible::KNX::KNXValue_DPT1.new(KNX, "1/0/40") ).declare_callback(:onUpdate) { |sender, cb, args| + puts "KNX value 1/0/40 updated! args=#{args}" + Dimmer.set(args) # FIXME convert value domains +} +Dimmer.declare_callback(:onUpdate) { | sender, cb, args| + puts "ZWave DimerOnOff HAS CHANGED #{args}" + knxval = sender.current_value == 0 ? 0 : 1 + KNX.send_apdu_raw(str2addr("1/0/41"), [0, 0x80 | knxval]) +} + + +AnsibleValue.insert( Ansible::KNX::KNXValue_DPT5.new(KNX, "1/0/42") ).declare_callback(:onUpdate) { |sender, cb, args| + puts "KNX value 1/0/42 updated! args=#{args}" + zwval = sender.current_value * 99 / 255 + DimmerAbsolute.set(zwval.round) # FIXME convert value domains +} +DimmerAbsolute.declare_callback(:onUpdate) { | sender, cb, args| + puts "ZWave DimmerAbsolute HAS CHANGED #{args}" + knxval = sender.current_value * 255 / 99 + KNX.send_apdu_raw(str2addr("1/0/43"), [0, 0x80, knxval.round]) ## NOTICE apdu +} diff --git a/transceiver.rb b/transceiver.rb index b340bef..e5509e1 100644 --- a/transceiver.rb +++ b/transceiver.rb @@ -36,7 +36,7 @@ def initialize() begin run() rescue Exception => e - puts "----#{self.class.name.upcase} EXCEPTION----" + puts "----#{self.class.name.upcase} EXCEPTION: #{e} ----" puts "backtrace:\n\t" + e.backtrace.join("\n\t") end } diff --git a/zwave/zwave_transceiver.rb b/zwave/zwave_transceiver.rb index 93446b8..557b0cb 100644 --- a/zwave/zwave_transceiver.rb +++ b/zwave/zwave_transceiver.rb @@ -125,9 +125,9 @@ def init_thrift() puts "Thrift: New heartbeat thread, #{Thread.current}" # aargh, ugly heartbeat while (@thrift_ok) do - manager_send(:GetControllerNodeId, HomeID) - #puts 'ping...' sleep(1) + #puts 'ping...' + manager_send(:GetControllerNodeId, HomeID) end puts "Thrift: heartbeat thread exiting, #{Thread.current}" } @@ -230,11 +230,8 @@ def notification_Type_ValueRemoved(nodeId, value) def notification_Type_ValueChanged(nodeId, value) # A node value has been updated from the Z-Wave network. */ puts "Notification: ValueChanged #{value}" - # get new state - # value.trigger_change_monitor - value.get - #OpenZWave::ValueID.mark_node_dirty(notification_node) - #OpenZWave::ValueID.trigger_change_monitor(notification_node) + # notify the value + value.changed end def notification_Type_Group(nodeId, value) @@ -306,21 +303,20 @@ def notification_Type_EssentialNodeQueriesComplete(nodeId, value) # The queries on a node that are essential to its operation have been completed. The node can now handle incoming messages. */ puts "Notification: EssentialNodeQueriesComplete (node:#{nodeId})" puts "==> marking node #{nodeId} as refreshed" - OpenZWave::RefreshedNodes[nodeId] = true + #OpenZWave::RefreshedNodes[nodeId] = true end def notification_Type_NodeQueriesComplete(nodeId, value) # All the initialisation queries on a node have been completed. */ puts "Notification: NodeQueriesComplete (node:#{nodeId})" - if OpenZWave::RefreshedNodes[nodeId] then + OpenZWave::RefreshedNodes[nodeId] = true + Thread.new { AnsibleValue[:_nodeId => nodeId].each { |val| - val.get + val.get() } - # mark node as not refreshed, meaning all calls to GetValue - # should be taken with a grain of salt - puts "==> marking node #{nodeId} as NOT refreshed" + #sleep(3) # give me 3 secs to get the values!!! OpenZWave::RefreshedNodes[nodeId] = false - end + } end def notification_Type_AwakeNodesQueried(nodeId, value) diff --git a/zwave/zwave_value.rb b/zwave/zwave_value.rb index 0d276fe..b71b29c 100644 --- a/zwave/zwave_value.rb +++ b/zwave/zwave_value.rb @@ -64,7 +64,10 @@ class ValueID < RemoteValueID # equality checking def == (other) - return ((@_homeId == other._homeId) and (@value_id == other.value_id)) + return ( + other.is_a?(ValueID) and + (@_homeId == other._homeId) and (@value_id == other.value_id) + ) end # initialize ValueID by home and value id (both hex strings) @@ -123,17 +126,12 @@ def get() end result = @transceiver.manager_send(operation, self) if result and result.retval then - puts "get() result=#{result.o_value}, prev=#{@previous_value.inspect} curr=#{@current_value.inspect} Refreshed=#{RefreshedNodes[@_nodeId]}" + puts "get() result=#{result.o_value}, curr=#{@current_value.inspect} Refreshed=#{RefreshedNodes[@_nodeId]}" # call succeeded, let's see what we got from OpenZWave - if (@current_value == result.o_value) and not RefreshedNodes[@_nodeId] then - #ZWave peculiarity: we got a ValueChanged event, but the value - # reported by OpenZWave is unchanged. Thus we need to RefreshNodeInfo - @poll_delayed = true - trigger_change_monitor - end - update(result.o_value) fire_callback(:onAfterGet) - return(result) + # update the current value and return + # the new current value to our callers + return(update(result.o_value)) else raise "value #{self}: call to #{operation} failed!!" end @@ -163,19 +161,37 @@ def set(new_val) new_val = new_val ? 1 : 0 end if result = @transceiver.manager_send(operation, self, new_val) then - update(new_val) fire_callback(:onAfterSet) + # update the current value + update(new_val) return(result) else raise "SetValue failed for #{self}" end end - + # called by the transceiver on all values that received a ValueChanged + # notification. NOTICE! we need to do some polling if the value returned + # by the library is the same... + def changed + result = get() + if (@current_value == result) and not OpenZWave::RefreshedNodes[@_nodeId] then + #ZWave peculiarity: we got a ValueChanged event, but the value + # reported by OpenZWave is unchanged. Thus we need to poll the + # device using :RequestNodeDynamic, wait for NodeQueriesComplete + # then re-get the value + trigger_change_monitor unless @poll_delayed + else + # update the current value + update(result) + end + end + # Zwave value notification system only informs us about a # value being changed (ie by manual operation or by an # external command). def trigger_change_monitor + @poll_delayed = true unless @@NodesPolled[@_nodeId] then @@NodesPolled[@_nodeId] = true # spawn new polling thread @@ -183,12 +199,16 @@ def trigger_change_monitor puts "==> spawning trigger change monitor thread #{Thread.current} for node: #{@_nodeId}<==" begin fire_callback(:onChangeMonitorStart, @_nodeId) - # request node status update + # request node status update after 1 sec sleep(1) - @transceiver.manager_send(:RefreshNodeInfo, Ansible::HomeID, @_nodeId) + @transceiver.manager_send(:RequestNodeDynamic, Ansible::HomeID, @_nodeId) + #@transceiver.manager_send(:RefreshNodeInfo, Ansible::HomeID, @_nodeId) + sleep(1) + fire_callback(:onChangeMonitorComplete, @_nodeId) puts "==> trigger change monitor thread (#{Thread.current} ENDED<==" @@NodesPolled[@_nodeId] = false + @poll_delayed = false rescue Exception => e puts "#{e}:\n\t" + e.backtrace.join("\n\t") end