Fancy Gap HTRN

My Personal Playground

  • Increase font size
  • Default font size
  • Decrease font size
Home Home Automation
Home Automation

My Insteon interface

As part of the Insteon software I created.  I also created an Insteon component and plugin for Joomla CMS.

This tools is designed to configure my Insteon system and to allow for user access.

The Joomla system then uses various templates to display on various devices.

Here is a standard browser view.

web

Here is a view on my Android device

android

 

Freeswitch

Freeswitch is an open source telephony systems. In using it, it quickly became apparent to me that I needed a GUI tool to manage the configuration and provide user productivity tools.

What I found after searching is that there are not many options.

  • FusionPBX really the only usable tools, but I found it had limitations. Mainly, around being a generic CMS.
  • FreePBX, which is a great asterisk tool, is working to develop a Freeswitch version. It show great promise, but in my opinion is way to complicated to extend.

This was started, because I need a CMS as the glue to connect all my systems and content. I have been using Joomla for many years, an so this was an easy fit.

I created a component for Joomla and publish it for general use at some point.  This work is a standard addon to Joomla that provides a backend/administrative interface for configuration, and a frontend interface that provides productivity tools for end users.

 

Insteon 2412n controller

The following demonstrates the ability to use the Smarthome Smartlinc network controller, similar to a dedicated PLM.

I recently saw a post on the Smarthome forum that uncovered a previously undocument method to communicate directly with the PLM embedded with in the Smartlinc controller.

The Smartlinc listens on TCP port 9761 and allows bi-directional communication. 

Insteon.rb  Main program

#!/usr/bin/env ruby
require 'rubygems'
require 'thread'
require 'logger'
require '/usr/src/insteon/Sunrise.rb'
require 'pp'
require "mysql"
require 'net/smtp'
require '/usr/src/insteon/ControllerPLM.rb'
require '/usr/src/insteon/insteonDeviceTypes.rb'
require '/usr/src/insteon/insteonConfig.rb'

#$log.level = Logger::DEBUG

$log.info('Starting...')


$mysql = Mysql::new($dbhost, $dbusername, $dbpassword, $dbtable)

$recvqueue = Queue.new
$sendqueue = Queue.new

$controller = ControllerPLM.new($chost,$cport,$cusername,$cpassword,$shost,$sport,$susername,$spassword)
producer,webserver = $controller.start
producer.priority = 2
producer.abort_on_exception = true
webserver.abort_on_exception = true


def parse_buffstatus(e)

   logdata = "Message: #{e['Description']}(#{e['Event']}) "

   case e['Event'] 
      when '0250' # received standard message
         logdata += "From: #{e['From']} "
         logdata += "To: #{e['To']} "
         logdata += "Flags:[" + parse_flag(e['Flags']) + '] '
         logdata += "Command: #{e['Command']} "
         if e['Flags'].hex & 32 == 32
            query = "SELECT * from jos_insteondevices WHERE device = '#{e['From']}'"
            $log.debug(query)
            res = $mysql.query(query)
            if res.num_rows() > 0
               r = res.fetch_hash()
               case r['type'][0..1]
                  when '01', '02'
                     #if  e['Command'][0..1] == '00' or e['Command'][0..1] == '11' or e['Command'][0..1] == '13'
                        if  e['Command'][2..3].hex > 0x00
                           query = "UPDATE jos_insteondevices SET state='ON',brightness='#{hextopercent(e['Command'][2..3])}',lastupdate=NOW() WHERE device = '#{e['From']}'"
                           $log.debug(query)
                           res = $mysql.query(query)
                        else
                           query = "UPDATE jos_insteondevices SET state='OFF',brightness='#{hextopercent(e['Command'][2..3])}',lastupdate=NOW() WHERE device = '#{e['From']}'"
                           $log.debug(query)
                           res = $mysql.query(query)
                        end
                     #end
                     if  e['Command'][0..1] == '15' or e['Command'][0..1] == '16'
                         $controller.SendLightStatus(e['From'],0)
                     end
                  when '10'
                     if  e['Command'][0..1] == '11'
                        query = "UPDATE jos_insteondevices SET state='ON',brightness='N/A',lastupdate=NOW() WHERE device = '#{e['From']}'"
                        $log.debug(query)
                        res = $mysql.query(query)
                     else                                                                
                        query = "UPDATE jos_insteondevices SET state='OFF',brightness='N/A',lastupdate=NOW() WHERE device = '#{e['From']}'"
                        $log.debug(query)
                        res = $mysql.query(query)
                     end
               end
            end
         end   
         if e['Flags'].hex & 192 == 192
            query = "SELECT * from jos_insteondevices WHERE device = '#{e['From']}'"
            $log.debug(query)
            res = $mysql.query(query)
            if res.num_rows() > 0
               r = res.fetch_hash()
               case r['type'][0..3]
                  when '1001'
                     if  e['Command'][0..1] == '11'
                        query = "UPDATE jos_insteondevices SET state='ON',brightness='N/A',lastupdate=NOW() WHERE device = '#{e['From']}'"
                        $log.debug(query)
                        res = $mysql.query(query)
                     else                                                                
                        query = "UPDATE jos_insteondevices SET state='OFF',brightness='N/A',lastupdate=NOW() WHERE device = '#{e['From']}'"
                        $log.debug(query)
                        res = $mysql.query(query)
                     end
               end
            end
         end   
         if e['Flags'].hex & 192 == 192
            #query = "SELECT * from jos_insteondevices WHERE todeviceid = '#{e['From']}' and devicegroup = '#{e['To'][-2..-1]}'"
            #$log.debug(query)
            #res = $mysql.query(query)
            #if res.num_rows() > 0
            #   r = res.fetch_hash()
            #   case r['type'][0..3]
            #      when '0005'
            #         case e['Command'][0..1]
            #            when '11'
            #               query = "UPDATE jos_insteondevices SET state='ON' WHERE todeviceid = '#{e['From']}' and devicegroup = '#{e['To'][-2..-1]}'"
            #               $log.debug(query)
            #               #res = $mysql.query(query)
            #            when '13'
            #               query = "UPDATE jos_insteondevices SET state='OFF' WHERE todeviceid = '#{e['From']}' and devicegroup = '#{e['To'][-2..-1]}'"
            #               $log.debug(query)
            #               #res = $mysql.query(query)
            #      end
            #   end
            #end
         end
        when '0251' # received extended message
                logdata +=  "From: #{e['From']} "
                logdata += "To: #{e['To']} "
                logdata += "Flags:[" + parse_flag(e['Flags']) + '] '
                logdata += "Command: #{e['Command']} "
                logdata += "Extended: #{e['Extended']} "
        when '0253' # ALL-Linking Completed
                logdata += "From: #{e['From']} "
                logdata += "Group: #{e['To']} "
                logdata += "Link Code: "
                case e['Linkcode']
                  when '00'
                    logdata += "IM is a Responder"
                  when '01'
                    logdata += "IM is a Controller"
                  when 'FF'
                    logdata += "ALL-Link to the device was deleted"
                  else
                    logdata += "Unknown"
                end
                logdata += "Cat: #{e['Category']} "
                logdata += "Sub Cat: #{e['Subcategory']} "
                logdata += "Status: #{e['Status']}"
        when '0254' # Button Event Report
                logdata += "Status: "
                case e['Status']
                  when '02'
                    logdata += "IM SET Button tapped"
                  when '03'
                    logdata += "IM SET Button held"
                  when '04'
                    logdata += "IM SET Button released after hold"
                  when '12'
                    logdata += "IM Button 2 tapped"
                  when '13'
                    logdata += "IM Button 2 held"
                  when '14'
                    logdata += "IM Button 2 released after hold"
                  when '22'
                    logdata += "IM Button 3 tapped"
                  when '23'
                    logdata += "IM Button 3 held"
                  when '24'
                    logdata += "IM Button 3 released after hold"
                  else
                    logdata += "Unknown"
                end
        when '0256' # ALL-Link Cleanup Failure Report
                logdata += "Group: #{e['To']} "
                logdata += "Device: #{e['From']} "
        when '0257' # All-Link Report
                logdata += "Flags:[" + parse_flag(e['Flags']) + '] '
                logdata += "Group: #{e['To']} "
                logdata += "Device: #{e['From']} "
                logdata += "Data: #{e['Command']} "
        when '0258' # ALL-Link Cleanup Status Report
                logdata += "Status: #{e['Status']}"
        when '0260' # Get IM Info
                logdata += "Device: #{e['From']} "
                logdata += "Cat: #{e['Category']} "
                logdata += "Sub Cat: #{e['Subcategory']} "
                logdata += "Version: #{e['Version']} "
                logdata += "Status: #{e['Status']}"
        when '0261' # Scene Command
                logdata += "Scene: #{e['From']} "
                logdata += "Command: "
                case e['Command']
                  when '11'
                    logdata += "On"
                  when '12'
                    logdata += "Fast On"
                  when '13'
                    logdata += "Off"
                  when '14'
                    logdata += "Fast Off"
                  else
                    logdata += "Unknown"
                end
                logdata += " Result: #{e['Status']}"
        when '0262' # Send Message                                         
                logdata += "To: #{e['To']} "
                logdata += "Flags:[" + parse_flag(e['Flags']) + '] '
                logdata += "Command: #{e['Command']} "
                if e['Flags'].hex & 16 == 16
                  logdata += e['Extended'] + ' '
                end
                logdata += "Status: "
                case e['Status']
                  when '06'
                    logdata += "Ack"
                  when '15'
                    logdata += "Nack"
                  when 'FF'
                    logdata += "Fail"
                  else
                    logdata += "Unknown"
                end
        when '0265' # Cancel All-linking
                logdata += "Status: #{e['Status']} "
        when '0266' # Get IM Info
                logdata += "Cat: #{e['Category']} "
                logdata += "Sub Cat: #{e['Subcategory']} "
                logdata += "Version: #{e['Version']} "
                logdata += "Status: #{e['Status']}"
        when '0269' # Get First ALL-Link
                logdata += "Status: #{e['Status']} "
        when '026A' # Get Next ALL-Link
                logdata += "Status: #{e['Status']} "
        when '026B' # Set IM Configuration
                logdata += "Command: #{e['Command']} "
                logdata += "Status: #{e['Status']} "
        when '026F' # Manage ALL-Link Record
                logdata += "Command: #{e['Command']} "
                logdata += "Flags: #{e['Command']} "
                logdata += "Group: #{e['To']} "
                logdata += "Device: #{e['From']} "
                logdata += "Cat: #{e['Category']} "
                logdata += "Sub Cat: #{e['Subcategory']} "
                logdata += "Version: #{e['Version']} "
                logdata += "Status: #{e['Status']} "
        else
                logdata += "Command: #{e['Command']} "
  end
  $log.info(logdata)
  rescue 
      $log.error("Parse Buffer error #{$!}  #{$@[0]} Line: #{$.}")
