masahitechのアイコン画像

ペットのトカゲを現地と同じ環境で飼育する日照管理システムの構築

masahitech 2021年05月16日に作成  (2021年05月16日に更新)

ペットのトカゲを現地と同じ環境で飼育する日照管理システムの構築

私は現在,アフリカ北部の主にマリ共和国に生息する「マリトゲオアガマ」と,アフリカ南部に生息する「コモンフラットロックリザード」というトカゲを飼育しています.
マリトゲオアガマ (下)とコモンフラットロックリザード (上)
現地ではアフリカの強い日差しを浴びて生活しているトカゲたちです.
太陽光からの強い紫外線を浴びて体内時計を調整しているので,飼育する際は紫外線ライトの設置が必須です.
飼育下では,ライトがつく時間と消える時間を手動で設定できるプログラムタイマーを使って,紫外線を浴びせる時間を制御するのが一般的です.

しかし,実際の日の出や日の入りの時間は毎日変わります.
プログラムタイマーは一度設定したらその時間にしか作動せず,現地と同じように日の出や日の入りの時間を制御するためには,設定を毎日手動で変える必要があります.
これは大変な労力ですし,時には外出中で設定をできない日もあり,現実的ではありません.

そこで,
日の出や日の入りの時間をプログラムで計算し,現地の環境にできるだけ近くなるように紫外線ライトを制御しよう!
と考えました.

システム構成

今回実装した,日照管理システムの構成図は以下のとおりです.
日照管理システム構成図
紫外線ライトはAC100Vのコンセントにつなぎます.
対して,obnizは最大で約DC5Vの出力しかできません.
そこで,obniznのDC5V出力を利用して,AC100Vをオンオフするリレー回路を組みました.
今回は秋月電子で売られているソリッド・ステート・リレー (SSR)キットを組み立てて,obnizとコンセントの間に挟みました.
また,最終目標は紫外線ライトの制御ですが,紫外線ライトは大電流が流れるため,慎重に回路を組む必要があります.
これは安全のために,もう少し時間をかけながらゆっくり工作していきたいので,今回はスマホの充電器とスマホを接続し,充電できているかどうかでシステム自体の動作を検証します.

部品

  • obniz Board 1Y
  • ソリッド・ステート・リレー (購入先:秋月電子のリンク)
  • ガラス管ヒューズ,ヒューズボックス
  • ACコンセント
  • ACアウトレット
  • コンセントに電気が来ていることが確認できるもの (今回はスマホとその充電器)

プログラム

ソースコードの全体像はとても長いので,記事の最後に貼りました.
上記と同じシステム構成を構築し,プログラムを実行すれば,実現できます.
以下,プログラムの内容を解説します.

ウェブ実行画面

ウェブ実行画面
プログラムの実行後は,ウェブ上の実行画面で次の情報が確認できます.表示自体はhtmlによって行われていて,表示する内容は,後に記述するjavascriptによる日の出と日の入りの計算 (以下,日照計算)プログラムで決定しています.

  1. Time Now
    Japan :日本の現在時刻
    Mali :マリ共和国の現在時刻
  2. Sunlight
    Sunrise :マリ共和国の日の出時刻
    Sunset :マリ共和国の日の入り時刻
  3. Sunset Now. or Sunrise Now.
    Sunset Now. :太陽が出ていない状態.(夜)
    Sunrise Now. :太陽が出ている状態.(昼)
  4. Daytime Today
    ? [h] ? [m] ? [s] :今日の太陽が出ている時間の長さを表示

今回は,マリトゲオアガマの原産国であるマリ共和国の時間に合わせて日照計算をしています (外務省,マリ共和国).
次に,javascriptで記述された部分の説明をします.

javascriptによる表示内容の計算

現在時刻

日本とマリ共和国の現在時刻は,javascriptのDateオブジェクトを使って出力しています.ちなみに,マリ共和国のタイムゾーンはUTC+0なので計算が楽で助かりました.

日照計算

日照計算には,以下ソースコード内の自作クラスSunlightと,Timeを使用しています.

Sunlight

日の出時刻と日の入り時刻の計算を担当するクラスです.
太陽がいつどこから昇るかは,Sunrise equationを解いて決定できます.
Sunrise equationは,以下の3つを用いた天文学的な計算を行います (Wikipedia, Sunrise equation).

  1. ユリウス通日
  2. 緯度
  3. 経度
    この中で見慣れないのは,ユリウス通日だと思います.

