スマートメータのBルートサービスで Wi-SUN モジュールを使って瞬間消費電力を読み出す | tech - 氾濫原 にひき続き Wi-SUN モジュール ROHM BP35A1 と ECHONET Lite プロトコルを使い、スマートメータから値を取得するサンプルです。

前回のコードはさすがにちゃんと動かなすぎるものなので、多少まともにしたものを書きました。一応16時間ぐらい動かしても止まることなく動く感じです。

連続して動かす場合大事なところ

  • UDP 送信時の失敗処理をちゃんとやること
  • タイムアウト処理をちゃんとやること
    • たとえこちらからの UDP の送信に成功しても、UDPパケットがこちらに必ず受信できる保証はない

途中に環境変数で分岐していますが、片方はテスト用のコードです。Wi-SUN のスキャンが結構時間がかかってイライラするので、想定問答をシミュレーションしています。ちゃんとテスト化したほうがいいんですが、長時間実際に動かしてみるほうが有益だと思ったので限られた時間でそこまでやってません。

#!/usr/bin/env ruby -v

require 'stringio'

module ECHONET_Lite
	EHD1 = 0b00010000
	EHD2_DEFINED = 0x81
	EHD2_ANY = 0x82

	class ParseError < Exception
	end

	def self.parse_frame(frame)
		ret = Frame.parse(frame)
		unless ret.valid?
			raise ParseError.new("not an ECHONET Lite frame")
		end
		ret
	end

	Frame = Struct.new(:ehd1, :ehd2, :tid, :edata) do
		def self.parse(frame)
			ret = self.new(*frame.unpack("CCna*"))
			if ret.valid? && ret.format_defined?
				ret.edata = EDATA.parse(ret.edata)
			end
			ret
		end

		def valid?
			ehd1 == EHD1
		end

		def format_defined?
			ehd2 == EHD2_DEFINED
		end

		def format_any?
			ehd2 == EHD2_ANY
		end

		def pack
			[ehd1, ehd2, tid].pack("CCn") + edata.pack
		end
	end

	EDATA = Struct.new(:seoj, :deoj, :esv, :opc, :properties) do
		def self.parse(edata)
			ret = self.new(*edata.unpack("a3a3CCa*"))
			ret.seoj = EOJ.parse(ret.seoj)
			ret.deoj = EOJ.parse(ret.deoj)

			props = []
			StringIO.open(ret.properties) do |io|
				ret.opc.times do
					epc, pdc = *io.read(2).unpack("CC")
					edt = io.read(pdc)
					props << Property.new(epc, pdc, edt)
				end
			end
			ret.properties = props

			ret
		end

		def pack
			seoj.pack + deoj.pack + [esv, opc].pack("CC") + properties.map {|i|
				i.pack
			}.join
		end
	end

	EOJ = Struct.new(:class_group_code, :class_code, :instance_code) do
		def self.parse(eoj)
			self.new(*eoj.unpack("CCC"))
		end

		def pack
			to_a.pack("CCC")
		end
	end

	Property = Struct.new(:epc, :pdc, :edt) do
		def pack
			self.pdc = edt.length
			[epc, pdc].pack("CC") + edt
		end
	end
end

