FreeBSDのmrubyでもスマートスイッチ

実はこちらの記事があったのでHS105を購入してみました。

https://qiita.com/Y_uuu/items/8d4af1c246262939b08e

ZRouterでビルドしたFreeBSDでも動かしてみました。

```ruby:
#!/usr/local/bin/mruby

target = 'hs105'
broadcast = '192.168.1.255'

class String
  def encrypt
    input = self.unpack('C*')
    output = []
    key = 171
    input.each { |c| key = (key ^ c); output << key }
    output.pack('C*')
  end

  def decrypt
    input = self.unpack('C*')
    output = []
    key = 171
    input.each { |c| output << (key ^ c);key = c }
    output.pack('C*')
  end
end

class Plug
  def initialize(ip)
    @ip = ip
  end

  # check plug
  def self.discovery(name, broadcast)
    s = UDPSocket.open
    s.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)
    s.bind("0.0.0.0", 10000)

    # broadcast message
    message = { system: { get_sysinfo: nil } }.to_json
    s.send(message.encrypt, 0, broadcast, 9999)

    # 10 times respose check
    10.times do
      message, addr = s.recvfrom(1000)
      device = JSON.parse(message.decrypt)

      # get target module then return instace
      break Plug.new(addr.last) if name == device.dig('system', 'get_sysinfo', 'alias')
      nil
    end
  end

  # ON/OFF chane
  def relay_state=(state)
    s = TCPSocket.open(@ip, 9999)
    message = { system: { set_relay_state: { state: state ? 1 : 0 } } }.to_json
    s.write([message.size].pack('N*') + message.encrypt)
    s.close
  end

end

plug = Plug.discovery(target, broadcast)

if ARGV[0] == "on" then
  plug.relay_state = true
end

if ARGV[0] == "off" then
  plug.relay_state = false
end
```

FreeBSDではブロードキャストの255.255.255.255は使えないようです。tcpdump -e ip broadcastで見てみたところEthernetのunicastになっていました。DHCPで予約されているのかもしれません。このためネットワークのブロードキャストアドレスを使います。192.168.1.0/24なネットワークでは192.168.1.255がブロードキャストアドレスです。Rubyでは'&lt;broadcast&gt;'が使えるようですが、mrubyでは使えないようなので、定義してみました。

HS105がローカルのネット上にいればrecvfromが返ってきますが、無い場合はブロックされるので、selectで待つなどの処理が必要です。

kasaアプリを使っていると、これが外にアクセスしている可能性があるので、このスクリプトを使ってon/offしてれば、外への情報漏洩の可能性はなくなります。

またkasaアプリはVPNに対応していないようなので、TP-Linkのアカウントなしに、より安全に外からおうちのコントロールが可能です。

HS105はスマートスイッチですが、100Vを制御しているためか、結構大きくてスマートではありません。


![IMG_20220401_114945.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104066/85bc2e10-045a-767e-c282-d1e5b4206f66.jpeg)

HS105は極性がありN(接地)側が大きくなっています。このためタップなどの極性がないコンセントにはささらないことがあります。逆にした場合動作にも影響するというケースがあるそうです。屋内配線が正しくないと動作しないことがあるのかもしれません。


mruby-uvで非同期処理にして、機器が見つからない場合もブロックしないようにしてみました。

```ruby
#!/usr/local/bin/mruby

target ='hs105'
ipaddr = '192.168.1.161'

class String
  def encrypt
    input = self.unpack('C*')
    output = []
    key = 171
    input.each { |c| key = (key ^ c); output << key }
    output.pack('C*')
  end

  def decrypt
    input = self.unpack('C*')
    output = []
    key = 171
    input.each { |c| output << (key ^ c);key = c }
    output.pack('C*')
  end
end

class Plug
  def initialize(ip, state)
    addr = ip.to_s.split(':')
    @ip = addr[0]
    @state = state
  end

  # check plug
  def self.discovery(name, ipaddr)
    res = nil

    t = UV::Timer.new()
    c = 2

    u = UV::UDP.new()

    u.bind(UV::ip4_addr('0.0.0.0', 10000))
    # broadcast message
    message = { system: { get_sysinfo: nil } }.to_json
    u.send(message.encrypt, UV::ip4_addr(ipaddr, 9999)) {|x|
    }

    u.recv_start {|message, addr, flags|
      if message && message.size > 0
        device = JSON.parse(message.decrypt)

        if name == device.dig('system', 'get_sysinfo', 'alias')
          res = Plug.new(addr,
            device.dig('system', 'get_sysinfo', 'relay_state'))
          u.recv_stop
          t.stop()
        end
      end
    }

    t.start(1000, 1000) {|x|
      c -= 1
      if c < 0
        u.recv_stop
        t.stop()
      end
    }

    UV::run()

    u.close()

    res

  end

  # ON/OFF chane
  def relay_state=(state)
    s = TCPSocket.open(@ip, 9999)
    message = { system: { set_relay_state: { state: state ? 1 : 0 } } }.to_json
    s.write([message.size].pack('N*') + message.encrypt)
    s.close
  end

  def curstate
    @state
  end

end

plug = Plug.discovery(target, ipaddr)

if plug
  if ARGV[0] == "on" then
    plug.relay_state = true
  end

  if ARGV[0] == "off" then
    plug.relay_state = false
  end

  if ARGV[0] == nil then
    p plug.curstate
  end
end
```

UV::UDPでなぜかネットワークのブロードキャストが送信できなかったので、ユニキャストになってます。我が家は固定でDHCPしているので、これでも実用になります。

UDPのユニキャストの送信はarpがある、または返答があった場合は送信されますが、そうでないときは送信されず捨てられます。

ZRouterでmruby-uvを使うためにはportsもどきのlibuvを先にビルドしておく必要があります。