end              

def parse_flag(flag)
    retval = flag.hex
    f = ''
    if retval & 128 == 128
        f = f +  'B'
    else
        f = f +  'b'
    end
    if retval & 64  == 64
        f = f +  'G'
    else
        f = f +  'g'
    end
    if retval & 32 == 32
        f = f +  'A'
    else
        f = f +  'a'
    end
    if retval & 16  == 16
         f = f +  'E'
   else
        f = f +  'e'
    end
    f = f + ' HL:' 
    if retval & 8 == 8
        f = f +  '1'
    else
        f = f +  '0'
    end
    if retval & 4 == 4
        f = f +  '1'
    else
        f = f +  '0'
    end
    f = f + ' MH:' 
    if retval & 2 == 2
        f = f +  '1'
    else
        f = f +  '0'
    end
    if retval & 1 == 1
        f = f +  '1'
    else
        f = f +  '0'
    end
    
    return f
end
def percenttohex(level)
  # convert from percent to byte
  return "%X" %(level.to_i * 2.55).to_i
end

def hextopercent(level)
  return (level.hex.to_i / 2.55).to_i
end

def checkSchedule(id,time=Time.now)
   date = Date.parse(time.to_s)
   calc = SolarEventCalculator.new(date, BigDecimal.new($long), BigDecimal.new($lat))  
   localSunrise = calc.compute_official_sunrise($timezone) 
   localSunset = calc.compute_official_sunset($timezone)  
   sr = Time.parse(localSunrise.to_s)
   ss = Time.parse(localSunset.to_s)
   
   logdata = ""
   retval = false
   query = "SELECT * from jos_insteonschedules as c WHERE c.id = '#{id}' and c.published = 1"
   $log.debug(query)
   sres = $mysql.query(query)
   if sres.num_rows() > 0
      s = sres.fetch_hash()
      logdata += "Schedule: #{s['description']}, "
      daymatch = false
      case time.wday()
      when 0
         daymatch = true if s['dsun'] == '1'
      when 1
         daymatch = true if s['dmon'] == '1' 
      when 2
         daymatch = true if s['dtue'] == '1'
      when 3
         daymatch = true if s['dwed'] == '1'
      when 4
         daymatch = true if s['dthu'] == '1'
      when 5
         daymatch = true if s['dfri'] == '1'
      when 6
         daymatch = true if s['dsat'] == '1'
      end
      if daymatch
         logdata += "Days matched, "
         case s['timecode'] 
         when '0' # timecode 0 = before time
            logdata += "Schedule before time "
            stime = Time.parse("#{time.strftime("%Y-%m-%d")} #{s['stime']}")
            #$log.info("Times ct #{time.strftime("%Y/%m/%d %I:%M%p")} at #{stime.strftime("%Y/%m/%d %I:%M%p")} ")
            if (time - stime) <= 0
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '1' # timecode 1 = After time
            logdata += "Schedule After time "
            stime = Time.parse("#{time.strftime("%Y-%m-%d")} #{s['stime']}")
            #$log.info("Times ct #{time.strftime("%Y/%m/%d %I:%M%p")} at #{stime.strftime("%Y/%m/%d %I:%M%p")} ")
            if time >= stime
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '2' # timecode 2 = at Time
            logdata += "Schedule AT time "
            stime = Time.parse("#{time.strftime("%Y-%m-%d")} #{s['stime']}")
            #$log.info("Times ct #{time.strftime("%Y/%m/%d %I:%M%p")} at #{stime.strftime("%Y/%m/%d %I:%M%p")} diff #{time - stime}")
            diff = time - stime
            if (diff >= 0 && diff <= 0.5)
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '3' # timecode 3 = before sunrise
            logdata += "Schedule before sunrise "
            
            if time >= ss
               sr += 60*60*24  # add day for sunrise tomorrow
            end
            #$log.info("Times sr #{sr.strftime("%Y/%m/%d %I:%M%p")} ss #{ss.strftime("%Y/%m/%d %I:%M%p")} ")
            if time <= sr - (s['timeoffset'].to_i * 60)
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '4' # timecode 4 = after sunrise
            logdata += "Schedule after sunrise "
            if time >= ss
               sr += 60*60*24  # add day for sunrise tomorrow
            end
            #$log.info("Times sr #{sr.strftime("%Y/%m/%d %I:%M%p")} ss #{ss.strftime("%Y/%m/%d %I:%M%p")} ")
            if time >= sr + (s['timeoffset'].to_i * 60)
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '5' # timecode 5 = before sunset
            logdata += "Schedule before sunset "
            
            if time <= sr
               ss -= 60*60*24  # subtract day for sunrise yesterday
            end
            #$log.info("Times sr #{sr.strftime("%Y/%m/%d %I:%M%p")} ss #{ss.strftime("%Y/%m/%d %I:%M%p")} ")
            if time <= ss - (s['timeoffset'].to_i * 60)
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
         when '6' # timecode 6 = after sunset
            logdata += "Schedule after sunset "
            
            if time <= sr
               ss -= 60*60*24  # subtract day for sunrise yesterday
            end
            #$log.info("Times sr #{sr.strftime("%Y/%m/%d %I:%M%p")} ss #{ss.strftime("%Y/%m/%d %I:%M%p")} ")
            if time >= ss + (s['timeoffset'].to_i * 60)
               retval = true
               #$log.info("Time match #{time} #{stime}")
            end
        end
      end
      logdata += "Matched" 
      #pp s
   end
   return retval, logdata
end

def sendcommands(id)
   query  = "SELECT * from jos_insteoncommands as c WHERE c.eventid = #{id} and c.published = 1 ORDER BY eventid, ordering,id"
   $log.debug(query)
   cres = $mysql.query(query)
   if cres.num_rows() > 0
      clearqueue = true
      cres.each_hash() {|sd|
         if sd['commandtype'] == '0'  # insteon command
            command = "#{sd['commandprefix']}#{sd['commanddevice']}#{sd['commandflag']}#{sd['command']}"
            if clearqueue
               $controller.ClearSendQueue(sd['commanddevice'])
               clearqueue = false
            end
            $log.info("Sending insteon command #{sd['description']} #{sd['command']} delay #{sd['delay']}")
            $controller.SendPLM(command,desc='Schedule command',sd['delay'].to_i)
         end   
         if sd['commandtype'] == '1'  # email command
            $log.info("Sending email #{sd['description']} to #{sd['email']}")
            time = Time.new
            msg  = "From: #{$emailfromname} <#{$emailfrom}>\n"
            msg += "To: <#{sd['email']}>\n"
            msg += "Subject: #{sd['description']}\n"
            msg += "\n#{sd['description']} at #{time.strftime("%Y/%m/%d %I:%M%p")}\n"
    
            Net::SMTP.start($emailserver) do |smtp|
               smtp.send_message msg, $emailfrom, sd['email']
               smtp.finish
              end
         end   
      }
   end
end

def execute_message(m)
   time = Time.now
   if m == nil
      query  = "SELECT * from jos_insteonevents as c "
      query += "WHERE c.eventtype = 1 "
      query += "and c.published = 1"
   else
      query  = "SELECT * from jos_insteonevents as c "
      query += "WHERE c.from = '#{m['From']}' and c.to = '#{m['To']}' and '#{m['Command']}' LIKE c.command and c.eventtype = 0 "
      query += "and c.published = 1"
   end
   $log.debug(query)
   eres = $mysql.query(query)
   if eres.num_rows() > 0
      eres.each_hash() {|e|
         logdata = "Event:#{e['description']}\n"
         query = "SELECT * from jos_insteonscheduledetail as c WHERE c.eventid = #{e['id']} and c.published = 1"
         $log.debug(query)
         sdres = $mysql.query(query)
         if sdres.num_rows() > 0
            match = true
            sdres.each_hash() {|sd|
               if match
                  match,slog = checkSchedule(sd['schedulesid'])
                  logdata += "   Schedule Detail:#{sd['description']}\n      #{slog}\n" if match
               end
            }
         else
            match = true
         end
         if match
            logdata.split("\n").each { |l|
               $log.info(l)
            }
            sendcommands(e['id'])
         end   
      }
   end
end