require 'thread'
class SKSTACK_IP
	EVENT_RECV_NS = 1
	EVENT_RECV_NA = 2
	EVENT_RECV_ECHO = 5
	EVENT_COMPLETED_ED_SCAN = 0x1F
	EVENT_RECV_BEACON = 0x20
	EVENT_UDP_SENT = 0x21
	EVENT_COMPLETED_ACTIVE_SCAN = 0x22

	EVENT_PANA_ERROR = 0x24
	EVENT_PANA_COMPLETED = 0x25
	EVENT_RECV_SESSION_CLOSE = 0x26
	EVENT_PANA_CLOSED = 0x27
	EVENT_PANA_TIMEOUT = 0x28
	EVENT_SESSION_EXPIRED = 0x29
	EVENT_SEND_LIMIT = 0x32
	EVENT_SEND_UNLOCK = 0x33

	def initialize(port)
		@event_callbacks = {}

		@port = port
		@port.set_encoding(Encoding::BINARY)

		@rest = nil

		@queue = Queue.new
		@read_thread = Thread.start do
			Thread.current.abort_on_exception = true
			buffer = ""
			while true
				# need to know command name preceded by whole line
				# because there is ERXUDP/ERXTCP which include length and any binary bytes.
				c = @port.getc
				if c.nil?
					raise "unexpected IO closed"
				end
				buffer << c
				case c
				when ' ', "\r"
					command = buffer.sub(/[\r ]$/, '')
					case command
					when "ERXUDP"
						event = {}
						event[:sender]    = @port.gets(" ").sub(/\s+$/, '')
						event[:dest]      = @port.gets(" ").sub(/\s+$/, '')
						event[:rport]     = @port.gets(" ").sub(/\s+$/, '').unpack("n")[0]
						event[:lport]     = @port.gets(" ").sub(/\s+$/, '').unpack("n")[0]
						event[:senderlla] = @port.gets(" ").sub(/\s+$/, '')
						event[:secured]   = @port.gets(" ").sub(/\s+$/, '')
						datalen           = @port.gets(" ").sub(/\s+$/, '')
						event[:data]      = @port.read(datalen.to_i(16))
						@port.read(2) # ignore crlf
						callback_event(:ERXUDP, event)
						buffer.clear
					when "ERXTCP"
						event = {}
						event[:sender] = @port.gets(" ").sub(/\s+$/, '')
						event[:rport]  = @port.gets(" ").sub(/\s+$/, '')
						event[:lport]  = @port.gets(" ").sub(/\s+$/, '')
						datalen = @port.gets(" ").sub(/\s+$/, '')
						event[:data]   = @port.read(datalen.to_i(16))
						@port.read(2) # ignore crlf
						callback_event(:ERXTCP, event)
						buffer.clear
					when "EPONG"
						event = {}
						event[:sender] = @port.gets("\n").sub(/\s+$/, '')
						callback_event(:EPONG, event)
						buffer.clear
					when "ETCP"
						event = {}
						event[:status] = @port.gets(" ").sub(/\s+$/, '')
						if event[:status] == "1"
							event[:handle] = @port.gets(" ").sub(/\s+$/, '')
							event[:ipaddr] = @port.gets(" ").sub(/\s+$/, '')
							event[:rport] = @port.gets(" ").sub(/\s+$/, '')
							event[:lport] = @port.gets("\n").sub(/\s+$/, '')
						else
							event[:handle] = @port.gets("\n").sub(/\s+$/, '')
						end
						callback_event(:EPONG, event)
						buffer.clear
					when "EADDR", "ENEIGHBOR"
						# ignore
					when "EPANDESC"
						event = {}
						@port.gets("\n") # ignore
						event[:channel]      = @port.gets("\n")[/Channel:(\S+)/, 1]
						event[:channel_page] = @port.gets("\n")[/Channel Page:(\S+)/, 1]
						event[:pan_id]       = @port.gets("\n")[/Pan ID:(\S+)/, 1]
						event[:addr]         = @port.gets("\n")[/Addr:(\S+)/, 1]
						event[:lqi]          = @port.gets("\n")[/LQI:(\S+)/, 1]
						event[:pair_id]      = @port.gets("\n")[/PairID:(\S+)/, 1]
						p event
						callback_event(:EPANDESC, event)
						buffer.clear
					when "EEDSCAN"
						@port.gets("\n") # ignore
						_rssi = @port.gets("\n")
					when "EPORT"
						@port.gets("\n") # ignore
						6.times do
							_udp = @port.gets("\n") # ignore
						end
						@port.gets("\n") # ignore
						4.times do
							_tcp = @port.gets("\n") # ignore
						end
						@port.gets("\n") # "OK" ignore
					when "EHANDLE"
						@port.gets("\n") # ignore
						while line = @port.gets("\n")
							line.chomp!
							break if line == "OK"
						end
					when "EVENT"
						num, sender, param = *@port.gets("\n").sub(/\s+$/, '').split(/ /)
						event = {
							num: num,
							sender: sender,
							param: param
						}
						callback_event(:EVENT, event)
						buffer.clear
					when "EVER"
						event = {}
						event[:version] = @port.gets("\n").sub(/\s+$/, '')
						callback_event(:EVER, event)
						buffer.clear
					when "EAPPVER"
						event = {}
						event[:version] = @port.gets("\n").sub(/\s+$/, '')
						callback_event(:EAPPVER, event)
						buffer.clear
					else
						# do nothing
					end
				when "\n"
					# event 以外
					line = buffer.chomp
					@queue << line
					buffer.clear
				end
			end
		end
	end

	def command(string)
		@port.write(string + "\r\n")
		res = @queue.pop
		if string.split(/ /)[0] == res.split(/ /)[0] # ignore echoback
			res = @queue.pop
		end
		res
	end

	def on(name, &block)
		(@event_callbacks[name.to_sym] ||= []) << block
	end

	private
	def callback_event(name, event)
		(@event_callbacks[name.to_sym] || []).each do |cb|
			cb.call(event)
		end
	end