ユリウス通日(ユリウスつうじつ、Julian Day、JD)とは、ユリウス暦[注釈 1]紀元前4713年1月1日、すなわち西暦 -4712年1月1日の正午(世界時)からの日数である (Wikipedia, ユリウス通日)

ユリウス通日は,年月日時分秒を,小数点も含めた日の単位で表すことができます.そのため,日数などを計算するのに便利で天文学などでよく使われているそうです.
Sunlight クラスはSunrise equationを実装したものです.上の3つからユリウス通日で表された日の出と日の入り時刻を計算して返します.
ただし,ユリウス通日のままでは私たちにはわかりにくいので,一般に使っているグレゴリオ暦に変換する必要があります.
このグレゴリオ暦→ユリウス通日変換とユリウス通日→グレゴリオ暦を行うのがTimeクラスです.

Time

Astro Commons, グレゴリオ暦からユリウス通日を求めるを参考にして実装を行いました.
ユリウス通日は正午を起点にした計算方法を採用しているので,適宜0.5日の修正を行いながら計算しました.
日の出と日の入りのユリウス通日→グレゴリオ暦の変換は,年月日の計算に床関数を使っている都合で何もない状態からはできないので,現在時刻の年月日も利用して工夫しました.

Sunset or Sunrise Now

主にソースコードの最後,クラス外での処理です.
現在ユリウス通日が日の出ユリウス通日の前なら,Sunset.
現在ユリウス通日が日の出ユリウス通日の後で日の入りユリウス通日の前なら,Sunrise.
現在ユリウス通日が日の入りユリウス通日の後なら,Sunsetと表示します.

Daytime Today

今日のマリ共和国でのSunriseとSunsetの間の時間を計算して表しています.
計算は,Time関数内のcalcDayTimeで行っていて,日の出ユリウス通日と日の入りユリウス通日の差分をとって,グレゴリオ暦に直し,今日マリでの昼の長さを教えてくれます.

デモ動画

デモは安全のために,スマホとその充電器で行いました.
紫外線ライトも100Vではあるのですが,ワット数が非常に大きいのでよく調べて回路に間違いがないか確認してから試そうと思っています.(おそらく今回のリレー回路では定格電流が足りないのでは?と思っている)

まとめ

今回は,obniz Board 1Yを無償で提供していただけるということで,コンテストに参加してみました.
実はこのアイディア自体は以前に,Raspberry Pi Picoで実現しようとしたものでした.
しかし,Raspberry Pi Picoは時間を管理する機能が存在せず,単体ではネットに接続して時間を取得することもできないので諦めていたものでした.
obnizはクラウドにプログラムを置いて実行するので,時間の管理が容易です.
また,このシステムは基本的には紫外線ライトのオンオフの1日2回しか動作しないので,本体が最小構成で済むobnizは日照管理システムを実現するのに最適だと思いました.

最後に今後の課題を2つ示して終えたいと思います.
一つ目に,今回のプログラムでは,現状1秒毎に計算して,現在昼か夜か判別しています.
しかし,コンセントのオンオフは1日2回で良いので,ずっとプログラムが走っている必要はないと思っています.
今後,深夜0時にその日の日の出と日の入り時刻を計算し,obnizの定期実行を利用してその時間になったらデバイスを立ち上げるということをしたいです.
二つ目は,時差の問題です,マリと日本には時差が9時間あります.
このまま使っていると,日本の夜中に電気がついてしまうことがあります.
今後,昼時間と夜時間およびその比率は維持しつつ,時差だけ修正して日本時間でもできるだけ違和感ないようにライトを制御できたらいいなと思っています.

ソースコードの全体像

日照管理システムのプログラム

