Fancy Gap HTRN

My Personal Playground

  • Increase font size
  • Default font size
  • Decrease font size
Home Home Automation Insteon Insteon 2412n controller (old method)

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