end

require 'logger'
require 'timeout'
class SmartMeterController
	def initialize
		@logger = Logger.new($stdout)
	end

	def start(io, opts)
		@stack = SKSTACK_IP.new(io)
		@events = Queue.new
		@stack.on(:EVENT) do |e|
			@logger.debug("EVENT %p" % e)
			@events << e
		end
		@epandesc = nil
		@stack.on(:EPANDESC) do |e|
			@logger.debug("EPANDESC %p" % e)
			@epandesc = e
		end
		@transactions = {}
		@stack.on(:ERXUDP) do |e|
			@logger.info("ERXUDP %p" % e)
			begin
				frame = ECHONET_Lite.parse_frame(e[:data])
				if transaction = @transactions.delete(frame.tid)
					transaction.call(frame)
				end
			rescue ECHONET_Lite::ParseError
				@logger.info("Not an ECHONET Lite frame")
			end
		end

		@stack.on(:EVER) do |e|
			@logger.info("EVER %p" % e)
		end
		@stack.on(:EAPPVER) do |e|
			@logger.info("EAPPVER %p" % e)
		end

		@stack.command("SKRESET") == "OK" or raise
		@stack.command("SKVER") == "OK" or raise
		@stack.command("SKAPPVER") == "OK" or raise
		@stack.command("SKSREG SFE 0") == "OK" or raise
		@logger.info("Setting ID and Password")
		@stack.command("SKSETPWD C #{opts[:PASS]}") == "OK" or raise
		@stack.command("SKSETRBID #{opts[:ID]}") == "OK" or raise

		while true
			@logger.info("Scanning device...")
			@stack.command("SKSCAN 2 FFFFFFFF 6")

			while e = @events.pop
				if e[:num].to_i(16) == SKSTACK_IP::EVENT_COMPLETED_ACTIVE_SCAN
					@logger.info("Scan Completed")
					break
				end
			end
			if @epandesc
				break
			end
			@logger.info("Device not found... retrying...")
			sleep 1
		end

		@logger.info("Device found %p" % @epandesc)

		@logger.info("Getting IPv6 Address from MAC Address (%p)" % @epandesc[:addr])
		@ipv6_addr = @stack.command("SKLL64 #{@epandesc[:addr]}")

		@logger.info("Setting Channel and Pan ID")
		@stack.command("SKSREG S2 #{@epandesc[:channel]}") == "OK" or raise
		@stack.command("SKSREG S3 #{@epandesc[:pan_id]}") == "OK" or raise

		@logger.info("Starting PANA")
		@stack.command("SKJOIN #{@ipv6_addr}") == "OK" or raise
		while e = @events.pop
			case e[:num].to_i(16)
			when SKSTACK_IP::EVENT_PANA_COMPLETED
				break
			when SKSTACK_IP::EVENT_PANA_ERROR
				raise "pana error"
			end
		end
		@logger.info("PANA Completed")

		@tid = 0
	end

	def retrieve_power
		@tid += 1

		tid = @tid

		q = Queue.new
		@transactions[tid] = proc {|frame|
			q << frame
		}

		frame = ECHONET_Lite::Frame.new(
			ECHONET_Lite::EHD1,
			ECHONET_Lite::EHD2_DEFINED,
			@tid,
			ECHONET_Lite::EDATA.new(
				ECHONET_Lite::EOJ.new(0x05, 0xFF, 0x01),
				ECHONET_Lite::EOJ.new(0x02, 0x88, 0x01),
				0x62,
				1,
				[
					ECHONET_Lite::Property.new(
						0xe7,
						0x00,
						""
					)
				]
			)
		)

		handle = 1
		port_num = 3610
		sec = 1
		data = frame.pack
		p [:packed, data]
		@stack.command("SKSENDTO %s %s %04X %s %04X %s" % [
			handle,
			@ipv6_addr,
			port_num,
			sec,
			data.length,
			data
		])

		while e = @events.pop
			if e[:num].to_i(16) == SKSTACK_IP::EVENT_UDP_SENT
				unless e[:param].to_i(16) == 0 # success
					return nil
				end
				break
			end
		end

		ret = nil
		begin
			Timeout.timeout(5) do
				ret = q.pop
			end
		rescue Timeout::Error
			@logger.info "UDP Response Timeout"
			@transactions.delete(tid)
		end
		ret
	end
