├── assets ├── logo.png ├── web-ui.png └── WT8266-S1 DataSheet V1.0.pdf ├── LICENSE ├── README.md └── ir_controller.yaml /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCBP/ORVIBO-CT30W-ESPHome/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/web-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCBP/ORVIBO-CT30W-ESPHome/HEAD/assets/web-ui.png -------------------------------------------------------------------------------- /assets/WT8266-S1 DataSheet V1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCBP/ORVIBO-CT30W-ESPHome/HEAD/assets/WT8266-S1 DataSheet V1.0.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CCBP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](./assets/logo.png) 2 | 3 | # 基于ORVIBO CT30W的ESPHome空调红外遥控器 4 | 5 | ## 功能特性 6 | 7 | 本项目基于ORVIBO CT30W,通过修改固件的方式刷入ESPHome,实现支持格力(Gree)`Kelvinator`协议的红外遥控器,支持如下功能: 8 | 9 | - 作为红外遥控空调 10 | - 开关空调 11 | - 定时关机 12 | - 模式选择(自动、制冷、干燥、通风、制热) 13 | - 温度调整(16℃~25℃) 14 | - 风速调整(1~5) 15 | - 水平扫风 16 | - 垂直扫风(固定位置以及自动扫动) 17 | - 静音模式 18 | - 强力模式 19 | - 灯光开关 20 | - 离子过滤 21 | - XFan模式 22 | - 配置断电保存 23 | - 支持红外接收 24 | - 具有系统状态指示灯 25 | - 提供Web UI与接入Home Assistant以及其他ESPHome所支持的功能 26 | 27 | 其中红外接收功能主要是用来实现学码功能,使用此功能首先需要打开Web UI,然后**按住**设备边缘的按键,用其他的红外遥控器对准设备按下想要学码的按键,这样就可以在Web UI的日志中看到接收到的数据,之后如何将获取到的红外数据应用到设备中请参考[这个文档](https://github.com/crankyoldgit/IRremoteESP8266/wiki/Adding-support-for-a-new-AC-protocol#a-note-on-collecting-data)自行尝试。 28 | 29 | 另外对于系统状态指示灯关联了所有ESPHome组件,可以指示设备的状态,灯光的含义如下: 30 | 31 | - 当警告处于活动状态时,缓慢闪烁(大约每秒一次); 32 | - 当出现错误时,快速闪烁(每秒多次); 33 | - 其他情况保持关闭。 34 | 35 | ## Web UI 36 | 37 | ![Web UI](./assets/web-ui.png) 38 | 39 | 在设备成功启动并接入WiFi后,在浏览器中打开`http://`即可访问Web UI,在上图的`Sensor and Control`面板中即可实现对空调的红外遥控。 40 | 41 | ## 如何使用 42 | 43 | 1. 参照[官方文档](https://esphome.io/guides/installing_esphome)完成ESPHome的安装; 44 | 2. 将此仓库拉取到本地: 45 | 46 | ``` 47 | git clone https://github.com/CCBP/ORVIBO-CT30W-ESPHome.git 48 | ``` 49 | 50 | 3. 打开配置文件`ir-controller.yaml`,根据实际情况修改下方配置: 51 | 52 | ``` 53 | # Enable Home Assistant API 54 | api: 55 | encryption: 56 | key: 57 | 58 | ota: 59 | - platform: esphome 60 | password: 61 | 62 | wifi: 63 | ssid: 64 | password: 65 | 66 | # Enable fallback hotspot (captive portal) in case wifi connection fails 67 | ap: 68 | ssid: "Ir-Controller Fallback Hotspot" 69 | password: 70 | ``` 71 | 72 | 其中`api.encryption.key`的配置可以使用[官方文档](https://esphome.io/components/api.html#configuration-variables)中所给出随机生成的密钥;`ota.password`与`wifi.ap.password`可以任意配置一个自己喜欢的密码;`wifi.ssid`与`wifi.password`根据实际情况填写即可。 73 | 74 | 4. 生成工程并尝试编译 75 | 76 | ``` 77 | esphome compile ir-controller.yaml 78 | ``` 79 | 80 | :warning: **注意**:这次编译由于缺少头文件将会**报错**属于正常情况。 81 | 82 | 5. 添加头文件 83 | 84 | 为了实现红外收发功能,需要包含`IRremoteESP8266`库中的一些头文件`IRremoteESP8266.h`、`ir_Kelvinator.h`、`IRsend.h`、`IRrecv.h`和`IRac.h`,为此修改`ir-controller.yaml`如下(头文件所在路径可能需要根据实际情况修改): 85 | 86 | ``` 87 | esphome: 88 | name: ir-controller 89 | libraries: 90 | - IRremoteESP8266 91 | includes: 92 | - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRremoteESP8266.h 93 | - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/ir_Kelvinator.h 94 | - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRsend.h 95 | - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRrecv.h 96 | - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRac.h 97 | ``` 98 | 99 | 由于ESPHome在生成cpp代码时会将`includes`组件放在`globals`组件之后,这导致声明在`globals`中`IRremoteESP5266`的自定义变量将会缺失,因此需要手动修改`.esphome/build/ir-controller/src/main.cpp`,补充头文件在`main.cpp`的**最开始**(注意要**放在自动生成代码之外**)。 100 | 101 | ``` 102 | #include "ir_Kelvinator.h" 103 | #include "IRrecv.h" 104 | // Auto generated code by esphome 105 | // ========== AUTO GENERATED INCLUDE BLOCK BEGIN =========== 106 | ``` 107 | 108 | :question: 对于头文件的操作极为别扭,但我一番搜索下来并未发现优雅的解决办法,如果谁有更好的解决方案还望指教~ 109 | 110 | 6. 连接USB转TTL串口模块 111 | 112 | 将USB转TTL串口模块**选择为3.3V模式**,并按照如下关系进行连接(使用杜邦线即可,可能需要焊接操作): 113 | 114 | | USB转TTL串口模块 | ORVIBO CT30W | 115 | |------------------|--------------| 116 | |TXD |RX | 117 | |RXD |TX | 118 | |GND |GND | 119 | 120 | 之后将USB转TTL串口模块连接到PC上即可(如果需要安装驱动请自行搜索学习)。 121 | 122 | 7. 编译并开始上传: 123 | 124 | ``` 125 | esphome run ir-controller.yaml 126 | ``` 127 | 128 | **注意**:在编译完成后选择通过串口上传到设备中前,需**将`GPIO0`在拉低情况下上电以进入刷机模式**。 129 | 130 | 8. 访问WebUI 131 | 132 | 待完成编译并上传到设备中后,根据提示将`RST`引脚拉低完成复位动作即可开始运行我们编写的程序,随后稍待片刻便可看到日志输出。 133 | 134 | 在日志中找到`wifi`组件的如下输出: 135 | 136 | ``` 137 | [14:26:55][C][wifi:600]: WiFi: 138 | [14:26:55][C][wifi:428]: Local MAC: 139 | [14:26:55][C][wifi:433]: SSID: 140 | [14:26:55][C][wifi:436]: IP Address: 192.168.1.125 141 | [14:26:55][C][wifi:439]: BSSID: 142 | [14:26:55][C][wifi:441]: Hostname: 'ir-controller' 143 | [14:26:55][C][wifi:443]: Signal strength: -49 dB ▂▄▆█ 144 | [14:26:55][C][wifi:447]: Channel: 1 145 | [14:26:55][C][wifi:448]: Subnet: 255.255.255.0 146 | [14:26:55][C][wifi:449]: Gateway: 192.168.1.1 147 | [14:26:55][C][wifi:450]: DNS1: 192.168.1.1 148 | [14:26:55][C][wifi:451]: DNS2: 0.0.0.0 149 | ``` 150 | 151 | 其中`IP Address`便是设备的IP地址,之后便可访问`http://`访问Web UI以控制设备。 152 | 153 | ## 自定义 154 | 155 | 本项目基于ORVIBO CT30W开发,因此所用引脚均根据CT30W中的硬件连接所定,但理论上只要是ESP8266都可以使用此项目。并且由于我家中的设备是格力的空调设备,所以本项目仅适配了其对应的`Kelvinator`协议,但理论上`IRremoteESP8266`库中列出[支持的IR协议](https://github.com/crankyoldgit/IRremoteESP8266/blob/master/SupportedProtocols.md)都是可以修改此项目进行适配的,但是因为IR协议众多并且我也没有对应的设备测试,所以这里不再进行描述,如果有问题欢迎提issue与我交流。 156 | 157 | ### 引脚修改 158 | 159 | 本项目所使用的4个引脚的功能机修改位置如下: 160 | 161 | - `GPIO14`:输出,用于控制红外发射二极管,即实现红外遥控的功能,修改`globals`组件`id`为`ir_send_pin`的`initial_value`的值即可; 162 | - `GPIO5`:输入,用于读取红外接收二极管的电平,实现学码的功能,修改`globals`组件`id`为`ir_recv_pin`的`initial_value`的值即可; 163 | - `GPIO4`:输入,用于读取按键电平,仅当按键按下时才开启红外接收功能,修改`binary_sensor`组件`pin`中的`number`的值即可; 164 | - `GPIO15`:输出,用于控制LED状态,指示当前系统状态,修改`status_led`组件的`pin`的值即可。 165 | 166 | # 参考 167 | 168 | - https://github.com/crankyoldgit/IRremoteESP8266 169 | - https://esphome.io/guides/getting_started_command_line 170 | - https://www.bilibili.com/opus/967982171394408465?jump_opus=1 171 | - https://github.com/web1n/ORVIBO-CT30W 172 | -------------------------------------------------------------------------------- /ir_controller.yaml: -------------------------------------------------------------------------------- 1 | esphome: 2 | name: ir-controller 3 | libraries: 4 | - IRremoteESP8266 5 | # includes: 6 | # - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRremoteESP8266.h 7 | # - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/ir_Kelvinator.h 8 | # - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRsend.h 9 | # - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRrecv.h 10 | # - .esphome/build/ir-controller/.piolibdeps/ir-controller/IRremoteESP8266/src/IRac.h 11 | on_boot: 12 | priority: -100 13 | then: 14 | - lambda: |- 15 | id(ac) = new IRKelvinatorAC(id(ir_send_pin)); 16 | id(ac)->begin(); 17 | id(ac)->setFan(id(ac_fan_speed).state); 18 | auto index = id(ac_mode).active_index(); 19 | if (index.has_value()) id(ac)->setMode(index.value()); 20 | id(ac)->setTemp(id(ac_temp).state); 21 | index = id(ac_swing_vertical).active_index(); 22 | if (index.has_value()) { 23 | if (id(ac_swing_vertical).state == "auto" || 24 | id(ac_swing_vertical).state == "LowAuto" || 25 | id(ac_swing_vertical).state == "MiddleAuto") { 26 | id(ac)->setSwingVertical(true, index.value()); 27 | } else if (id(ac_swing_vertical).state == "HighAuto") { 28 | id(ac)->setSwingVertical(true, index.value() + 1); 29 | } else { 30 | id(ac)->setSwingVertical(false, index.value()); 31 | } 32 | } 33 | id(ac)->setSwingHorizontal(id(ac_swing_horizontal).state); 34 | id(ac)->setXFan(id(ac_xfan).state); 35 | id(ac)->setIonFilter(id(ac_ion_filter).state); 36 | id(ac)->setLight(id(ac_light).state); 37 | id(ac)->setQuiet(id(ac_quiet).state); 38 | id(ac)->setIonFilter(id(ac_turbo).state); 39 | 40 | id(irrecv) = new IRrecv(id(ir_recv_pin), id(ir_recv_buf_size), 41 | id(ir_recv_timeout), true); 42 | id(irrecv)->setUnknownThreshold(id(ir_recv_ignore)); 43 | id(irrecv)->setTolerance(id(ir_recv_tolerance)); 44 | id(irrecv)->enableIRIn(); 45 | on_shutdown: 46 | priority: 700 47 | then: 48 | - lambda: |- 49 | delete id(ac); 50 | 51 | web_server: 52 | port: 80 53 | version: 3 54 | 55 | esp8266: 56 | board: esp_wroom_02 57 | 58 | # Enable logging 59 | logger: 60 | level: INFO 61 | 62 | # Enable Home Assistant API 63 | api: 64 | encryption: 65 | key: 66 | 67 | ota: 68 | - platform: esphome 69 | password: 70 | 71 | wifi: 72 | ssid: 73 | password: 74 | 75 | # Enable fallback hotspot (captive portal) in case wifi connection fails 76 | ap: 77 | ssid: "Ir-Controller Fallback Hotspot" 78 | password: 79 | 80 | captive_portal: 81 | 82 | status_led: 83 | pin: 15 84 | 85 | globals: 86 | - id: ir_send_pin 87 | type: const uint16_t 88 | restore_value: no 89 | initial_value: "14" 90 | - id: ir_recv_pin 91 | type: const uint16_t 92 | restore_value: no 93 | initial_value: "5" 94 | - id: ac 95 | type: IRKelvinatorAC * 96 | restore_value: no 97 | initial_value: "NULL" 98 | - id: ac_off_time 99 | type: ESPTime 100 | restore_value: no 101 | initial_value: "{0}" 102 | - id: ir_recv_buf_size 103 | type: uint16_t 104 | restore_value: no 105 | initial_value: "1024" 106 | - id: ir_recv_timeout 107 | type: uint8_t 108 | restore_value: no 109 | initial_value: "50" 110 | - id: ir_recv_ignore 111 | type: uint16_t 112 | restore_value: no 113 | initial_value: "12" 114 | - id: ir_recv_tolerance 115 | type: uint8_t 116 | restore_value: no 117 | initial_value: "25" 118 | - id: irrecv 119 | type: IRrecv * 120 | restore_value: no 121 | initial_value: "NULL" 122 | 123 | binary_sensor: 124 | - platform: gpio 125 | id: ir_dump_pin 126 | name: "IR dump" 127 | icon: "mdi:record-rec" 128 | pin: 129 | number: 4 130 | inverted: true 131 | mode: 132 | input: true 133 | pullup: true 134 | filters: 135 | - delayed_on: 10ms 136 | web_server: 137 | sorting_weight: 12 138 | 139 | interval: 140 | - interval: 100ms 141 | then: 142 | - lambda: |- 143 | decode_results results; 144 | if (id(ir_dump_pin).state && id(irrecv)->decode(&results)) { 145 | if (results.overflow) ESP_LOGW("ir_dump", "Buffer is full"); 146 | ESP_LOGI("ir_dump", "%s", resultToTimingInfo(&results).c_str()); 147 | } 148 | 149 | event: 150 | - platform: template 151 | event_types: 152 | - "ac_conf_change" 153 | name: "Apply AC configuration" 154 | id: ac_apply_event 155 | internal: true 156 | on_event: 157 | then: 158 | - lambda: |- 159 | if (id(ac) && id(ac_power).state) id(ac)->send(); 160 | 161 | switch: 162 | - platform: template 163 | id: ac_power 164 | name: "Power" 165 | icon: "mdi:car-defrost-rear" 166 | web_server: 167 | sorting_weight: 0 168 | optimistic: true 169 | restore_mode: RESTORE_DEFAULT_OFF 170 | turn_on_action: 171 | - lambda: |- 172 | if (id(ac)) { 173 | id(ac)->on(); 174 | id(ac)->send(); 175 | ESP_LOGI("ac_power", "Configurations: %s", id(ac)->toString().c_str()); 176 | } 177 | turn_off_action: 178 | - lambda: |- 179 | if (id(ac)) { 180 | id(ac)->off(); 181 | id(ac)->send(); 182 | auto call = id(ac_timed_off).make_call(); 183 | call.set_time("00:00:00"); 184 | call.perform(); 185 | } 186 | - platform: template 187 | id: ac_swing_horizontal 188 | name: "Horizontal swing" 189 | icon: "mdi:swap-horizontal-bold" 190 | web_server: 191 | sorting_weight: 5 192 | optimistic: true 193 | restore_mode: RESTORE_DEFAULT_OFF 194 | turn_on_action: 195 | - lambda: |- 196 | if (id(ac)) { 197 | id(ac)->setSwingHorizontal(true); 198 | id(ac_apply_event).trigger("ac_conf_change"); 199 | } 200 | turn_off_action: 201 | - lambda: |- 202 | if (id(ac)) { 203 | id(ac)->setSwingHorizontal(false); 204 | id(ac_apply_event).trigger("ac_conf_change"); 205 | } 206 | - platform: template 207 | id: ac_xfan 208 | name: "XFan" 209 | icon: "mdi:radiator" 210 | web_server: 211 | sorting_weight: 10 212 | optimistic: true 213 | restore_mode: RESTORE_DEFAULT_ON 214 | turn_on_action: 215 | - lambda: |- 216 | if (id(ac)) { 217 | id(ac)->setXFan(true); 218 | id(ac_apply_event).trigger("ac_conf_change"); 219 | } 220 | turn_off_action: 221 | - lambda: |- 222 | if (id(ac)) { 223 | id(ac)->setXFan(false); 224 | id(ac_apply_event).trigger("ac_conf_change"); 225 | } 226 | - platform: template 227 | id: ac_ion_filter 228 | name: "Ion filter" 229 | icon: "mdi:electron-framework" 230 | web_server: 231 | sorting_weight: 11 232 | optimistic: true 233 | restore_mode: RESTORE_DEFAULT_OFF 234 | turn_on_action: 235 | - lambda: |- 236 | if (id(ac)) { 237 | id(ac)->setIonFilter(true); 238 | id(ac_apply_event).trigger("ac_conf_change"); 239 | } 240 | turn_off_action: 241 | - lambda: |- 242 | if (id(ac)) { 243 | id(ac)->setIonFilter(false); 244 | id(ac_apply_event).trigger("ac_conf_change"); 245 | } 246 | - platform: template 247 | id: ac_light 248 | name: "Light" 249 | icon: "mdi:brightness-6" 250 | web_server: 251 | sorting_weight: 7 252 | optimistic: true 253 | restore_mode: RESTORE_DEFAULT_ON 254 | turn_on_action: 255 | - lambda: |- 256 | if (id(ac)) { 257 | id(ac)->setLight(true); 258 | id(ac_apply_event).trigger("ac_conf_change"); 259 | } 260 | turn_off_action: 261 | - lambda: |- 262 | if (id(ac)) { 263 | id(ac)->setLight(false); 264 | id(ac_apply_event).trigger("ac_conf_change"); 265 | } 266 | - platform: template 267 | id: ac_quiet 268 | name: "Quiet" 269 | icon: "mdi:volume-off" 270 | web_server: 271 | sorting_weight: 8 272 | optimistic: true 273 | restore_mode: RESTORE_DEFAULT_OFF 274 | turn_on_action: 275 | - lambda: |- 276 | if (id(ac)) { 277 | id(ac)->setQuiet(true); 278 | id(ac_apply_event).trigger("ac_conf_change"); 279 | } 280 | turn_off_action: 281 | - lambda: |- 282 | if (id(ac)) { 283 | id(ac)->setQuiet(false); 284 | id(ac_apply_event).trigger("ac_conf_change"); 285 | } 286 | - platform: template 287 | id: ac_turbo 288 | name: "Turbo" 289 | icon: "mdi:speedometer" 290 | web_server: 291 | sorting_weight: 9 292 | optimistic: true 293 | restore_mode: RESTORE_DEFAULT_OFF 294 | turn_on_action: 295 | - lambda: |- 296 | if (id(ac)) { 297 | id(ac)->setTurbo(true); 298 | id(ac_apply_event).trigger("ac_conf_change"); 299 | } 300 | turn_off_action: 301 | - lambda: |- 302 | if (id(ac)) { 303 | id(ac)->setTurbo(false); 304 | id(ac_apply_event).trigger("ac_conf_change"); 305 | } 306 | 307 | number: 308 | - platform: template 309 | id: ac_fan_speed 310 | name: "Fan speed" 311 | icon: "mdi:fan" 312 | min_value: 1 313 | max_value: 5 314 | step: 1 315 | web_server: 316 | sorting_weight: 4 317 | optimistic: true 318 | restore_value: true 319 | on_value: 320 | then: 321 | lambda: |- 322 | if (id(ac)) { 323 | id(ac)->setFan(id(ac_fan_speed).state); 324 | id(ac_apply_event).trigger("ac_conf_change"); 325 | } 326 | - platform: template 327 | id: ac_temp 328 | name: "Temperatrue" 329 | icon: "mdi:thermometer" 330 | min_value: 16 331 | max_value: 30 332 | step: 1 333 | web_server: 334 | sorting_weight: 3 335 | optimistic: true 336 | restore_value: true 337 | on_value: 338 | then: 339 | lambda: |- 340 | if (id(ac)) { 341 | id(ac)->setTemp(id(ac_temp).state); 342 | id(ac_apply_event).trigger("ac_conf_change"); 343 | } 344 | 345 | select: 346 | - platform: template 347 | id: ac_mode 348 | name: "Mode" 349 | icon: "mdi:order-bool-ascending-variant" 350 | options: 351 | - "Auto" 352 | - "Cool" 353 | - "Dry" 354 | - "Fan" 355 | - "Heat" 356 | initial_option: "Auto" 357 | web_server: 358 | sorting_weight: 2 359 | optimistic: true 360 | restore_value: true 361 | on_value: 362 | then: 363 | lambda: |- 364 | if (id(ac)) { 365 | auto index = id(ac_mode).active_index(); 366 | if (index.has_value()) { 367 | id(ac)->setMode(index.value()); 368 | id(ac_apply_event).trigger("ac_conf_change"); 369 | } 370 | } 371 | - platform: template 372 | id: ac_swing_vertical 373 | name: "Vertical swing" 374 | icon: "mdi:swap-vertical-bold" 375 | options: 376 | - "Off" 377 | - "Auto" 378 | - "Highest" 379 | - "UpperMiddle" 380 | - "Middle" 381 | - "LowerMiddle" 382 | - "Lowest" 383 | - "LowAuto" 384 | - "MiddleAuto" 385 | - "HighAuto" 386 | initial_option: "Auto" 387 | web_server: 388 | sorting_weight: 6 389 | optimistic: true 390 | restore_value: true 391 | on_value: 392 | then: 393 | lambda: |- 394 | if (id(ac)) { 395 | auto index = id(ac_swing_vertical).active_index(); 396 | if (index.has_value()) { 397 | if (id(ac_swing_vertical).state == "auto" || 398 | id(ac_swing_vertical).state == "LowAuto" || 399 | id(ac_swing_vertical).state == "MiddleAuto") { 400 | id(ac)->setSwingVertical(true, index.value()); 401 | } else if (id(ac_swing_vertical).state == "HighAuto") { 402 | id(ac)->setSwingVertical(true, index.value() + 1); 403 | } else { 404 | id(ac)->setSwingVertical(false, index.value()); 405 | } 406 | id(ac_apply_event).trigger("ac_conf_change"); 407 | } 408 | } 409 | 410 | time: 411 | - platform: sntp 412 | id: sntp_time 413 | timezone: Asia/Shanghai 414 | servers: 415 | - ntp.ntsc.ac.cn 416 | - ntp.aliyun.com 417 | - time.windows.com 418 | on_time: 419 | - seconds: 0 420 | minutes: /1 421 | then: 422 | lambda: |- 423 | if (id(ac) && id(ac_off_time).is_valid() && 424 | id(sntp_time).now() >= id(ac_off_time)) { 425 | if (id(ac_power).state) { 426 | id(ac_power).turn_off(); 427 | ESP_LOGI("sntp_time", "[%s] A/C powered off", 428 | id(sntp_time).now().strftime("%m-%d %H:%M:%S").c_str()); 429 | } 430 | auto call = id(ac_timed_off).make_call(); 431 | call.set_time("00:00:00"); 432 | call.perform(); 433 | } 434 | 435 | datetime: 436 | - platform: template 437 | id: ac_timed_off 438 | name: "Timed off" 439 | icon: "mdi:calendar-alert" 440 | type: time 441 | initial_value: "00:00:00" 442 | web_server: 443 | sorting_weight: 1 444 | optimistic: yes 445 | disabled_by_default: true 446 | on_value: 447 | then: 448 | - lambda: |- 449 | ESPTime time_set = id(ac_timed_off).state_as_esptime(); 450 | if (id(sntp_time).now().is_valid()) { 451 | if (time_set.hour != 0 || time_set.minute != 0) { 452 | id(ac_off_time) = id(sntp_time).now(); 453 | id(ac_off_time).hour += time_set.hour; 454 | id(ac_off_time).minute += time_set.minute; 455 | id(ac_off_time).second = 0; 456 | if (id(ac_off_time).minute > 59) { 457 | id(ac_off_time).hour += 1; 458 | id(ac_off_time).minute -= 59; 459 | } 460 | if (id(ac_off_time).hour > 23) { 461 | id(ac_off_time).day_of_month += 1; 462 | id(ac_off_time).hour -= 23; 463 | } 464 | id(ac_off_time).recalc_timestamp_local(); 465 | ESP_LOGI("ac_timed_off", "[%s] Turn off at %s", 466 | id(sntp_time).now().strftime("%m-%d %H:%M:%S").c_str(), 467 | id(ac_off_time).strftime("%m-%d %H:%M:%S").c_str()); 468 | } else { 469 | id(ac_off_time).year = 0; 470 | id(ac_off_time).month = 0; 471 | id(ac_off_time).day_of_month = 0; 472 | id(ac_off_time).hour = 0; 473 | id(ac_off_time).minute = 0; 474 | id(ac_off_time).second = 0; 475 | id(ac_off_time).recalc_timestamp_local(); 476 | } 477 | } else 478 | ESP_LOGW("ac_timed_off", "SNTP not finished yet."); 479 | --------------------------------------------------------------------------------