この記事は、CyberAgent Developers Advent Calendar 2025 22 日目の記事です。

こんにちは。株式会社 WinTicket の@seipanです。Serverチームに所属しサービス開発を行っています。

背景

クリスマスも近づきすっかり寒くなってきました。寒くなると活躍するのがエアコンと鍋ですね。私は一人暮らしですが、部屋の温度をエアコンで管理しています。しかし、部屋の温度はエアコンだけでコントロールされるわけではありません。例えば冬の代名詞である鍋を食べたりすると、それだけで部屋の温度がグッとあがり、エアコンを消したり設定温度を下げたりすることは往々にしてあると思います。

そうなると、わざわざエアコンの設定温度を下げて、また寒くなってきたら上げるみたいな操作が煩わしくなってきます。これを自動化したいなと考えました。展望としては設定温度を最初に定義して、定期的に現在の室温を確認して室温によってあるべき設定温度になるように、エアコンの設定温度を上げたり下げたりしたいです。

「あるべき状態(ユーザーが設定した状態)と、実際のシステムの状態を比較し、差分があればそれを埋めるための処理を実行」どこかで聞いたことがある言葉だと思います。そうですKubernetesのReconciliation loopと同じですよね。

私は自宅にKubernetesクラスタを飼っているので、そことの親和性を含めてKubernetesOperatorを自作し、その中で部屋の温度をコントロールすることにしました。

作成したものはこちらです。よければStarください。

https://github.com/seipan/thermo-pilot-controller

前提

Kubernetes Operator

Kubernetes OperatorとはKubernetes上でアプリケーションやミドルウェアの運用作業を自動化する仕組みです。DBの初期化・バックアップ・リストアなど人でやるには大変な作業をKubernetesにしてもらいます。例えばMysqlのOperatorであるMOCOなどがあります。

Kubernetes OperatorはCustom Resource Definition(CRD)と、そのCRDを制御するCustom Controllerとの組み合わせにより構成されます。

宣言的オペレーション

Kubernetesが実現するべき状態。CRDに定義された状態(今回だと部屋の温度)を正の状態とし、Operator内に実装したReconciliation loopにより、今の状態を正の状態に収束させる。

Reconciliation Loop

CRDに定義された状態を正の状態として今の状態を正の状態に収束させるループ。

SwitchBot

私の家ではSwitchBotを使ってスマート家電化をしてるので、エアコンをKubernetes Operatorから操作するためにSwitchBotのAPIを使用します。

今回使用するのはSwitchBot HubMiniとSwitchBot 温湿度計Proです。APIに関する操作は以下のGithubのリポジトリを参考にしています。

https://github.com/OpenWonderLabs/SwitchBotAPI

Device

SwitchBotに接続しているデバイスの一覧を取得します。このAPIを使用して温度計・エアコンを見つけます。

GET https://api.switch-bot.com/v1.1/devices

Status

部屋の温度を取得するのに使います。

GET https://api.switch-bot.com/v1.1/devices/{deviceId}/status

Command

実際にエアコンに対する操作を行います。

POST https://api.switch-bot.com/v1.0/devices/{deviceId}/commands
{
  “command”: “setAll”,
  “parameter”: “26,2,1,on”, // 26度で冷房モードautoファンスピードで電源on
  “commandType”: “command”
}

実装

CRD

実際にKubernetesOperatorを実装していきます。まずはCRDを定義してみます。

必要なものは目標にしたい温度・許容できる温度幅・エアコンの運転モード・使用する温度計のタイプ・SwitchBotのAPIの認証に必要なtoken,secretです。token, secretはCRDにベタ書きにするより、KubernetesのSecretから読み込ませるようにしたいです。

そこで以下のようなCRDを考えました。ThermoPilotという名はChatGPTが考えてくれました。使用感としては設定温度・運転モード・閾値を設定するだけでSwitchBotに接続しているエアコン・温度計を取得してきて温度調節を行ってくれます。

apiVersion: thermo-pilot.yadon3141.com/v1
kind: ThermoPilot
metadata:
  labels:
    app.kubernetes.io/name: thermo-pilot-controller
    app.kubernetes.io/managed-by: kustomize
  name: thermopilot-sample
spec:
  secretRef:
    name: switchbot-credentials
    tokenKey: token
    secretKey: secret
  # airConditionerId: "02-202509160308-19770636"  # Optional - if not specified, controls all air conditioners
  temperatureSensorType: MeterPro
  targetTemperature: "25.0"
  threshold: "1.0"
  mode: cool

CustomController

生成

次にCustomControllerの実装を始めていきます。CustomControllerの実装に用いるライブラリとしてメジャーなものにkubebuilderというものがあります。今回はkubebuilderを使用して実装していきます。

まずはprojectの雛形を作成していきます。domainには自身が保有する固有のdomainを、repoには対象のリポジトリを選択します。

kubebuilder init --domain yadon3141.com --repo https://github.com/seipan/thermo-pilot-controller

次にAPIの雛形を作成します。これにより基本的なカスタムリソース・カスタムコントローラーが生成されます。