end

io = nil
if ENV["PORT"].nil?
	require 'socket'
	s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
	Thread.start do
		while l = s2.gets
			l.chomp!
			case l
			when "SKVER"
				s2 << "SKVER\r\n"
				s2 << "EVER 1.2.10\r\n"
				s2 << "OK\r\n"
			when "SKSREG SFE 0"
				s2 << "SKSREG SFE 0\r\n"
				s2 << "OK\r\n"
			when /^SKSETPWD/
				s2 << "OK\r\n"
			when /^SKSETRBID/
				s2 << "OK\r\n"
			when "SKSCAN 2 FFFFFFFF 6"
				s2 << "OK\r\n"
				s2 << "EVENT 20 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX\r\n"
				s2 << "EPANDESC\r\n"
				s2 << "  Channel:2F\r\n"
				s2 << "  Channel Page:09\r\n"
				s2 << "  Pan ID:A0E6\r\n"
				s2 << "  Addr:001C64000357XXXX\r\n"
				s2 << "  LQI:84\r\n"
				s2 << "  PairID:00AXXXXX\r\n"
				s2 << "EVENT 22 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX\r\n"
			when /^SKLL64/
				s2 << "FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
			when /^SKSREG S2/
				s2 << "OK\r\n"
			when /^SKSREG S3/
				s2 << "OK\r\n"
			when /^SKJOIN/
				s2 << "OK\r\n"
				s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 02\r\n"
				s2 << "EVENT 02 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
				s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0028 (����O�y\r\n"
				s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
				s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0068 h����O�z$�r%^a�;H)��#L�8�8/�4+�u\-����&ѨSM00000099021000000000000000AXXXXX\r\n"
				s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
				s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0054 T����O�{;�;/��4+�u\-����&Ѩ��?s�����r��2���0��D�R��\r\n"
				s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
				s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0058 X����O�|�  Q�{��(F���.�[�<\r\n"
				s2 << "EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00\r\n"
				s2 << "EVENT 25 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY\r\n"
			when /SKSENDTO/
				s2 << "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 \x10\x81\x00\x01\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04Z\r\n"
			end
		end
	end
	io = s1
else
	require 'serialport'

	begin
		io = SerialPort.new(
			"/dev/tty.usbserial-A500YQPG",
			115200,
			8,
			1,
			0
		)
	rescue Errno::EBUSY
		sleep 1
		retry
	end
end

c = SmartMeterController.new
c.start(io, {
	ID: "0000 00XX 0XXX 0000 0000 0000 XXXX XXXX".gsub(/ /, ''),
	PASS: "XXXX XXXX XXXX".gsub(/ /, ''),
})
loop do
	frame = c.retrieve_power
	unless frame
		puts "failed to get power"
		next
	end
	frame.edata.properties.each do |prop|
		p prop
		if prop.epc == 0xe7 && prop.pdc == 4
			watts = prop.edt.unpack("N")[0]
			p "#{watts} W"
		end
	end
end
  1. トップ
  2. tech
  3. スマートメータから瞬間消費電力を読むRubyのコード

先週火曜日は体調不良で休んだが、子供も熱を出してひきとることになったので、結局完全には休めなかった。

鼻炎と喉がすこし痛いぐらいなので耳鼻科にいって薬をもらっているけど治らない。というか鼻炎と喉はともかく全身倦怠感がひどくてなにもできない。

とりあえずやってみたレベルですが、Bルートサービスが利用開始できたので瞬間電力を読むところまでやってみました。

  • Bルートサービスの申込
  • Wi-SUN モジュールについて
  • ECHONET Lite について

あたりがポイントです。

Bルートサービス申込から

混んでることはわかっていたのであまり期待せずに申し込んでいます。スマートメータに変更していない状態で申し込んでおり、途中でスマートメータ切替工事が入るパターンです。

関東在住ですので、東京電力パワーグリッドの管轄です。

  1. 6月24日に申込
  2. 6月27日に「お客さま情報の照合を完了し、お申込みを受け付けましたので、お知らせいたします」メールがくる
  3. 7月上旬ぐらいに電気メータ交換について東京電力パワーグリッドから「7月15日〜8月10日の間で工事を実施させて頂きます」というお知らせがポスト投函(郵送ではない)される。「停電いたしません」が「ご在宅の場合で停電の了解がいただける場合は停電工事に変更させていただくことがあります」と書いてあった。立ち会いの必要なし。
  4. 7月27日にメーターの「取替完了のお知らせ」がポスト投函されており、メータが替わっていた。工事業者は関電工
  5. 8月3日にメールで『「電力メーター情報発信サービス(Bルートサービス)」パスワードのお知らせ』がくる
  6. 8月4日に郵便で『「電力メーター情報発信サービス(Bルートサービス)」認証IDお知らせ』がくる

できれば工事を見学をしたかったけど、帰宅したら替わってしまっていた。

備考:申込時に必要な供給地点特定番号について

ウェブの電気家計簿を使っている場合、ログインして以下のURLにアクセスするとわかります。

https://www.kakeibo.tepco.co.jp/dk/wmf/meterInfoReference/

電気家計簿を使ってない場合、検針票に書いてあると思います。

通信モジュール

Wi-SUN モジュールは今のところ個人でも手に入るのはロームのものしかなさそう。News Detail に書いてある通り。技適付きなので心配いらない。

RS Online で BP35A1, BP35A7, BP35A7 - accessories を買おうとしたが入手困難といわれてダメで、BP35A7 - accessories だけ送られてきて辛い。チップワンストップ だと在庫があった…… 個人とは取引しないと書いてあるが無視して個人事業主ということで登録して買った。

BP35A7 は既に廃盤?で、BP35A7A というのがある。BP35A1はそのまま。よくわからないがまとめて入手する方法がすくない。

マザーボードとして BP359C というのがあるが、これはUSBシリアル変換とかがついてるだけなので、あまり必要なさそう。

Amazon でもなぜか取り扱いがあって (マーケットプレイスではなく Amazon.co.jp 販売)

Amazon で買う場合 BP35A7 - accessories は高価すぎるので買わずに別のところで買ったほうがよさそう。BP35A7もなぜか高い。が、在庫探しが面倒なら買ってもいいのではないか…

BP35A1 についているコネクタに対応するコネクタは 20P3.0-JMCS-G-TF という JST のコネクタ。Digikey で 120円ぐらいで売っているので、Digikey を利用する際に買っておけばBP35A7相当品を自作できるが、基板設計とか考えるとやはり面倒なのとそこまで高価ではないので素直に買ったほうが良いでしょう。

どうも結構今だと高いので辛い感じがしますがそのうち安いプロダクトがでると信じたい。

モジュールの使いかた

最低限使うのに必要な配線は

  • VCC
  • GND
  • RXD
  • TXD

だけです。電源電圧は3.3Vなので注意。

UART 経由でコマンドを打てば良いので、モジュールとUSB-UART変換さえあればPCで簡単に試せます。

モジュールの仕様書などは「ROHM Sub-GHzシリーズ」サポートページというところから辿れる。

サンプルも用意されているし、コマンドリファレンスも親切に書いてある。

備考: Wi-SUN モジュールについて

Wi-SUN 自体は国際規格化されているけど、日本のスマートメータ用に産まれたというそもそもの経緯があるので、今のところ日本以外で需要がない。なのでモジュール自体が少ない。

Wi-SUN の認証を通っているのはウェブから一覧でみれる。ECHONET Route B から辿ればよさそう。ロームの以外にもあるはあるけど、手に入らない。

スマートメータからのデータ取得

アプリケーションレイヤーでのプロトコルは ECHONET Lite というものです。Wi-SUN の OSI参照モデルでの関係はWi-SUN ECHONET Profile 準拠通信制御ソフトウェア がわかりやすいです。

BP35A1 モジュールでは物理層からトランスポート層(TCP/UDP)及びセッション層(PANA : Protocol for Carrying Authentication for Network Access)までのサポートがありますので、実際の必要なのはアプリケーション層の実装のみです。

ECHONET Lite

仕様書は普通に公開されていて、しかも日本語です。とりあえず必要なのは「第2部 ECHONET Lite 通信ミドルウェア仕様」で、具体的には ECHONET Lite のフレーム構造を理解していればとりあえず通信できます。

ECHONET Lite は概念的には BLE の GATT に似たような仕組みです。「それぞれの機器のあるプロパティを読み書きする」ことができます。

サンプルコードと実行結果

全体的な処理の流れを Ruby で書いてみました。

このコード自体は例外を考慮しておらず全く応用性はありませんが、プロトコルの理解のためにはある程度有用だと思います。ECHONET Lite 部分に細かくコメントをつけています。

ハマりポイント

  • 接続後に「インスタンスリスト通知」というのがこちらから要求しなくても送られてきました
    • ちゃんと受信フレームをパースしないといけないですがこのコードではやってません
require 'serialport'

begin
	@port = SerialPort.new(
		"/dev/tty.usbserial-A500YQPG",
		115200,
		8,
		1,
		0
	)
rescue Errno::EBUSY
	sleep 1
	retry
end

puts "init"

def @port.send(line)
	$stdout.puts ">> #{line}"
	self.write(line + "\r\n")
end

def @port.wait(val)
	while
		line = self.gets.chomp
		$stdout.puts "<< #{line}"
		if val === line
			return Regexp.last_match
		end
	end
end


@port.set_encoding(Encoding::BINARY)

@port.send "SKVER\r\n"
@port.wait "OK"

# エコーバックをオフに
@port.send "SKSREG SFE 0\r\n"
@port.wait "OK"

# スペース区切りで通知されるが、実際は削除する
ROUTE_B_ID = "0000 0099 0210 0000 0000 0000 00XX XXXX".gsub(/ /, '')
ROUTE_B_PASS = "XXXX  XXXX  XXXX".gsub(/ /, '')

@port.send "SKSETPWD C #{ROUTE_B_PASS}"
@port.wait 'OK'

@port.send "SKSETRBID #{ROUTE_B_ID}"
@port.wait 'OK'

info = {}
@port.send 'SKSCAN 2 FFFFFFFF 6'
info[:sender] = @port.wait(/^EVENT 20 (?<sender>.+)/)[1]
@port.wait(/^EPANDESC/)
info[:channel]      = @port.wait(/^  Channel:(?<channel>.+)/)[1]
info[:channel_page] = @port.wait(/^  Channel Page:(?<channel_page>.+)/)[1]
info[:pan_id]       = @port.wait(/^  Pan ID:(?<pan_id>.+)/)[1]
# MACアドレス
info[:addr]         = @port.wait(/^  Addr:(?<addr>.+)/)[1]
info[:lqi]          = @port.wait(/^  LQI:(?<lqi>.+)/)[1]
info[:pair_id]      = @port.wait(/^  PairID:(?<pair_id>.+)/)[1]
p info
#<<   Channel:2F
#<<   Channel Page:09
#<<   Pan ID:A0E6
#<<   Addr:00XXXXXXXXXXXXXX
#<<   LQI:7D
#<<   PairID:00XXXXXX
@port.wait(/^EVENT 22/)

# macアドレスをipv6 アドレスへ変換
@port.send "SKLL64 #{info[:addr]}"
info[:ipv6_addr] = @port.wait(/[0-9A-F:]+/)

# channel
@port.send "SKSREG S2 #{info[:channel]}"
@port.wait "OK"

# pan id
@port.send "SKSREG S3 #{info[:pan_id]}"
@port.wait "OK"

@port.send "SKJOIN #{info[:ipv6_addr]}"
# 0x25:PANA による接続が完了した
@port.wait(/^EVENT 25/)

@tid = 0
while true

#    (1)Layer4 で UDP(User Datagram Protocol)、Layer3 で IP(Internet Protocol)、
#    を使用する場合
#    各 ECHONET Lite ノードは、それぞれ IP アドレスを持つ。IP アドレスの範囲、
#    取得方法は規定しない。1つの ECHONET Lite フレームは、1つの UDP パケッ
#    トにて転送する。UDP パケットにおける送信先 PORT 番号は、要求・応答・通知
#    等の種別に関わらず、常に 3610 とする。

    handle = 1
    port_num = 3610
    sec = 1 # 暗号化有効・有効でなければ送信しない

    # 上記の通り data は ECHONET Lite フレームとなる
    # ref. https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/ECHONET_lite_V1_12_jp/ECHONET-Lite_Ver.1.12_02.pdf
    ### ref. https://echonet.jp/wp/wp-content/uploads/pdf/General/Standard/Release/Release_G_revised/Appendix_G_revised.pdf
    @tid += 1
    data = [
        # EHD1 = ECHONET Lite 1 byte
        0b00010000,
        # EHD2 = 既定形式 1 byte
        0x81,
        # TID - トランザクションID 2 bytes
        @tid,

        # EDATA
        
        ## SEOJ - 送信元ECHONET Liteオブジェクト指定 3 bytes
        ### Class Group Code - 管理・操作関連機器クラスグループ
        0x05,
        ### Class Code - コントローラ
        0xFF,
        ### Instance Code
        0x01,

        ## DEOJ - 相手先ECHONET Liteオブジェクト指定 3 bytes
        ### Class Group Code - 住宅・設備関連機器クラスグループ
        0x02,
        ### Class Code - 低圧スマート電力量メータ
        0x88,
        ### Instance Code
        0x01,

        ## ESV - ECHONET Lite サービス 1 byte
        ## プロパティ値読み出し要求
        0x62,

        ## OPC - 処理プロパティ数
        0x01,

        ### EPC - プロパティ - 瞬時電力計測値
        #### 電力実効値の瞬時値を 1W 単位で示す。(結果は 4 bytes singed long)
        0xe7,
        ### PDC - EDTのバイト数
        0x00,
        ### EDT - プロパティデータ
        # なし
    ].pack("CCn CCC CCC C C CC".gsub(" ", ""))

    p data

    @port.send "SKSENDTO %s %s %04X %s %04X %s" % [
        handle,
        info[:ipv6_addr],
        port_num,
        sec,
        data.length,
        data
    ]

    # TODO use datalen to read
    res = @port.wait(/^ERXUDP (?<sender>\S+) (?<dest>\S+) (?<rport>\S+) (?<lport>\S+) (?<senderlla>\S+) (?<secured>\S+) (?<datalen>\S+) (?<data>\S+)/)
    p res
    # まず接続後にインスタンスリスト通知 がくる
    # \x10\x81\x00\x00
    # \x0E\xF0\x01 ノードプロファイル
    # \x0E\xF0\x01 ノードプロファイル
    # \x73 プロパティ値通知
    # \x01 1つ 
    # \xD5
    # \x04 4バイト
    # \x01\x02\x88\x01
    #
    # "\x10\x81\x00\x00\x0E\xF0\x01\x0E\xF0\x01s\x01\xD5\x04\x01\x02\x88\x01"
    p res[:data][-4..-1].unpack("N")
    sleep 3
end

@port.send "SKTERM"
@port.wait "ENT 27"

実行すると以下のようになります。一部アドレスなどは念のため置き換えています。最後のほうに出てくる[1097]が瞬間消費電力です。

init
>> SKVER
<< SKVER
<< EVER 1.2.10
<< OK
>> SKSREG SFE 0
<< SKSREG SFE 0
<< OK
>> SKSETPWD C XXXXXXXXXXXX
<< OK
>> SKSETRBID 000000000000000000000000000000XX
<< OK
>> SKSCAN 2 FFFFFFFF 6
<< OK
<< EVENT 20 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX
<< EPANDESC
<<   Channel:2F
<<   Channel Page:09
<<   Pan ID:A0E6
<<   Addr:001C64000357XXXX
<<   LQI:84
<<   PairID:00AXXXXX
{:sender=>"FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX", :channel=>"2F", :channel_page=>"09", :pan_id=>"A0E6", :addr=>"001C64000357XXXX", :lqi=>"84", :pair_id=>"00AXXXXX"}
<< EVENT 22 FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX
>> SKLL64 001C64000357XXXX
<< FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY
>> SKSREG S2 2F
<< OK
>> SKSREG S3 A0E6
<< OK
>> SKJOIN FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY
<< OK
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 02
<< EVENT 02 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0028 (����O�y
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0068 h����O�z$�r%^a�;H)��#L�8�8/�4+�u\-����&ѨSM00000099021000000000000000AXXXXX
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0054 T����O�{;�;/��4+�u\-����&Ѩ��?s�����r��2���0��D�R��
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 02CC 02CC 001C64000357XXXX 0 0058 X����O�|�  Q�{��(F���.�[�<
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< EVENT 25 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY


"\x10\x81\x00\x01\x05\xFF\x01\x02\x88\x01b\x01\xE7\x00"
>> SKSENDTO 1 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 0E1A 1 000E ���b�
<<
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< OK
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FF02:0000:0000:0000:0000:0000:0000:0001 0E1A 0E1A 001C64000357XXXX 1 0012 ���s��
#<MatchData "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FF02:0000:0000:0000:0000:0000:0000:0001 0E1A 0E1A 001C64000357XXXX 1 0012 \x10\x81\x00\x00\x0E\xF0\x01\x0E\xF0\x01s\x01\xD5\x04\x01\x02\x88\x01" sender:"FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY" dest:"FF02:0000:0000:0000:0000:0000:0000:0001" rport:"0E1A" lport:"0E1A" senderlla:"001C64000357XXXX" secured:"1" datalen:"0012" data:"\x10\x81\x00\x00\x0E\xF0\x01\x0E\xF0\x01s\x01\xD5\x04\x01\x02\x88\x01">
[16943105]
"\x10\x81\x00\x02\x05\xFF\x01\x02\x88\x01b\x01\xE7\x00"
>> SKSENDTO 1 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 0E1A 1 000E ���b�
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 ���r�Z
#<MatchData "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 \x10\x81\x00\x01\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04Z" sender:"FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY" dest:"FE80:0000:0000:0000:021D:1290:0003:A4F1" rport:"0E1A" lport:"0E1A" senderlla:"001C64000357XXXX" secured:"1" datalen:"0012" data:"\x10\x81\x00\x01\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04Z">
[1114]
"\x10\x81\x00\x03\x05\xFF\x01\x02\x88\x01b\x01\xE7\x00"
>> SKSENDTO 1 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 0E1A 1 000E ���b�
<<
<< EVENT 21 FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY 00
<< OK
<< ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 ���r�I
#<MatchData "ERXUDP FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY FE80:0000:0000:0000:XXXX:XXXX:XXXX:XXXX 0E1A 0E1A 001C64000357XXXX 1 0012 \x10\x81\x00\x02\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04I" sender:"FE80:0000:0000:0000:YYYY:YYYY:YYYY:YYYY" dest:"FE80:0000:0000:0000:021D:1290:0003:A4F1" rport:"0E1A" lport:"0E1A" senderlla:"001C64000357XXXX" secured:"1" datalen:"0012" data:"\x10\x81\x00\x02\x02\x88\x01\x05\xFF\x01r\x01\xE7\x04\x00\x00\x04I">
[1097]

実際はイベントや UDP の処理を適切に書く必要があります。ちゃんとやらないとダメなので、適切なライブラリがあると嬉しそうです。

  1. トップ
  2. tech
  3. スマートメータのBルートサービスで Wi-SUN モジュールを使って瞬間消費電力を読み出す