def datetest
$times = []
$times << Time.parse("2010-11-10 01:00")
$times << Time.parse("2010-11-10 02:00")
$times << Time.parse("2010-11-10 03:00")
$times << Time.parse("2010-11-10 04:00")
$times << Time.parse("2010-11-10 05:00")
$times << Time.parse("2010-11-10 06:00")
$times << Time.parse("2010-11-10 07:00")
$times << Time.parse("2010-11-10 08:00")
$times << Time.parse("2010-11-10 09:00")
$times << Time.parse("2010-11-10 10:00")
$times << Time.parse("2010-11-10 11:00")
$times << Time.parse("2010-11-10 12:00")
$times << Time.parse("2010-11-10 13:00")
$times << Time.parse("2010-11-10 14:00")
$times << Time.parse("2010-11-10 15:00")
$times << Time.parse("2010-11-10 16:00")
$times << Time.parse("2010-11-10 17:00")
$times << Time.parse("2010-11-10 18:00")
$times << Time.parse("2010-11-10 19:00")
$times << Time.parse("2010-11-10 20:00")
$times << Time.parse("2010-11-10 21:00")
$times << Time.parse("2010-11-10 22:00")
$times << Time.parse("2010-11-10 23:00")
$times << Time.parse("2010-11-10 00:00")

query = "SELECT * from jos_insteonschedules as c WHERE c.published = 1"
$log.debug(query)
sdres = $mysql.query(query)
if sdres.num_rows() > 0
   sdres.each_hash() {|sd|
      $times.each {|t|
         $log.info("Schedule #{sd['description']} Time #{t.strftime("%Y/%m/%d %I:%M%p")}")
         match=checkSchedule(sd['id'],t)
         if match
            $log.info("Matched #{sd['description']} \n")
         else
            $log.info("Missed  #{sd['description']} \n")
         end
      }
   }
end

exit
end

sleep(2)  #wait for the controller thread to get going
$controller.clearRecvQueue()

$log.info("Sending IM Monitor Mode")
$controller.SendMonitor()

#get status of the lights
query = "SELECT * from jos_insteondevices as c WHERE c.published = 1"
$log.debug(query)
res = $mysql.query(query)
if res.num_rows() > 0
    res.each_hash() {|d|
        case d['type'][0..1]
        when '01', '02'
            command = "0262#{d['device']}0F1900"
            $log.info("Sending insteon status #{command} delay 0")
            $controller.SendPLM(command,desc='Initialize',0)
        end
    }
end

scheduletime = Time.now
while true
  $log.debug("Producer #{producer.status} Webserver #{webserver.status} Main #{Thread.main.status}")
  #puts $recvqueue.length()
  if producer.status == 'run'
  end
  if $recvqueue.length() > 0
    #$log.info("event")
    message = $recvqueue.pop()
    parse_buffstatus(message)
    execute_message(message)
  else
   if Time.now > scheduletime + 15
        #$log.info("periodic")
        execute_message(nil)
        scheduletime = Time.now
    end    
  end
  sleep(0.1)
end
$log.close
ControllerPLM.rb
require 'timeout'
require 'thread'
require 'logger'
require 'socket'  

