カメニッキ

カメとインコと釣りの人です

eSensorとeRemoteによって外出時にペット部屋の状況を知る・状況に応じて家電操作する

困っていたこと

  • 今日おもったより気温下がったけど、ペット部屋の温度が不安
  • おもったより帰宅遅くなって、22時に部屋が明るいまま…

僕と妻どちらも家を留守にする時間が長いため、この辺困ってました。

これまでどうしていたか

  • 安全側に倒してエアコン常時つけっぱ > 電気代3万↑
  • タイマーコンセントを使ってスポットライトを自動オン・オフ > あんまり明るくないし、直接光で目への影響が不安

さらにさらに

  • 実際、昼間何℃になってるのか知りたくて、ネットワークカメラ越しに温度計を写すということなぞもしていた f:id:tapira:20180509174559p:plain

. . . . . .

電気代つらいし、カメラで温度計みるの虚しいのでなんとかしたい

解決するために必要なもの

  • eSensor
  • eRemote

eSensor

以下のことが計測できます

  • 温度
  • 湿度
  • 明るさ
  • 空気の流れ

また、今回諸事情により利用していませんが以下のように環境条件をトリガーにリモコン操作が可能です - 温度が○℃を下回った - 温度が○℃を上回った - 湿度が○%を上回った

スマートフォンのアプリから以下のように確認できる f:id:tapira:20180509221154j:plain

eRemote

いわゆるスマートリモコンとして作動し、かつ外部からネットワーク経由で指示できます

  • 外からエアコンの電源を入れる
  • 外からエアコンの温度設定を変更する
  • 外から照明を消す
  • 時刻設定でリモコン操作する

f:id:tapira:20180509221845j:plain

できるようになったこと

  • OK 今日おもったより気温下がったけど、ペット部屋の温度が不安
  • OK おもったより帰宅遅くなって、22時に部屋が明るいまま…

今後やりたいこと

環境条件をトリガーにして自動的にエアコンの設定を変更する機能を活用したい。 相性が悪いのか、エアコンの動作モードを指定してからの条件設定がうまくいかないが、機能としては存在するので使えるようにしたい。

例) - 24℃を下回ったら暖房26℃設定でエアコンをオン - 27℃を上回ったらエアコンをオフ - 30℃を上回ったら冷房でエアコンをオン

リモコン対応していない加湿器をリモート操作したい

例) - 湿度50%を下回ったら加湿器をオン

あったらいいな

  • リモート餌やり装置
  • リモート水循環装置

おしまい

fluentdでログ中の特定キーワード出現回数を集計しTreasure Dataへインポートする

やりたいこと

外部からのアクセスを、ある条件をもとにブロックした時、/var/log/nginx/blocked.log というファイルにログ記録しています。 集計単位(10分毎とか)ごとに、その件数をカウントし、Treasure Dataへインポートするためにやったことのメモです。

前提

  • 全部ログ突っ込んで抜き出したら? > ログ量が膨大なので...

必要なtd-agent-gem

  • キーワードをもとにログ行数をカウントするのに使います github.com

  • Treasure Dataへインポートするために使います* github.com

conf

<source>
  @type tail
  format /^.+nginx: (?<time>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?<log_level>\w+)\] (?<pid>\d+).(?<tid>\d+): (?<message>.*)$/
  time_format %Y/%m/%d %H:%M:%S
  path /var/log/nginx/%Y%m%d/blocked.log_%Y%m%d_%H
  pos_file /var/log/nginx/blocked.log.pos
  tag nginx-blocked.log
</source>

<match nginx-blocked.log>
  type grepcounter
  count_interval 600
  input_key message
  regexp BLOCKED
  threshold 1
  add_tag_prefix blocked.count
</match>

<filter blocked.count.nginx-blocked.log>
  @type record_transformer
  enable_ruby
  remove_keys message,input_tag,input_tag_last
  <record>
    time ${Time.now.to_i}
  </record>
</filter>

<match blocked.count.nginx-blocked.log>
  @type tdlog
  apikey xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  auto_create_table
  database test
  table blocked_count
  buffer_type file
  buffer_path /var/log/td-agent/buffer/td
  flush_interval 600s
</match>

補足

  • /var/log/nginx/%Y%m%d/blocked.log_%Y%m%d_%H をtailで監視します。
  format /^.+nginx: (?<time>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?<log_level>\w+)\] (?<pid>\d+).(?<tid>\d+): (?<message>.*)$/
  time_format %Y/%m/%d %H:%M:%S