<html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" /> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://unpkg.com/obniz@3.14.0/obniz.js" crossorigin="anonymous" ></script> </head> <body> <div id="obniz-debug"></div> <div class="time-now"> <h3 class="text-center" style="color:red">Time Now</h3> <div> <h4 class="text-center"> Japan : <strong id="current-jst"></strong><br> Mali : <strong id="current-mst"></strong><br><br> </h4> </div> </div> <div class="sunlight-today"> <h3 class="text-center" style="color:red">Sunlight</h3> <div> <h4 class="text-center"> Sunrise : <strong id="sunrise"></strong><br> Sunset : <strong id="sunset"></strong><br><br> <i><u><h3 id="sunlight" class="text-center" style="color:red"></h3></u></i><br> </h4> </div> </div> <div class="day-time-today"> <h3 class="text-center" style="color:red">Daytime Today</h3> <div> <h4 class="text-center"> <strong id="day-hour"></strong> [h] <strong id="day-minute"></strong> [m] <strong id="day-second"></strong> [s] <br> </h4> </div> </div> <script> class Sunlight { constructor(julianDate, longitude, latitude) { this.julianDate = julianDate; this.longitude = longitude; this.latitude = latitude; } // calculater functions calcCurrentJulianDay() { this.currentJulianDay = this.julianDate - 2451545.0 + 0.0008; } calcMeanSolarTime() { this.meanSolarTime = this.currentJulianDay - this.longitude / 360; } calcSolarMeanAnomaly() { this.solarMeanAnomaly = (357.5291 + 0.98560028*this.meanSolarTime) % 360; } calcEquationOfTheCenter() { this.equationOfTheCenter = 1.9148 * Math.sin(this.degreeToRadian(this.solarMeanAnomaly)) + 0.0200 * Math.sin(this.degreeToRadian(2 * this.solarMeanAnomaly)) + 0.0003 * Math.sin(this.degreeToRadian(3 * this.solarMeanAnomaly)); } calcEclipticLongitude() { this.eclipticLongitude = (this.solarMeanAnomaly + this.equationOfTheCenter + 180 + 102.9372) % 360; } calcSolarTransit() { this.solarTransit = 2451545.0 + this.meanSolarTime + 0.0053 * Math.sin(this.degreeToRadian(this.solarMeanAnomaly)) - 0.0069 * Math.sin(this.degreeToRadian(2 * this.eclipticLongitude)); } calcDeclinationOfTheSun() { let sinDeclinationOfTheSun = Math.sin(this.degreeToRadian(this.eclipticLongitude))*Math.sin(this.degreeToRadian(23.44)); this.declinationOfTheSun = this.radianToDegree(Math.asin(sinDeclinationOfTheSun)); } calcHourAngle() { let numerator = Math.sin(this.degreeToRadian(- 0.83)) - Math.sin(this.degreeToRadian(this.latitude))*Math.sin(this.degreeToRadian(this.declinationOfTheSun)); let denominator = Math.cos(this.degreeToRadian(this.latitude))*Math.cos(this.degreeToRadian(this.declinationOfTheSun)); let cosHourAngle = numerator / denominator; this.hourAngle = this.radianToDegree(Math.acos(cosHourAngle)); } calcSunrise() { this.julianDateOfSunrise = this.solarTransit - this.hourAngle / 360 + 0.5; } calcSunset() { this.julianDateOfSunset = this.solarTransit + this.hourAngle / 360 + 0.5; } run() { this.calcCurrentJulianDay(); this.calcMeanSolarTime(); this.calcSolarMeanAnomaly(); this.calcEquationOfTheCenter(); this.calcEclipticLongitude(); this.calcSolarTransit(); this.calcDeclinationOfTheSun(); this.calcHourAngle(); this.calcSunrise(); this.calcSunset(); } // utility functions degreeToRadian(degree) { return degree * (Math.PI / 180); } radianToDegree(radian) { return radian / (Math.PI / 180); } } class Time { constructor () { this.update(); } update() { this.date = new Date(); console.log(this.date) this.setDate(); } specify(dateSpecify) { this.date = dateSpecify; console.log(this.date) this.setDate(); } setDate() { this.year = this.date.getUTCFullYear(); this.month = this.date.getUTCMonth() + 1; this.day = this.date.getUTCDate(); this.hour = this.date.getUTCHours(); this.minute = this.date.getUTCMinutes(); this.second = this.date.getUTCSeconds(); this.fixYearAndMonth(); this.calcJulianDate(); } fixYearAndMonth() { if (this.month == 1 || this.month == 2) { this.month += 12; this.year -= 1; } } calcJulianDate() { return Math.floor(365.25 * this.year) + Math.floor(this.year / 400) - Math.floor(this.year / 100) + Math.floor(30.59 * (this.month - 2)) + this.day + 1721088.5; //+ this.hour / 24 //+ this.minute / 1440 //+ this.second / 86400 } calcJulianDateNow() { return Math.floor(365.25 * this.year) + Math.floor(this.year / 400) - Math.floor(this.year / 100) + Math.floor(30.59 * (this.month - 2)) + this.day + 1721088.5 + this.hour / 24 + this.minute / 1440 + this.second / 86400; } calcGregorianDate(julian) { let dateJul = new Date(this.date.getTime()); let diffDate = julian - this.julianDate; console.log(diffDate); console.log(this.julianDate); console.log(julian); let diffHour = Math.floor(24 * diffDate); dateJul.setUTCHours(diffHour); console.log(diffHour); let diffMinute = Math.floor(60 * (24 * diffDate - diffHour)); dateJul.setUTCMinutes(diffMinute); console.log(diffMinute); let diffSecond = Math.floor(60 * (24 * 60 * diffDate - 60 * diffHour - diffMinute)); dateJul.setUTCSeconds(diffSecond); console.log(diffSecond); return dateJul; } calcDayTime(sunriseTime, sunsetTime) { let diffDayTime = sunsetTime - sunriseTime; let diffDayHour = Math.floor(24 * diffDayTime); console.log(diffDayHour); let diffDayMinute = Math.floor(60 * (24 * diffDayTime - diffDayHour)); console.log(diffDayMinute); let diffDaySecond = Math.floor(60 * (24 * 60 * diffDayTime - 60 * diffDayHour - diffDayMinute)); console.log(diffDaySecond); return [diffDayHour, diffDayMinute, diffDaySecond]; } get julianDate() { return this.calcJulianDate(); } get julianDateNow() { return this.calcJulianDateNow(); } } var obniz = new Obniz("3569-2712"); obniz.onconnect = async function() { obniz.display.clear(); //let countUp = 0 //検証用 const manageSunLight = () => { let timeNow = new Time(); //timeNow.specify(new Date(2021, 4, 16, 13, 49, countUp++)); //検証用 $("#current-jst").text(timeNow.date.toString()); $("#current-mst").text(timeNow.date.toUTCString()); // Republic of Mali, 17.5706, -3.9961 let sunLight = new Sunlight(timeNow.julianDate, 17.5706, - 3.9961); sunLight.run(); let diffDayTime = timeNow.calcDayTime(sunLight.julianDateOfSunrise, sunLight.julianDateOfSunset) $("#day-hour").text(diffDayTime[0]); $("#day-minute").text(diffDayTime[1]); $("#day-second").text(diffDayTime[2]); $("#sunrise").text(timeNow.calcGregorianDate(sunLight.julianDateOfSunrise).toUTCString()); $("#sunset").text(timeNow.calcGregorianDate(sunLight.julianDateOfSunset).toUTCString()); obniz.display.clear(); console.log(timeNow.julianDateNow) console.log(sunLight.julianDateOfSunrise) console.log(sunLight.julianDateOfSunset) if (timeNow.julianDateNow < sunLight.julianDateOfSunrise) { obniz.display.print("Sunset Now."); $("#sunlight").text("Sunset Now."); obniz.io1.output(false); obniz.io0.output(false); }else if (timeNow.julianDateNow < sunLight.julianDateOfSunset) { obniz.display.print("Sunrise Now."); $("#sunlight").text("Sunrise Now."); obniz.io1.output(false); obniz.io0.output(true); }else { obniz.display.print("Sunset Now."); $("#sunlight").text("Sunset Now."); obniz.io1.output(false); obniz.io0.output(false); } } setInterval(manageSunLight, 1000) }; </script> </body> </html>
masahitechのアイコン画像
趣味で電子工作をしている大学院生です.
  • masahitech さんが 2021/05/16 に 編集 をしました。 (メッセージ: 初版)
  • masahitech さんが 2021/05/16 に 編集 をしました。
  • Opening
    masahitechのアイコン画像 masahitech 2021/05/19

    コメントありがとうございます!
    これだと1500Wまで対応しているようですし,ライトでも問題なさそうですね!
    ちなみにSwitchBotだと,単体で毎日違う時間にオンオフするような設定も可能ですか?

    takeruのアイコン画像 takeru 2021/05/20

    曜日+時間でスケジュール設定ができるので1週間後ぐらいまでの予約は出来そうです。
    あとはGoogle、Amazon、ifttt などで操作できるのでobnizからweb apiを叩けばいいかなと思って初めのコメントをしたのですが、obnizいらなくなっちゃいますね。
    サーボで物理ボタン押しましょうか?

    masahitechのアイコン画像 masahitech 2021/05/21

    なるほど...SwitchBotも便利ですね
    サーボで物理ボタン押すのはシュールでおもしろいですね笑

    3 件の返信が折りたたまれています
ログインしてコメントを投稿する