kubebuilder create api --group thermo-pilot --version v1 --kind ThermoPilot
INFO Create Resource [y/n]
y
INFO Create Controller [y/n]
y

実装

まずはCRDで定義した通りに構造体を変更します。Secretや目標にしたい温度などはrequiredにエアコンのdeviceIDは無くても問題ないため、optionalにします。

type ThermoPilotSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// SwitchBot API credentials stored in a Secret
	// +required
	SecretRef SecretReference `json:"secretRef"`

	// Device IDs for controlling temperature
	// +optional
	AirConditionerID string `json:"airConditionerId,omitempty"`

	// Type of temperature sensor to use (e.g., MeterPro)
	// +kubebuilder:validation:Enum=MeterPro
	// +required
	TemperatureSensorType string `json:"temperatureSensorType"`

	// Temperature control settings
	// +kubebuilder:validation:Pattern=^([1-3][0-9]|[1-9])(\.[0-9])?$
	// +required
	TargetTemperature string `json:"targetTemperature"`
	// +kubebuilder:validation:Pattern=^[0-5](\.[0-9])?$
	// +kubebuilder:default="1.0"
	// +optional
	Threshold string `json:"threshold,omitempty"`

	// Air conditioner mode: cool or heat
	// +kubebuilder:validation:Enum=cool;heat
	// +required
	Mode string `json:"mode"`
}

フロー

次にコアロジックであるReconcileについて考えていきます。https://github.com/seipan/thermo-pilot-controller/tree/main/internal/controllerにある、Reconcileを関数を編集します。

// +kubebuilder:rbac:groups=thermo-pilot.yadon3141.com,resources=thermopilots,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=thermo-pilot.yadon3141.com,resources=thermopilots/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=thermo-pilot.yadon3141.com,resources=thermopilots/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (r *ThermoPilotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

Reconcile関数内での具体的なフローについては以下のように考えました。目標の温度になるように現在の温度と比較してエアコンを操作します。
室温コントロールのReconcile内のフロー図
Reconcile関数では気をつけるべきポイントがいくつか存在します。その中でも結果が必ず収束するように実装しなければならないというものがあります。Reconcile関数の結果が冪等でないとそれは宣言的とは言い難いからです。今回の例で行くと何度Reconcileが行われても最終的に目標の温度に収束させないといけません。つまり現在の温度が目標の温度より高くても低くても目標の温度を目指すような処理にしないといけません。なので以下のような処理を行うようにしました。

状況 モード アクション エアコン設定温度
暑い 冷房 冷房ON 目標温度
暑い 暖房 温度下げる 目標温度 – 3度
寒い 冷房 温度上げる 目標温度 + 3度
寒い 暖房 暖房ON 目標温度

ループ

Reconcileは関数内でctrl.Result構造体を設定したりすることにより、ループの再実行の時間を決めることができます。SwitchbotのRateLimitは10000/1dayです。一回のReconcile関数内でSwitchBotAPIを呼び出す回数は最大で4回です(温度センサー取得+現在の温度取得+エアコンの取得+エアコン制御)。なのでReconcile関数は10000/4 = 2500, 2500/24(24時間) = 104なので1分以上間隔であればRateLimitを気にせず動作することができます。

Secret

SwitchBotのtoken,secretをKubernetesのSecretで管理するようにしたので、CustomController内でSecretを取得してそのtokenを使ってSwitchBotAPIに認証させる必要があります。controller-runtimeのclientを使って実装できます。

secret := &corev1.Secret{}
err := c.Get(ctx, types.NamespacedName{
	Name:      secretRef.Name,
	Namespace: namespace,
}, secret)

RBACでSecretのget, list, watchの権限が必要なので追加する必要があります。

// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch

実装

あとはフロー通りに実装していきます。airConditionerIdが指定されてない場合はSwitchBotに繋いてるエアコンを全て取得してきて、すべてのエアコンに適用します。

動かしてみる

実際に動かしてみます。我が家では以下の設定で運用しています。暖房で21度で閾値1度で運用します。

apiVersion: thermo-pilot.yadon3141.com/v1
kind: ThermoPilot
metadata:
  labels:
    app.kubernetes.io/name: thermo-pilot-controller
    app.kubernetes.io/managed-by: kustomize
  name: thermopilot-sample
spec:
  secretRef:
    name: switchbot-credentials
    tokenKey: token
    secretKey: secret
  temperatureSensorType: MeterPro
  targetTemperature: "21.0"
  threshold: "1.0"
  mode: heat

これで実際に数時間運用した時の室温の遷移が以下になります。
実際の室温
最初は21度を目指して上がっていきますが22度を超えたところから設定温度が18度に変わり、温度が下がっていくのがわかると思います。

まとめ

今回はKubernetesOperatorを自作してSwitchBotを使って室温の自動調節に取り組みました。KubernetesOperatorを作成するのは初めてでしたが、kubebuilderとChatGPTがかなり優秀でほとんど詰まるところなく実装できてよかったです。KubernetesOperatorはさまざまな用途に応用できそうです。

アバター画像
2025新卒入社のバックエンドエンジニアです。現在はWinTicketでバックエンド開発を行っています