でログ形式・Time形式を指定しており、該当したものに nginx-blocked.log tagを付与しています。

  • ここで行数のカウントをおこなっています。 内容としては 600sec 単位に BLOCKED キーワードを抜き出しています。 threshold 1 と本来の使い方では回数がしきい値超えた時にmailする、などとなるようですが、今回は全て記録したいため 1 としました。

  • filterを使用して以降の処理 tdlog で {"time": 1234567, "count": 123 }のようなjson形式でTreasure Dataへなげれるように整形しています。 record内でrubyを使いたいのでenable_ruby grepcounterから使わないキーも渡ってきているのでremove_keys message,input_tag,input_tag_lastで削除します。 recordディレクティブ内でtime` というキーでunixtimestampを列追加しています

  • Treasure Dataにデータインポートしています。 使い方は標準の内容なので割愛


  • Apacheのログを全部TDに突っ込む
  • 条件に一致したnginxログをTDに突っ込む

のような方法はすぐ見つかったのですが、集計した個数を一行TDに突っ込む方法が見つからず苦戦しました。(そもそもTDの使い方にあっているか謎) 参考までに

Fukuoka.go#9でLTさせてもらった

connpass.com

当日、急遽参加させてもらうことになって、いそいそお昼休みに資料作ってなんとか間に合った

gitpitch.com


内容は初歩的なもので、ここ数ヶ月golangに触れる機会があって、その時ハマったgolangのゼロ値によるものでした。 トーク前に某golang貴公子に「それもっといい方法ありますよ」とプレッシャー掛けられた この辺使えと sql - The Go Programming Language


僕は人前で喋るのも資料作るのも苦手なので、これらのイベントの喋る側は避けて通ってるけど、
喋る側としてイベント参加したほうが絶対おもしろいな〜と、PHPカンファレンス福岡に引き続き思った。。。


『Goならわかるシステムプログラミング』www.lambdanote.com

面白そうな本が予約受付してたのでポチッた

おしまい

gormのUpdateColumnsでモデル内のboolゼロ値(false)を持ったカラムが更新されなかった の続き

前回の記事(http://tapira.hatenablog.com/entry/2017/08/09/173718)で以下の通りよくわからんくて飛ばしてたところを、gormのコードを追ってみた。

func (s DB) UpdateColumns(values interface{}) DB { ここで呼ばれたタイミングではvaluesに値が渡ってきてるけど、その後instanceにセットしてcallback呼んだタイミングで消えてるので、 Setするどっかのロジックでゼロ値だとスキップするような動きがあると思うけどどこかわからんかった・・・


  • UpdateColumns の呼び出し
db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
  • gorm/callback_update.goのinit関数にて、assignUpdatingAttributesCallbackがコールバック関数に登録されます
// Define callbacks for updating
func init() {
  DefaultCallback.Update().Register("gorm:assign_updating_attributes", assignUpdatingAttributesCallback)
  • その後callCallbacksの呼び出しによって、登録されたコールバック関数が実行されます
func (s *DB) UpdateColumns(values interface{}) *DB {
  return s.clone().NewScope(s.Value).
    Set("gorm:update_column", true).
    Set("gorm:save_associations", false).
    InstanceSet("gorm:update_interface", values).
    callCallbacks(s.parent.callbacks.updates).db
}
  • assignUpdatingAttributesCallbackの中身
// assignUpdatingAttributesCallback assign updating attributes to model
func assignUpdatingAttributesCallback(scope *Scope) {
  if attrs, ok := scope.InstanceGet("gorm:update_interface"); ok {
    if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate {
      scope.InstanceSet("gorm:update_attrs", updateMaps)
    } else {
      scope.SkipLeft()
    }
  }
}

以下の処理で UpdateColumns(&eventExAfter) で渡された値をupdate_attrsにセットしている

    if updateMaps, hasUpdate := scope.updatedAttrsWithValues(attrs); hasUpdate {
      scope.InstanceSet("gorm:update_attrs", updateMaps)
  • updatedAttrsWithValuesの中を追いかけてみると、以下
func (scope *Scope) updatedAttrsWithValues(value interface{}) (results map[string]interface{}, hasUpdate bool) {
  if scope.IndirectValue().Kind() != reflect.Struct {
    return convertInterfaceToMap(value, false), true
  }

  results = map[string]interface{}{}

  for key, value := range convertInterfaceToMap(value, true) {
    if field, ok := scope.FieldByName(key); ok && scope.changeableField(field) {

ゼロ値の場合convertInterfaceToMapにて該当カラムが更新対象列から消える - convertInterfaceToMapは以下

func convertInterfaceToMap(values interface{}, withIgnoredField bool) map[string]interface{} {
  var attrs = map[string]interface{}{}

  switch value := values.(type) {
  case map[string]interface{}:
    return value
  case []interface{}:
    for _, v := range value {
      for key, value := range convertInterfaceToMap(v, withIgnoredField) {
        attrs[key] = value
      }
    }
  case interface{}:
    reflectValue := reflect.ValueOf(values)

    switch reflectValue.Kind() {
    case reflect.Map:
      for _, key := range reflectValue.MapKeys() {
        attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface()
      }
    default:
      for _, field := range (&Scope{Value: values}).Fields() {
        if !field.IsBlank && (withIgnoredField || !field.IsIgnored) {
          attrs[field.DBName] = field.Field.Interface()
        }
      }
    }
  }
  return attrs
}
  • 今回はinterface{}で値を受けており、ゼロ値が渡されたカラムは以下の処理でfalseと判定され、更新対象リストにアペンドされていないっぽい
        if !field.IsBlank && (withIgnoredField || !field.IsIgnored) {
          attrs[field.DBName] = field.Field.Interface()
        }
  • 確認してみるとゼロ値のカラムについてはIsBlank=trueとなっていた。IsBlankのセットは以下のコードでやっているぽい
func (field *Field) Set(value interface{}) (err error) {
・・・
field.IsBlank = isBlank(field.Field)
  • isBlankの実装を見てみると
func isBlank(value reflect.Value) bool {
  switch value.Kind() {
  case reflect.String:
    return value.Len() == 0
  case reflect.Bool:
    return !value.Bool()
  case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    return value.Int() == 0
  case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
    return value.Uint() == 0
  case reflect.Float32, reflect.Float64:
    return value.Float() == 0
  case reflect.Interface, reflect.Ptr:
    return value.IsNil()
  }

  return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

!value.Bool() false渡したらisBlank=true …

bool型がtrueかfalseしかもてない以上、こうなるのはやむないのか。 ここにたどり着くのにめちゃくちゃ時間かかってしまった。 もっと素早くコードリーディングできるようになりたい

今回お世話になったツール

Visual Studio Code(Mac版)のデバッグ機能

gormのUpdateColumnsでモデル内のboolゼロ値(false)を持ったカラムが更新されなかった

schema

mysql> desc events;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id    | int(10)      | YES  |     | NULL    |       |
| name  | varchar(255) | YES  |     | NULL    |       |
| flag  | tinyint(1)   | YES  |     | NULL    |       |
+-------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

問題

type Event struct {
  Id   uint   `json:"id"`
  Name string `json:"name"`
  Flag bool  `json:"flag"`
}

func gormConnect() *gorm.DB {
  db, err := gorm.Open("mysql", "root@tcp(localhost:3306)/sample")

  if err != nil {
    panic(err.Error())
  }
  return db
}

func hoge(c echo.Context) {
        db := gormConnect()

        // 初期データ作成
        eventEx := Event{Id: 1, Name: "taro", Flag: true}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
}

Createのタイミングで以下のINSERT文が発行される

INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',1);

Updateで以下のSQL発行を期待するが、、、

UPDATE `events` SET `name` = 'jiro', `flag` = 0  WHERE `events`.`id` = 1;

実際には以下のSQLが発行される

UPDATE `events` SET `name` = 'jiro'  WHERE `events`.`id` = 1;

以下のとおり、bool型ゼロ値ではないtrueを指定すると

func main() {
        db := gormConnect()

        // 初期データ作成
        eventEx := Event{Id: 1, Name: "taro", Flag: true}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: true} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
}

期待どおり動作する

INSERT INTO `events` (`id`,`name`,`flag`) VALUES (1,'taro',0);
UPDATE `events` SET `name` = 'jiro', `flag` = 1  WHERE `events`.`id` = 1;

なんでこうなるのか

func (s *DB) UpdateColumns(values interface{}) *DB { ここで呼ばれたタイミングではvaluesに値が渡ってきてるけど、その後instanceにセットしてcallback呼んだタイミングで消えてるので、
Setするどっかのロジックでゼロ値だとスキップするような動きがあると思うけどどこかわからんかった・・・
追記 以下でした

tapira.hatenablog.com

以下のように map[string]interface{}{} に更新パラメータをバインドさせてUpdateすれば意図した動きになるが、Structに定義されたカラム全てが載ってしまい、思った使い方ができなかった。

event := map[string]interface{}{}
    if err := c.Bind(&params); err != nil {
        return err
    }

期待どおりに動かすには

boolポインタ型を使用する。

--- a/gorm-sample.go
+++ b/gorm-sample.go
@@ -10,7 +10,7 @@ import (
 type Event struct {
        Id   uint   `json:"id"`
        Name string `json:"name"`
-       Flag bool   `json:"flag"`
+       Flag *bool  `json:"flag"`
 }

 func gormConnect() *gorm.DB {
@@ -24,15 +24,17 @@ func gormConnect() *gorm.DB {

 func main() {
        db := gormConnect()
+       ptrue := &[]bool{true}[0]
+       pfalse := &[]bool{false}[0]

        // 初期データ作成
-       eventEx := Event{Id: 1, Name: "taro", Flag: false}
+       eventEx := Event{Id: 1, Name: "taro", Flag: ptrue}
        result := db.Create(eventEx)
        fmt.Println(result)

        // 初期データのID=1を更新する
        eventExBefore := Event{Id: 1}
        db.First(&eventExBefore)

        eventExAfter := new(Event)
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)
 }

こうするとboolポインタ型のゼロ値はnilとなり、falseと値を指定した場合も意図した動きになる。

        eventExAfter := map[string]interface{}{}
        // {Name: "jiro", Flag: false} という値がPUTされてきたとする
        c.Bind(eventExAfter)
        db.Model(&eventExBefore).UpdateColumns(&eventExAfter)

ちなみに、この挙動は github.com/go-playground/validator を使用した際にも同じだった。 flag: falseと指定していた場合もrequire指定でエラーになってしまう。

uintなどを使用した場合にもゼロ値が 0 であるため、0で更新したい場合などは↑のようにuintポインタ型にしないとだめそう。 他にうまい回避方法がわからなくて、こんなことしたけど実はイケてるやり方があったら知りたい。。。