class HttpServer
   def initialize(session, request)
      @session = session
      @request = request
   end

   def serve()
      if @request =~ /GET .* HTTP.*/
         fileName = @request.gsub(/GET \//, '').gsub(/ HTTP.*/, '')
      end
      fileName = fileName.strip
      begin
         @session.print "HTTP/1.1 200/OK\r\nServer: Insteon\r\nContent-type: text/plain\r\n\r\n"
         @session.write(fileName)
      ensure
         @session.close
      end
      return fileName
   end
end

class ControllerPLM
   def initialize(host, port, username, password,shost, sport, susername, spassword)
      @buffer = ''
      @host = host
      @port = port
      @username = username
      @password = password
      @shost = shost
      @sport = sport
      @susername = susername
      @spassword = spassword
      @socket = nil
   end

   def start
      producer = Thread.new {
        @socket = TCPSocket.new( @host, @port )  
         while true
           if $sendqueue.length() > 0
               message = $sendqueue.pop()
               if message['Time'] <= Time.now
                  WritePLM(message['Path'],message['ErrorDesc'])
               else
                  $sendqueue.push(message)
               end                                              
            end
            check_controller()
            sleep(0.2) # don't go to fast or you will over run the PLM
         end     
      }
      webserver = Thread.new {
         server = TCPServer.new(@shost, @sport)

         loop do
            session = server.accept
            request = session.gets
            logStr =  "Web request: #{session.peeraddr[2]} (#{session.peeraddr[3]}) #{request.strip}"
            $log.info(logStr)

            Thread.start(session, request) do |session, request|
               command = HttpServer.new(session, request).serve()
               if command[0..1] == '02'
                  $log.info("Sending Web Server command #{command} ")
                  SendPLM(command,'Web Command')
               end
               sleep(0.1)
            end
         end
      }
      return producer, webserver
   end

   def recv_controller()
      retval = ''
      begin
         Timeout.timeout 1 do
            recv = @socket.recv( 1024 )
            retval = recv.unpack('H*').join.upcase
         end
      rescue Timeout::Error
         #$log.error("recv_controller timeout")
         retval = ''
      end
      return retval
   end

   def check_controller()
      @buffer += recv_controller()
      if !@buffer.empty?
        @buffer = $controller.queue_messages(@buffer)
      end
   end

   def queue_messages(buff)
      bs = ''
      buff = buff.gsub("0250", ",0250")
      buff = buff.gsub("0251", ",0251")
      buff = buff.gsub("0252", ",0252")
      buff = buff.gsub("0253", ",0253")
      buff = buff.gsub("0254", ",0254")
      buff = buff.gsub("0255", ",0255")
      buff = buff.gsub("0256", ",0256")
      buff = buff.gsub("0257", ",0257")
      buff = buff.gsub("0258", ",0258")

      buff = buff.gsub("0260", ",0260")
      buff = buff.gsub("0261", ",0261")
      buff = buff.gsub("0262", ",0262")
      buff = buff.gsub("0263", ",0263")
      buff = buff.gsub("0264", ",0264")
      buff = buff.gsub("0265", ",0265")
      buff = buff.gsub("0266", ",0266")
      buff = buff.gsub("0267", ",0267")
      buff = buff.gsub("0268", ",0268")
      buff = buff.gsub("0269", ",0269")
      buff = buff.gsub("026A", ",026A")
      buff = buff.gsub("026B", ",026B")
      buff = buff.gsub("026C", ",026C")
      buff = buff.gsub("026D", ",026D")
      buff = buff.gsub("026E", ",026E")
      buff = buff.gsub("026F", ",026F")

      buff = buff.gsub("0270", ",0270")
      buff = buff.gsub("0271", ",0271")
      buff = buff.gsub("0272", ",0272")
      buff = buff.gsub("0273", ",0273")
      
      time = Time.new

      e = buff.split(',')
      e.each_index {|i|
      if !e[i].empty?
         message = Hash.new
         message['Time'] = time
         message['Event'] = ''
         message['Description'] = ''
         message['From'] = ''
         message['To'] = ''
         message['Flags'] = ''
         message['Command'] = ''
         message['Extended'] = ''
         message['Category'] = ''
         message['Subcategory'] = ''
         message['Version'] = ''
         message['Status'] = ''
         message['Linkcode'] = ''

         event = e[i][0..3]
         case event 
         when '0250' # received standard message
            arr = e[i].scan(/(....)(......)(......)(..)(....)/)
            if arr.length == 1 and arr[0].length == 5
               message['Event'] = arr[0][0]
               message['Description'] = 'Std Message'
               message['From'] = arr[0][1]
               message['To'] = arr[0][2]
               message['Flags'] = arr[0][3]
               message['Command'] = arr[0][4]
            else
               bs = e[i]
            end
         when '0251' # received extended message
            arr = e[i].scan(/(....)(......)(......)(..)(....)(............................)/)
            if arr.length == 1 and arr[0].length == 6
               message['Event'] = arr[0][0]
               message['Description'] = 'Ext Message'
               message['From'] = arr[0][1]
               message['To'] = arr[0][2]
               message['Flags'] = arr[0][3]
               message['Command'] = arr[0][4]
               message['Extended'] = arr[0][5]
            else
               bs = e[i]
            end
         when '0253' # All Linking Completed
            arr = e[i].scan(/(....)(..)(..)(......)(..)(..)(..)/)
            if arr.length == 1 and arr[0].length == 7
               message['Event'] = arr[0][0]
               message['Description'] = 'All Linking Completed'
               message['Linkcode'] = arr[0][1]
               message['To'] = arr[0][2]
               message['From'] = arr[0][3]
               message['Category'] = arr[0][4]
               message['Subcateory'] = arr[0][5]
               message['Status'] = arr[0][6]
            else
               bs = e[i]
            end
         when '0254' # Button Event Report
            arr = e[i].scan(/(....)(..)/)
            if arr.length == 1 and arr[0].length == 2
               message['Event'] = arr[0][0]
               message['Description'] = 'Button Event Report'
               message['Status'] = arr[0][1]
            else
               bs = e[i]
            end
         when '0256' # ALL-Link Cleanup Failure Report
            arr = e[i].scan(/(....)(..)(......)/)
            if arr.length == 1 and arr[0].length == 3
               message['Event'] = arr[0][0]
               message['To'] = arr[0][1]
               message['From'] = arr[0][2]
               message['Description'] = 'ALL-Link Cleanup Failure Report'
            else
               bs = e[i]
            end
         when '0257' # All-Link Report
            arr = e[i].scan(/(....)(..)(..)(......)(......)/)
            if arr.length == 1 and arr[0].length == 5
               message['Event'] = arr[0][0]
               message['Description'] = 'All-Link Report'
               message['From'] = arr[0][3]
               message['To'] = arr[0][2]
               message['Flags'] = arr[0][1]
               message['Command'] = arr[0][4]
            else
               bs = e[i]
            end
         when '0258' # ALL-Link Cleanup Status Report
            arr = e[i].scan(/(....)(.*)/)
            if arr.length == 1 and arr[0].length == 2
               message['Event'] = arr[0][0]
               message['Description'] = 'All-Link Cleanup'
               message['Status'] = arr[0][1]
            else
               bs = e[i]
            end
         when '0260' # Get IM Info
            arr = e[i].scan(/(....)(......)(..)(..)(..)(..)/)
            if arr.length == 1 and arr[0].length == 6
               message['Event'] = arr[0][0]
               message['Description'] = 'Get IM Info'
               message['From'] = arr[0][1]
               message['Category'] = arr[0][2]
               message['Subcategory'] = arr[0][3]
               message['Version'] = arr[0][4]
               message['Status'] = arr[0][5]
            else
               bs = e[i]
            end
         when '0261' # Scene Command
            arr = e[i].scan(/(....)(..)(..)(.*)/)
            if arr.length == 1 and arr[0].length == 4
               message['Event'] = arr[0][0]
               message['Description'] = 'Scene Message'
               message['From'] = arr[0][1]
               message['Command'] = arr[0][2]
               message['Status'] = arr[0][3]
            else
               bs = e[i]
            end
         when '0262' # Send Message                                         
            case e[i].length
            when 46  # extended message
               arr = e[i].scan(/(....)(......)(..)(....)(............................)(..)/) 
               if arr.length == 1 and arr[0].length == 6
                  message['Event'] = arr[0][0]
                  message['Description'] = 'Send Message'
                  message['To'] = arr[0][1]
                  message['Flags'] = arr[0][2]
                  message['Command'] = arr[0][3]
                  message['Extended'] = arr[0][4]
                  message['Status'] = arr[0][5]
               end
            when 18  # standard message
               arr = e[i].scan(/(....)(......)(..)(....)(..)/) 
               if arr.length == 1 and arr[0].length == 5
                  message['Event'] = arr[0][0]
                  message['Description'] = 'Send Message'
                  message['To'] = arr[0][1]
                  message['Flags'] = arr[0][2]
                  message['Command'] = arr[0][3]
                  message['Status'] = arr[0][4]
                end
             else
                bs = e[i]
             end
         when '0265' # Cancel All-linking
            arr = e[i].scan(/(....)(..)/)
            if arr.length == 1 and arr[0].length == 2
               message['Event'] = arr[0][0]
               message['Description'] = 'Cancel All-linking'
               message['Status'] = arr[0][1]
            else
               bs = e[i]
            end
         when '0266' # Set Host Device Category
            arr = e[i].scan(/(....)(..)(..)(..)(..)/)
            if arr.length == 1 and arr[0].length == 5
               message['Event'] = arr[0][0]
               message['Description'] = 'Set Host Device Category'
               message['Category'] = arr[0][1]
               message['Subcategory'] = arr[0][2]
               message['Version'] = arr[0][3]
               message['Status'] = arr[0][4]
            else
               bs = e[i]
            end
         when '0269' # Get First ALL-Link
            arr = e[i].scan(/(....)(..)/)
            if arr.length == 1 and arr[0].length == 2
               message['Event'] = arr[0][0]
               message['Description'] = 'Get First ALL-Link'
               message['Status'] = arr[0][1]
            else
               bs = e[i]
            end
         when '026A' # Get Next ALL-Link
            arr = e[i].scan(/(....)(..)/)
            if arr.length == 1 and arr[0].length == 2
               message['Event'] = arr[0][0]
               message['Description'] = 'Get Next ALL-Link'
               message['Status'] = arr[0][1]
            else
               bs = e[i]
            end
         when '026B' # Set IM Configuration
            arr = e[i].scan(/(....)(..)(..)/)
            if arr.length == 1 and arr[0].length == 3
               message['Event'] = arr[0][0]
               message['Command'] = arr[0][1]
               message['Description'] = 'Set IM Configuration'
               message['Status'] = arr[0][2]
            else
               bs = e[i]
            end
         when '026F' # Manage ALL-Link Record
            #                 026F  20  A2  01  13DED1  00  05  38  06
            arr = e[i].scan(/(....)(..)(..)(..)(......)(..)(..)(..)(..)/)
            if arr.length == 1 and arr[0].length == 9
               message['Event'] = arr[0][0]
               message['Command'] = arr[0][1]
               message['Flags'] = arr[0][2]
               message['To'] = arr[0][3]
               message['From'] = arr[0][4]
               message['Category'] = arr[0][5]
               message['Subcategory'] = arr[0][6]
               message['Version'] = arr[0][7]
               message['Description'] = 'Manage ALL-Link Record'
               message['Status'] = arr[0][8]
            else
               bs = e[i]
            end
         else
            bs = e[i]
         end
         if !message['Event'].empty?
            $recvqueue.push(message)
         end
      end
   }
   return bs
   end

   def ClearSendQueue(device)
       tqueue = []
       while $sendqueue.length() > 0
          message = $sendqueue.pop()
          if message['Path'][0..9] == "0262#{device}"
            $log.info("Removing Queued command #{message['Path']} #{message['ErrorDesc']} #{message['Time']}")
          else
            tqueue << message
          end
       end
       tqueue.each {|m|
          $sendqueue.push(m)
       }

   end
   
   def clearRecvQueue()
       while $recvqueue.length() > 0
          message = $recvqueue.pop()
       end
   end
   
   def setclock()
   end

   def sendcommand(device,cmd,delay=0)
      path = "0262#{device}0F#{cmd}"  
      SendPLM(path,'Send Command',delay)
   end

   def sendExtCommand(device,cmd,delay=0)
      path = "0262#{device}1F#{cmd}"  
      SendPLM(path,'Send Extended Command',delay)
   end
   
   def SendGetFirst()
      path = "0269"  
      SendPLM(path,'Send Get First')
   end

   def SendGetNext()
      path = "026A"  
      SendPLM(path,'Send Get Next')
   end

   def SendRemoteSetMSB(device,msb)
      path = "0262#{device}0F28#{msb}"  
      SendPLM(path,'Send Remote Set MSB')
   end

   def SendRemotePeek(device,offset)
      path = "0262#{device}0F2B#{offset}"  
      SendPLM(path,'Send Remote Peek')
   end

   def SendGetFirst()
      path = "0269"  
      SendPLM(path,'Send Get First')
   end

   def SendMonitor()
      path = "026B40"  
      SendPLM(path,'Send Monitor')
   end

   def SendLightStatus(device,delay=10)
      path = "0262#{device}0F1900"  
      SendPLM(path,'Send Light status',delay)
   end

   def SendIdRequest(device)
      path = "0262#{device}0F1000"  
      SendPLM(path,'Send ID Request')
   end

   def SendEngineVersion(device)
      path = "0262#{device}0F0D00"  
      SendPLM(path,'Send Engine Version')
   end

   def SendGetIMinfo()
      path = "0260"  
      SendPLM(path,'Get IM Info')
   end

   def SendManageLink(record)
      path = "026F#{record}"  
      SendPLM(path,'Send Manage Link')
   end

   def SendPing(device)
      path = "0262#{device}0F1F00"  
      SendPLM(path,'Send Ping')
   end

   def ReadAllLinkDatabase(device,address='0000')
      d1  = '00' # unused
      d2  = '00' # read
      d3  = address
      #d5  = '01' # read one record
      d5  = '00' # read all records
      d6  = '00' # unused
      d7  = '00' # unused
      d8  = '00' # unused
      d9  = '00' # unused
      d10 = '00' # unused
      d11 = '00' # unused
      d12 = '00' # unused
      d13 = '00' # unused
      d14 = '00' # unused
      
      path = "0262#{device}1F2F00#{d1}#{d2}#{d3}#{d5}#{d6}#{d7}#{d8}#{d9}#{d10}#{d11}#{d12}#{d13}#{d14}"  
      SendPLM(path,'Read All Link Database')
   end

   def WritePLM(path,desc='Send error')
      # This is the HTTP request we send to fetch a file
      $log.debug("WritePLM #{path}")
      
      line=''
      while !path.empty?
        line += path[0..1].hex.chr
        path = path[2..path.length]
      end

      @socket.write line
      @socket.flush
      #line = ''
      #while !path.empty?
      #   line = path[0..1].hex.chr
      #   retval = $sp.write(line)
      #   path = path[2..path.length]
      #   #print "line #{line.inspect} retval #{retval} path #{path}\n"
      #end
      rescue 
         $log.error("#{desc} error #{$!} #{$@[0]} Line: #{$.}")
         retval = false
         return retval
   end

   def SendPLM(path,desc='Send error',delay=0)
      message = Hash.new
      message['Path'] = path
      message['ErrorDesc'] = desc
      message['Time'] = Time.now + delay
      $log.debug("SendPLM  Path:#{path} Time now:#{Time.now} Time delay:#{Time.now + delay}")
      $sendqueue.push(message)
   end

   def getIMinfo()
      begin
         retval = {}
         clearRecvQueue()
         Timeout.timeout 30 do
            SendGetIMinfo()
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '0260'
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               retval['Address'] = message['From']
               retval['Type'] = "#{message['Category']}#{message['Subcategory']}#{message['Version']}"
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("getIMinfo timeout...retrying")
         retry
      end
   end

   def getRequestID(device)
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 30 do
            SendIdRequest(device)
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '0262' and message['To'] == device
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               recfound = false
               while recfound == false
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0250' and message['From'] == device and message['Flags'].hex & 32 == 0
                        retval = message['To']
                        recfound = true
                     end
                  end
                  Thread.pass
               end
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("getRequestID timeout...retrying")
         retry
      end
   end

   def getEngineVersion(device)
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 30 do
            SendEngineVersion(device)
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '0262' and message['To'] == device
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               recfound = false
               while recfound == false
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0250' and message['From'] == device
                        retval = message['Command'][2..3]
                        recfound = true
                     end
                  end
                  Thread.pass
               end
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("getEngineVersion timeout...retrying")
         retry
      end
   end

   def remoteSpiderV1(device)
      begin
         Timeout.timeout 180 do
            linkrecords = []
            $remoteDBOffsetMSB = 0x0F # initial offset
            $remoteDBOffsetLSB = 0xF8 # initial LSB offset

            clearRecvQueue()
                  
            SendRemoteSetMSB(device,"%02X"%$remoteDBOffsetMSB)

            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '0262' and message['To'] == device
                     if message['Status'] != '06'
                        more = false
                     end
                     ackfound = message['Status']
                  end
               end
            end
            if ackfound == '06'
               recfound = false
               while recfound == false
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0250'
                        morepeek = true
                        while morepeek == true
                           peekrecord = ''
      
                           for x in 0..7
                              SendRemotePeek(device,"%02X"%($remoteDBOffsetLSB + x))
                              ackfound = ''
                              while ackfound != '06' and ackfound != '15'
                                 if $recvqueue.length() > 0
                                    message = $recvqueue.pop()
                                    if message['Event'] == '0262' and message['To'] == device
                                       if message['Status'] != '06'
                                          more = false
                                       end
                                       ackfound = message['Status']
                                    end
                                 end
                                 Thread.pass
                              end
                              if ackfound == '06'
                                 peekfound = false
                                 while !peekfound
                                    if $recvqueue.length() > 0
                                       message = $recvqueue.pop()
                                       if message['Event'] == '0250' and message['From'] == device
                                          peekfound = true
                                          peekrecord = peekrecord + message['Command'][2..3]
                                       end
                                    end
                                    Thread.pass
                                 end
                              end
                           end
                           peekflag = peekrecord[0..1]
                           peekgroup = peekrecord[2..3]
                           peekdevice = peekrecord[4..9]
                           peektype = peekrecord[10..15]
                           address = "#{"%02X"%$remoteDBOffsetMSB}#{"%02X"%$remoteDBOffsetLSB}"
                           $log.info("SpiderRemote device #{device} ALDB Flag:#{peekflag} Group:#{peekgroup} Device:#{peekdevice} Type:#{peektype} Address:#{address}")
                           if (peekflag.hex & 0x02) == 0x00 # High water mark
                              morepeek = false
                           end
                           if (peekflag.hex & 0x80) == 0x80 # Inuse
                              linkrecords << {'To'=>peekdevice,'From'=>device, 'Group'=>peekgroup, 'Flags' => peekflag,'Linkdata'  => peektype,'Address'  => address} 
                           end
                           if $remoteDBOffsetLSB == 0x00
                              #OffsetMSB needs to be decreased, and LSB needs reset.
                              $remoteDBOffsetMSB = $remoteDBOffsetMSB - 1
                              $remoteDBOffsetLSB = 0xFF # the next line will decrease this by 8
                           end
                           # decrease DBOffsetLSB by 0x08
                           $remoteDBOffsetLSB = $remoteDBOffsetLSB - 0x08
                        end
                        recfound = true
                     end
                  end
               end
            end
            return linkrecords
         end
      rescue Timeout::Error
         $log.error("remoteSpider timeout...retrying")
         retry
      end
   end

   def remoteSpider(device)
      begin
         Timeout.timeout 30 do
            linkrecords = []
            clearRecvQueue()

            nextaddress = '0000'            
            more = true                  
            while more
               ReadAllLinkDatabase(device,nextaddress)
               ackfound = ''
               while ackfound != '06' and ackfound != '15'
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0262' and message['To'] == device
                        if message['Status'] != '06'
                           more = false
                        end
                        ackfound = message['Status']
                     end
                  end
                  Thread.pass
               end
               if ackfound == '06'
                  recfound = false
                  while recfound == false
                     if $recvqueue.length() > 0
                        message = $recvqueue.pop()
                        if message['Event'] == '0251' and message['From'] == device
                           peekaddress    = message['Extended'][4..7]
                           nextaddress    = "%04X"%(message['Extended'][4..7].hex - 8)
                           peekflag   = message['Extended'][10..11]
                           peekgroup  = message['Extended'][12..13]
                           peekdevice = message['Extended'][14..19]
                           peektype   = message['Extended'][20..25]
                           $log.info("SpiderRemote device #{device} ALDB Flag:#{peekflag} Group:#{peekgroup} Device:#{peekdevice} Type:#{peektype} Address:#{peekaddress}")
                           if (peekflag.hex & 0x02) == 0x00 # High water mark
                              more = false
                              recfound = true
                           end
                           if (peekflag.hex & 0x80) == 0x80 # Inuse
                              linkrecords << {'To'=>peekdevice,'From'=>device, 'Group'=>peekgroup, 'Flags' => peekflag,'Linkdata'  => peektype,'Address'  => peekaddress} 
                           end
                        end
                        Thread.pass
                     end
                  end
               end
            end
            return linkrecords
         end
      rescue Timeout::Error
         $log.error("remoteSpider timeout...retrying")
         retry
      end
   end

   def scanController()
      begin
         Timeout.timeout 30 do
            linkrecords = []
            clearRecvQueue()
            SendGetFirst()
            more = true
            while more
               ackfound = ''
               while ackfound != '06' and ackfound != '15'
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0269' or message['Event'] == '026A'
                        if message['Status'] != '06'
                           more = false
                        end
                        ackfound = message['Status']
                     end
                  end
                  Thread.pass
               end
               if ackfound == '06'
                  recfound = false
                  while recfound == false
                     if $recvqueue.length() > 0
                        message = $recvqueue.pop()
                        if message['Event'] == '0257'
                           linkrecords << {'To'=>message['From'],'From'=>$controllerAddress, 'Group'=>message['To'], 'Flags' => message['Flags'],'Linkdata'  => message['Command'],'Type'  => message['Command'],'Engine'  => '00'} 
                           recfound = true
                        end
                     end
                     Thread.pass
                  end
               end
               if more 
                  $controller.SendGetNext()
               end
            end
            return linkrecords
         end
      rescue Timeout::Error
         $log.error("remoteSpider timeout...retrying")
         retry
      end
   end

   def ExistsInController(flags,devicegroup,device,linkdata)
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 30 do
            SendManageLink("00#{flags}#{devicegroup}#{device}#{linkdata}")
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '026F' 
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               retval = true
            else
               retval = false
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("Exists In Controller timeout...retrying")
         retry
      end
   end

   def DeleteFromController(flags,devicegroup,device,linkdata)
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 30 do
            SendManageLink("80#{flags}#{devicegroup}#{device}#{linkdata}")
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '026F' 
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               retval = true
            else
               retval = false
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("Delete From Controller timeout...retrying")
         retry
      end
   end

   def AddToController(flags,devicegroup,device,linkdata)
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 30 do
            SendManageLink("20#{flags}#{devicegroup}#{device}#{linkdata}")
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '026F' 
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               retval = true
            else
               retval = false
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("Add To Controller Controller timeout...retrying")
         retry
      end
   end

   def Ping(device)
      retries = 0
      begin
         retval = false
         clearRecvQueue()
         Timeout.timeout 5 do
            SendPing(device)
            ackfound = ''
            while ackfound != '06' and ackfound != '15'
               if $recvqueue.length() > 0
                  message = $recvqueue.pop()
                  if message['Event'] == '0262' and message['To'] == device  and message['Command'] == '1F00'
                     ackfound = message['Status']
                  end
               end
               Thread.pass
            end
            if ackfound == '06'
               recfound = false
               while recfound == false
                  if $recvqueue.length() > 0
                     message = $recvqueue.pop()
                     if message['Event'] == '0250' and message['From'] == device
                        retval = true
                        recfound = true
                     end
                  end
                  Thread.pass
               end
            end
            return retval
         end
      rescue Timeout::Error
         $log.error("Ping timeout...retrying")
         retries += 1
         if retries == 2
            return false
         else
            retry
         end
      end
   end

end
insteonConfig.rb
$log = Logger.new(STDOUT)
$log.level = Logger::INFO
#$log.level = Logger::DEBUG
$log.datetime_format = "%Y-%m-%d %H:%M:%S"

#Create the location
$long = "36.76"
$lat = "-80.73"
$timezone = 'America/New_York'

#$chost = "127.0.0.1"
$chost = "192.168.5.25" #address of the Smartlinc
#$cport = 20000
$cport = 9761
$cusername = "" 
$cpassword = "" 

$shost = "127.0.0.1"
$sport = 9091
$susername = "" 
$spassword = "" 

$tty = '/dev/insteon'

$dbhost = 'localhost'
$dbusername = 'mysqluser'
$dbpassword = 'mysqlpassword'
$dbtable = 'joomla'

$emailserver = ''
$emailfrom = ''
$emailfromname = 'Home'
 

Serial to TCP server

This code allows a usb or serial Insteon PLM to interface to the Insteon 2412n controller code posted here.

#!/usr/bin/env ruby
# serialserver.rb  
require 'rubygems'
require 'timeout'
require 'logger'
require 'pp'
require "socket"  
require "serialport"
require '/usr/src/insteon/insteonConfig.rb'
include Socket::Constants

$log = Logger.new(STDOUT)
$log.level = Logger::INFO
$log.datetime_format = "%Y-%m-%d %H:%M:%S"
$log.info('Starting...')


$log.info("Starting TCP server on port #{$cport}")
server = TCPServer.new($cport)

while true
   catch :socketerror do
      rclients= []
      wclients= []
      eclients= []

      $log.info("Opening serialport on device #{$tty}")
      sp = SerialPort.new $tty,19200
      sp.read_timeout = 100
      sp.flow_control = SerialPort::NONE

      begin
         rclients << server.accept_nonblock
      rescue Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
         IO.select([server])
         retry
      end
      $log.info("TCP connecton on port #{$cport} is accepted")  

      while true
         readable, writeable = IO.select(rclients,wclients,eclients,0.1)
         if readable
            readable.each do |s|
               command = ''
               begin
                  command,sa = s.read_nonblock(1024)
               rescue Errno::EAGAIN
                  IO.select([s])
               rescue
                  $log.error("TCP connecton on port #{$cport} is gone")  
                  s.close 
                  sp.close
                  throw :socketerror
               end
               if !command.empty?
                  retval = sp.write(command)
                  $log.info "Request:  #{command.unpack('H*').join.upcase}"
                 #sleep(0.1)
               end
            end
         end
         r = sp.read
         if !r.empty?
            $log.info("Response: #{r.unpack('H*').join.upcase}")
            rclients[0].write r
         end
         Thread.pass
      end
   end
end  
 

Insteon 2412n controller (old method)

This has been update.  Please see the update here

The following demonstrates the ability to use the Smarthome Smartlinc network controller, similar to a dedicated PLM.

I wrote this as my first Ruby program.  It allows me expand the use of the 2412n to include motion sensor and more advanced timed events.

Insteon.rb  Main program

require 'rubygems'
require 'thread'
require 'ice_cube'
require "rexml/document"
require "socket"
include Socket::Constants
require 'Sunrise'
require 'pp'

#Create the location
$long = "36.76"
$lat = "-80.73"
$timezone = 'America/New_York'

$host = "ha-controller.home.local"
$port = 80
$username = "htrn" 
$password = "foofoodd" 

$buffer = ""
$queue = Queue.new

producer = Thread.new {
def check_controller()
    path = "/buffstatus.xml"                 # The file we want 

    # This is the HTTP request we send to fetch a file
    request = "POST #{path} HTTP/1.1\r\n\r\n"

    mr = 2
    tbs = ''
    while mr > 0 
      socket = TCPSocket.new($host,$port)  # Connect to server
      socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

      socket.write(request[0..0])               # Send request
      socket.flush
      socket.write(request[1..-1])               # Send request
      socket.flush
      response = socket.read              # Read complete response
      # Split response at first blank line into headers and body
      socket.close
      header,body = response.split("\r\n\r\n", 2) 
      if header != nil
        m = header.scan(/\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/min)
        retval = {'status' => m[0][1].to_i,'content' => body}
      else
        retval = {'status' => 404,'content' => ''}
      end
      if retval['status'] == 200 
        bs = get_buffstatus(retval['content'])
        if bs != nil
            if tbs != bs
              tbs = bs
            else
              mr = mr - 1 
              if mr == 0
                clear_buffstatus()
                queue_messages(bs)
              end
            end
        end 
      else
        puts "Buffstatus error #{retval['status']}"
      end
    end
    return true
end
def queue_messages(buff)
       buff = buff.gsub("0250", ",0250")
       buff = buff.gsub("0251", ",0251")
       buff = buff.gsub("0252", ",0252")
       buff = buff.gsub("0253", ",0253")
       buff = buff.gsub("0254", ",0254")
       buff = buff.gsub("0255", ",0255")
       buff = buff.gsub("0256", ",0256")
       buff = buff.gsub("0257", ",0257")
       buff = buff.gsub("0258", ",0258")

       buff = buff.gsub("0260", ",0260")
       buff = buff.gsub("0261", ",0261")
       buff = buff.gsub("0262", ",0262")
       buff = buff.gsub("0263", ",0263")
       buff = buff.gsub("0264", ",0264")
       buff = buff.gsub("0265", ",0265")
       buff = buff.gsub("0266", ",0266")
       buff = buff.gsub("0267", ",0267")
       buff = buff.gsub("0268", ",0268")
       buff = buff.gsub("0269", ",0269")
       buff = buff.gsub("026A", ",026A")
       buff = buff.gsub("026B", ",026B")
       buff = buff.gsub("026C", ",026C")
       buff = buff.gsub("026D", ",026D")
       buff = buff.gsub("026E", ",026E")
       buff = buff.gsub("026F", ",026F")

       buff = buff.gsub("0270", ",0270")
       buff = buff.gsub("0271", ",0271")
       buff = buff.gsub("0272", ",0272")
       buff = buff.gsub("0273", ",0273")

       time = Time.new

       e = buff.split(',')
       e.each_index {|i|
          if !e[i].empty?
             message = Hash.new
             message['Time'] = time
             message['Event'] = ''
             message['Description'] = ''
             message['From'] = ''
             message['To'] = ''
             message['Flags'] = ''
             message['Command'] = ''
             message['Extended'] = ''
             message['Category'] = ''
             message['Subcateory'] = ''
             message['Version'] = ''
             message['Status'] = ''
             message['Linkcode'] = ''

             event = e[i][0..3]
             case event 
             when '0250' # received standard message
                   arr = e[i].scan(/(....)(......)(......)(..)(....)/)
                   if arr.length == 1 and arr[0].length == 5
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Std Message'
                     message['From'] = arr[0][1]
                     message['To'] = arr[0][2]
                     message['Flags'] = arr[0][3]
                     message['Command'] = arr[0][4]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0251' # received extended message
                   arr = e[i].scan(/(....)(......)(......)(..)(....)(............................)/)
                   if arr.length == 1 and arr[0].length == 6
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Ext Message'
                     message['From'] = arr[0][1]
                     message['To'] = arr[0][2]
                     message['Flags'] = arr[0][3]
                     message['Command'] = arr[0][4]
                     message['Extended'] = arr[0][5]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0253' # All Linking Completed
                   arr = e[i].scan(/(....)(..)(..)(......)(..)(..)(..)/)
                   if arr.length == 1 and arr[0].length == 6
                     message['Event'] = arr[0][0]
                     message['Description'] = 'All Linking Completed'
                     message['Linkcode'] = arr[0][1]
                     message['To'] = arr[0][2]
                     message['From'] = arr[0][3]
                     message['Category'] = arr[0][4]
                     message['Subcateory'] = arr[0][5]
                     message['Status'] = arr[0][6]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0254' # Button Event Report
                   arr = e[i].scan(/(....)(..)/)
                   if arr.length == 1 and arr[0].length == 2
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Button Event Report'
                     message['Status'] = arr[0][1]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0257' # All-Link Report
                   arr = e[i].scan(/(....)(..)(..)(......)(......)/)
                   if arr.length == 1 and arr[0].length == 5
                     message['Event'] = arr[0][0]
                     message['Description'] = 'All-Link Report'
                     message['From'] = arr[0][3]
                     message['To'] = arr[0][2]
                     message['Flags'] = arr[0][1]
                     message['Command'] = arr[0][4]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0258' # ALL-Link Cleanup Status Report
                   arr = e[i].scan(/(....)(.*)/)
                   if arr.length == 1 and arr[0].length == 2
                     message['Event'] = arr[0][0]
                     message['Description'] = 'All-Link Cleanup'
                     message['Status'] = arr[0][1]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0260' # Get IM Info
                   arr = e[i].scan(/(....)(......)(..)(..)(..)(..)/)
                   if arr.length == 1 and arr[0].length == 6
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Get IM Info'
                     message['From'] = arr[0][1]
                     message['Category'] = arr[0][2]
                     message['Subcateory'] = arr[0][3]
                     message['Version'] = arr[0][4]
                     message['Status'] = arr[0][5]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0261' # Scene Command
                   arr = e[i].scan(/(....)(..)(..)(.*)/)
                   if arr.length == 1 and arr[0].length == 4
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Scene Message'
                     message['From'] = arr[0][1]
                     message['Command'] = arr[0][2]
                     message['Status'] = arr[0][3]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0262' # Send Message                                         
                   arr = e[i].scan(/(....)(......)(..)(....)(.*)/) 
                   if arr.length == 1 and arr[0].length == 5
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Send Message'
                     message['To'] = arr[0][1]
                     message['Flags'] = arr[0][2]
                     message['Command'] = arr[0][3]
                     message['Status'] = arr[0][4][-2..-1]
                     if arr[0][2].hex & 16 == 16
                       message['Extended'] = arr[0][4][0..-3] 
                     end
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0265' # Cancel All-linking
                   arr = e[i].scan(/(....)(..)/)
                   if arr.length == 1 and arr[0].length == 2
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Cancel All-linking'
                     message['Status'] = arr[0][1]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '0269' # Get First ALL-Link
                   arr = e[i].scan(/(....)(..)/)
                   if arr.length == 1 and arr[0].length == 2
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Get First ALL-Link'
                     message['Status'] = arr[0][1]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             when '026A' # Get Next ALL-Link
                   arr = e[i].scan(/(....)(..)/)
                   if arr.length == 1 and arr[0].length == 2
                     message['Event'] = arr[0][0]
                     message['Description'] = 'Get Next ALL-Link'
                     message['Status'] = arr[0][1]
                   else
                    message['Event'] = "FF#{event}"
                    message['Description'] = 'Malformed Message'
                    message['Command'] = e[i]
                   end
             else
                   message['Event'] = 'FFFF'
                   message['Description'] = 'Unknown'
                   message['Command'] = e[i]
        end
        $queue.push(message)
    end
  }
end

def clear_buffstatus()
    path = "/1?XB=M=1"                 # The file we want 

    # This is the HTTP request we send to fetch a file
    request = "POST #{path} HTTP/1.1\r\n\r\n"

    socket = TCPSocket.new($host,$port)  # Connect to server
    socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
    socket.write(request[0..0])               # Send request
    socket.flush
    socket.write(request[1..-1])               # Send request
    socket.flush
    response = socket.read              # Read complete response
    socket.close
    # Split response at first blank line into headers and body
    header,body = response.split("\r\n\r\n", 2) 
    m = header.scan(/\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/min)
    retval = {'status' => m[0][1].to_i,'content' => body}
    return retval
    rescue Errno::ECONNRESET => e
          puts "Clear Buff Timeout error " + e
end

def get_buffstatus(xml)
       doc = REXML::Document.new(xml)
       doc.elements.each("/response/BS") do |elem|
              return elem.text
       end
end              


  while true
    check_controller()
    Thread.pass
  end
}
def setclock()
    now = Date.today.strftime("%H:%M")
    path = "1?TD=${now}=1=falsefalse"  

    # This is the HTTP request we send to fetch a file
    request = "POST #{path} HTTP/1.1\r\n\r\n"

    socket = TCPSocket.new($host,$port)  # Connect to server
    socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
    socket.write(request[0..0])               # Send request
    socket.flush
    socket.write(request[1..-1])               # Send request
    socket.flush
    response = socket.read              # Read complete response
    socket.close
    # Split response at first blank line into headers and body
    header,body = response.split("\r\n\r\n", 2) 
    m = header.scan(/\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/min)
    retval = {'status' => m[0][1].to_i,'content' => body}
    return retval
    rescue Errno::ECONNRESET => e
          puts "Set Clock Timeout error " + e
end

def sendcommand(device,cmd)
    path = "/3?0262#{device}0F#{cmd}=I=3"  

    # This is the HTTP request we send to fetch a file
    request = "POST #{path} HTTP/1.1\r\n\r\n"

    socket = TCPSocket.new($host,$port)  # Connect to server
    socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
    socket.write(request[0..0])               # Send request
    socket.flush
    socket.write(request[1..-1])               # Send request
    socket.flush
    response = socket.read              # Read complete response
    socket.close
    # Split response at first blank line into headers and body
    header,body = response.split("\r\n\r\n", 2) 
    m = header.scan(/\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/min)
    retval = {'status' => m[0][1].to_i,'content' => body}
    return retval
    rescue Errno::ECONNRESET => e
          puts "Send Command Timeout error " + e
end

def parse_buffstatus(e)

 print e['Time'].strftime("%Y-%m-%d %H:%M:%S") + " "
 print "Event: #{e['Description']}(#{e['Event']}) "


 case e['Event'] 
        when '0250' # received standard message
                print "From: #{e['From']} "
                print "To: #{e['To']} "
                print "Flags:[" + parse_flag(e['Flags']) + '] '
                print "Command: #{e['Command']} "
        when '0251' # received extended message
                print "From: #{e['From']} "
                print "To: #{e['To']} "
                print "Flags:[" + parse_flag(e['Flags']) + '] '
                print "Command: #{e['Command']} "
                print "Extended: #{e['Extended']} "
        when '0253' # ALL-Linking Completed
                print "From: #{e['From']} "
                print "Group: #{e['To']} "
                print "Link Code: "
                case e['Linkcode']
                  when '00'
                    print "IM is a Responder"
                  when '01'
                    print "IM is a Controller"
                  when 'FF'
                    print "ALL-Link to the device was deleted"
                  else
                    print "Unknown"
                end
                print "Cat: #{e['Category']} "
                print "Sub Cat: #{e['Subcategory']} "
                print "Status: #{e['Status']}"
        when '0254' # Button Event Report
                print "Status: "
                case e['Status']
                  when '02'
                    print "IM SET Button tapped"
                  when '03'
                    print "IM SET Button held"
                  when '04'
                    print "IM SET Button released after hold"
                  when '12'
                    print "IM Button 2 tapped"
                  when '13'
                    print "IM Button 2 held"
                  when '14'
                    print "IM Button 2 released after hold"
                  when '22'
                    print "IM Button 3 tapped"
                  when '23'
                    print "IM Button 3 held"
                  when '24'
                    print "IM Button 3 released after hold"
                  else
                    print "Unknown"
                end
        when '0257' # All-Link Report
                print "Flags:[" + parse_flag(e['Flags']) + '] '
                print "Group: #{e['To']} "
                print "Device: #{e['From']} "
                print "Data: #{e['Command']} "
        when '0258' # ALL-Link Cleanup Status Report
                print "Status: #{e['Status']}"
        when '0260' # Get IM Info
                print "Device: #{e['From']} "
                print "Cat: #{e['Category']} "
                print "Sub Cat: #{e['Subcategory']} "
                print "Version: #{e['Version']} "
                print "Status: #{e['Status']}"
        when '0261' # Scene Command
                print "Scene: #{e['From']} "
                print "Command: "
                case e['Command']
                  when '11'
                    print "On"
                  when '12'
                    print "Fast On"
                  when '13'
                    print "Off"
                  when '14'
                    print "Fast Off"
                  else
                    print "Unknown"
                end
                print " Result: #{e['Status']}"
        when '0262' # Send Message                                         
                print "To: #{e['To']} "
                print "Flags:[" + parse_flag(e['Flags']) + '] '
                print "Command: #{e['Command']} "
                if e['Flags'].hex & 16 == 16
                  print e['Extended'] + ' '
                end
                print "Status: "
                case e['Status']
                  when '06'
                    print "Ack"
                  when '15'
                    print "Nack"
                  when 'FF'
                    print "Fail"
                  else
                    print "Unknown"
                end
        when '0265' # Cancel All-linking
                print "Status: #{e['Status']} "
        when '0269' # Get First ALL-Link
                print "Status: #{e['Status']} "
        when '026A' # Get Next ALL-Link
                print "Status: #{e['Status']} "
        else
                print "Command: #{e['Command']} "
  end
  print "\n"
end              

def parse_flag(flag)
    retval = flag.hex
    f = ''
    if retval & 128 == 128
        f = f +  'B'
    else
        f = f +  'b'
    end
    if retval & 64  == 64
        f = f +  'G'
    else
        f = f +  'g'
    end
    if retval & 32 == 32
        f = f +  'A'
    else
        f = f +  'a'
    end
    if retval & 16  == 16
         f = f +  'E'
   else
        f = f +  'e'
    end
    f = f + ' HL:' 
    if retval & 8 == 8
        f = f +  '1'
    else
        f = f +  '0'
    end
    if retval & 4 == 4
        f = f +  '1'
    else
        f = f +  '0'
    end
    f = f + ' MH:' 
    if retval & 2 == 2
        f = f +  '1'
    else
        f = f +  '0'
    end
    if retval & 1 == 1
        f = f +  '1'
    else
        f = f +  '0'
    end
    
    return f
end

def daytime()
  date = Date.today
  calc = SolarEventCalculator.new(date, BigDecimal.new($long), BigDecimal.new($lat))  
  localSunrise = calc.compute_official_sunrise($timezone)  
  localSunset = calc.compute_official_sunset($timezone)  

  now = DateTime.now
  now = DateTime.new(now.year, now.month, now.day, now.hour, now.min, now.sec, now.sec_fraction, now.offset)

  if now >= localSunrise and now <= localSunset
    retval = true
  else
    retval = false
  end
  return retval
end

def execute_message(m)
   if m['From'] == '141120' and m['To'] == '000001'
     print 'Command from Motion Sensor ' + m['Command'] + ' ' + (daytime()?'Day':'Night') + ' '
     if m['Command'] = '1101' and daytime() == false
       print "Send on to 154AE3"
       sendcommand('154AE3', '11FF')
       sendcommand('154DE4', '11FF')
     end
     print "\n"
   end
   if m['From'] == '13DED1' and m['To'] == '000001'
     print 'Command from RemoteLinc ' + m['Command'] + ' '
     if m['Command'] == '1100'
       print 'Send On to 154AE3'
       sendcommand('154AE3', '11FF')
     end
     if m['Command'] == '1300'
       print 'Send Off to 154AE3'
       sendcommand('154AE3', '13FF')
     end
     print "\n"
   end
   if m['From'] == '13DED1' and m['To'] == '000002'
     print 'Command from RemoteLinc ' + m['Command'] + ' '
     if m['Command'] == '1100'
       print 'Send On to 154DE4'
       sendcommand('154DE4', '11FF')
     end
     if m['Command'] == '1300'
       print 'Send Off to 154DE4'
       sendcommand('154DE4', '13FF')
     end
     print "\n"
   end
end

producer.abort_on_exception = true

setclockrule = IceCube::Rule.daily
setclockschedule = IceCube::Schedule.new(Time.parse("01:00"), :duration => 2)
setclockschedule.add_recurrence_rule setclockrule

alloffrule = IceCube::Rule.daily
alloffschedule = IceCube::Schedule.new(Time.parse("11:00"), :duration => 2)
alloffschedule.add_recurrence_rule alloffrule

scheduletime = Time.now
while true
  #puts "Producer #{producer.status} Timers #{timers.status} Main #{Thread.main.status}"
  #puts $queue.length()
  if $queue.length() > 0
    message = $queue.pop()
    #PP.singleline_pp(message)
    #print ' '
    execute_message(message)
    parse_buffstatus(message)
  end

  if scheduletime + 1 <= Time.now 
    scheduletime = Time.now
    if setclockschedule.occurring_at?(Time.now)
      print "Setting clock\n"
      setclock()
    end
    if alloffschedule.occurring_at?(Time.now)
      print "All off\n"
      sendcommand('154AE3', '13FF')
      sendcommand('154DE4', '13FF')
    end
  end
  Thread.pass
end

Sunrise.rb  for daytime function

require 'bigdecimal'
require 'date'
require 'tzinfo'

class SolarEventCalculator

  @date
  @latitude
  @longitude

  def initialize(date, latitude, longitude)
    @date = date
    @latitude = latitude
    @longitude = longitude
  end

  def compute_lnghour
    lngHour = @longitude / BigDecimal.new("15")
    lngHour.round(4)
  end

  def compute_longitude_hour(isSunrise)
    minuend = (isSunrise) ? BigDecimal.new("6") : BigDecimal.new("18")
    longHour = @date.yday + ((minuend - compute_lnghour) / BigDecimal.new("24"))
    longHour.round(4)
  end

  def compute_sun_mean_anomaly(longHour)
    constant = BigDecimal.new("0.9856")
    ((longHour * constant) - BigDecimal.new("3.289")).round(4)
  end

  def compute_sun_true_longitude(meanAnomaly)
    mAsRads = degrees_as_rads(meanAnomaly)
    sinM = BigDecimal.new(Math.sin(mAsRads.to_f).to_s)
    sinTwoM = BigDecimal.new(Math.sin((2 * mAsRads).to_f).to_s)
    firstParens = BigDecimal.new("1.916") * sinM
    secondParens = BigDecimal.new("0.020") * sinTwoM
    trueLong = meanAnomaly + firstParens + secondParens + BigDecimal.new("282.634")
    trueLong = put_in_range(trueLong, 0, 360, 360)
    trueLong.round(4)
  end

  def compute_right_ascension(sunTrueLong)
    tanL = BigDecimal.new(Math.tan(degrees_as_rads(sunTrueLong).to_f).to_s)
    ra = rads_as_degrees(BigDecimal.new(Math.atan(BigDecimal.new("0.91764") * tanL).to_s))

    ra = put_in_range(ra, 0, 360, 360)
    ra.round(4)
  end

  def put_ra_in_correct_quadrant(sunTrueLong)
    lQuadrant = BigDecimal.new("90") * (sunTrueLong / BigDecimal.new("90")).floor
    raQuadrant = BigDecimal.new("90") * (compute_right_ascension(sunTrueLong) / BigDecimal.new("90")).floor

    ra = compute_right_ascension(sunTrueLong) + (lQuadrant - raQuadrant)
    ra = ra / BigDecimal.new("15")
    ra.round(4)
  end

  def compute_sin_sun_declination(sunTrueLong)
    sinL = BigDecimal.new(Math.sin(degrees_as_rads(sunTrueLong).to_f).to_s)
    sinDec = sinL * BigDecimal.new("0.39782")
    sinDec.round(4)
  end

  def compute_cosine_sun_declination(sinSunDeclination)
    cosDec = BigDecimal.new(Math.cos(Math.asin(sinSunDeclination)).to_s)
    cosDec.round(4)
  end

  def compute_cosine_sun_local_hour(sunTrueLong, zenith)
    cosZenith = BigDecimal.new(Math.cos(degrees_as_rads(BigDecimal.new(zenith.to_s))).to_s)
    sinLatitude = BigDecimal.new(Math.sin(degrees_as_rads(@latitude)).to_s)
    cosLatitude = BigDecimal.new(Math.cos(degrees_as_rads(@latitude)).to_s)

    sinSunDeclination = compute_sin_sun_declination(sunTrueLong)
    top = cosZenith - (sinSunDeclination * sinLatitude)
    bottom = compute_cosine_sun_declination(sinSunDeclination) * cosLatitude

    cosLocalHour = top / bottom
    cosLocalHour.round(4)
  end

  def compute_local_hour_angle(cosSunLocalHour, isSunrise)
    acosH = BigDecimal.new(Math.acos(cosSunLocalHour).to_s)
    acosHDegrees = rads_as_degrees(acosH)

    localHourAngle = (isSunrise) ? BigDecimal.new("360") - acosHDegrees : acosHDegrees
    localHourAngle = localHourAngle / BigDecimal.new("15")
    localHourAngle.round(4)
  end

  def compute_local_mean_time(sunTrueLong, longHour, t,  sunLocalHour)
    h = sunLocalHour
    ra = put_ra_in_correct_quadrant(sunTrueLong)

    parens = BigDecimal.new("0.06571") * t
    time = h + ra - parens - BigDecimal.new("6.622")

    utcTime = time - longHour
    utcTime = put_in_range(utcTime.round(4), 0, 24, 24)
  end

  def compute_utc_solar_event(zenith, isSunrise)
    longHour = compute_lnghour
    eventLongHour = compute_longitude_hour(isSunrise)

    meanAnomaly = compute_sun_mean_anomaly(eventLongHour)
    sunTrueLong = compute_sun_true_longitude(meanAnomaly)
    cosineSunLocalHour = compute_cosine_sun_local_hour(sunTrueLong, zenith)

    if(cosineSunLocalHour > BigDecimal.new("1") || cosineSunLocalHour < BigDecimal.new("-1"))
      return nil
    end

    sunLocalHour = compute_local_hour_angle(cosineSunLocalHour, isSunrise)
    localMeanTime = compute_local_mean_time(sunTrueLong, longHour, eventLongHour, sunLocalHour)

    timeParts = localMeanTime.to_s('F').split('.')
    mins = BigDecimal.new("." + timeParts[1]) * BigDecimal.new("60")
    mins = mins.truncate()
    mins = pad_minutes(mins.to_i)
    hours = timeParts[0]

    Time.utc(@date.year, @date.mon, @date.mday, hours, pad_minutes(mins.to_i))
  end

  def compute_utc_civil_sunrise
    convert_to_datetime(compute_utc_solar_event(96, true))
  end

  def compute_utc_civil_sunset
    convert_to_datetime(compute_utc_solar_event(96, false))
  end

  def compute_utc_official_sunrise
    convert_to_datetime(compute_utc_solar_event(90.8333, true))
  end

  def compute_utc_official_sunset
    convert_to_datetime(compute_utc_solar_event(90.8333, false))
  end

  def compute_utc_nautical_sunrise
    convert_to_datetime(compute_utc_solar_event(102, true))
  end

  def compute_utc_nautical_sunset
    convert_to_datetime(compute_utc_solar_event(102, false))
  end

  def compute_utc_astronomical_sunrise
    convert_to_datetime(compute_utc_solar_event(108, true))
  end

  def compute_utc_astronomical_sunset
    convert_to_datetime(compute_utc_solar_event(108, false))
  end

  def convert_to_datetime(time)
    DateTime.parse("#{@date.strftime}T#{time.hour}:#{time.min}:00+0000") unless time == nil
  end

  def compute_civil_sunrise(timezone)
    put_in_timezone(compute_utc_solar_event(96, true), timezone)
  end

  def compute_civil_sunset(timezone)
    put_in_timezone(compute_utc_solar_event(96, false), timezone)
  end

  def compute_official_sunrise(timezone)
    put_in_timezone(compute_utc_solar_event(90.8333, true), timezone)
  end

  def compute_official_sunset(timezone)
    put_in_timezone(compute_utc_solar_event(90.8333, false), timezone)
  end

  def compute_nautical_sunrise(timezone)
    put_in_timezone(compute_utc_solar_event(102, true), timezone)
  end

  def compute_nautical_sunset(timezone)
    put_in_timezone(compute_utc_solar_event(102, false), timezone)
  end

  def compute_astronomical_sunrise(timezone)
    put_in_timezone(compute_utc_solar_event(108, true), timezone)
  end

  def compute_astronomical_sunset(timezone)
    put_in_timezone(compute_utc_solar_event(108, false), timezone)
  end

  def put_in_timezone(utcTime, timezone)
    tz = TZInfo::Timezone.get(timezone)
    # puts "UTCTime #{utcTime}"
    local = utcTime + get_utc_offset(timezone)
    # puts "LocalTime #{local}"

    offset = (get_utc_offset(timezone) / 60 / 60).to_i
    offset = (offset > 0) ? "+" + offset.to_s : offset.to_s

    timeInZone = DateTime.parse("#{@date.strftime}T#{local.strftime('%H:%M:%S')}#{offset}")
    # puts "CALC:timeInZone #{timeInZone}"
    timeInZone
  end

  def get_utc_offset(timezone)
    tz = TZInfo::Timezone.get(timezone)
    noonUTC = Time.gm(@date.year, @date.mon, @date.mday, 12, 0)
    tz.utc_to_local(noonUTC) - noonUTC
  end

  def pad_minutes(minutes)
    if(minutes < 10)
      "0" + minutes.to_s
    else
      minutes
    end
  end

  def put_in_range(number, lower, upper, adjuster)
    if number > upper then
      number -= adjuster
    elsif number < lower then
      number += adjuster
    else
      number
    end
  end

  def degrees_as_rads(degrees)
    pi = BigDecimal(Math::PI.to_s)
    radian = pi / BigDecimal.new("180")
    degrees * radian
  end

  def rads_as_degrees(radians)
    pi = BigDecimal(Math::PI.to_s)
    degree = BigDecimal.new("180") / pi
    radians * degree
  end

end