我彻底弃用HA模版Jinja了,因为Python更好更容易 [Pyscript插件]
本帖最后由 relliky 于 2022-10-25 04:09 编辑-------------------------起因-------------------------
我有幸在学习HA的时候UI编写自动化已经很成熟,所以开始了跟着UI生成的代码去学习用yaml写自动化和Jinja2写模版,而没有学到node-red(而且我也不会javascript)。多亏了国内外论坛的帮助,学习起来没有太困难。但一直觉得模版用的Jinja2语言太难用,yaml里面写个if-else都麻烦死还不方便查错。当时就想看看能不能用现在最流行的python语言去写自动化,毕竟HA就是个爱好,平时实在难得有时间去熟练这两个写法,jinja2查错也麻烦。后来确实发现了有个python的插件叫AppDaemon,大喜,结果就发现自己的使用体验变成了 -- <AppDaemon, 从入门到放弃> :'(。 妈耶,真的有点复杂,中间还有多一层抽象,对python要求也有点高,还什么async callback,我这初级水平的python,实在玩不转。没办法,后面只好继续学HA yaml和jinja2。。
在我之前的帖子 用Sonos音箱让音乐走哪跟哪 - 音乐跟随 中 ,有一段不得不用到大量的jinja2,因为对语言不熟悉,调试几行jinja2代码花了好几个小时。下面就演示了jinja其中一个恶心的地方,一个在for循环外面的变量居然不能在for循环里被赋值,非得定义一个namespace才行。光知道这一点我就花了很多时间查错,读文档,实在太劝退。这要是放到python里面这几行代码最多半个小时左右就也弄完了。毕竟python的教程实在是太多太多了。不用看官网文档啥在网上一搜答案都有。
# Update the master speaker in the group
- service: input_select.select_option
entity_id: input_select.music_controller
data:
option: >
{% set ns = namespace() %}
{% set ns.primary_speaker = 'none' %}
{% set ns.secondary_speaker = 'none' %}
{# set the pri_speaker and sec_speaker #}
{% for speaker in state_attr(target_player, "sonos_group") %}
{% if loop.index == 1 %}
{% set ns.primary_speaker = speaker|regex_replace(find='media_player.', replace='', ignorecase=False) %}
{% elif loop.index == 2 %}
{% set ns.secondary_speaker = speaker|regex_replace(find='media_player.', replace='', ignorecase=False) %}
{% endif %}
{% endfor %}
{# use the second speaker as master speaker if target speaker is currently the master #}
{% if target_player == ('media_player.' + ns.primary_speaker) and ns.secondary_speaker != 'none' %}
{{ ns.secondary_speaker }}
{% else %}
{{ ns.primary_speaker }}
{% endif %}
那次体验后被这个模版语言弄的烦了,基本上也很少再想弄比较复杂的东西。。这次的python插件应该可以让我以后更有动力和可以有效率的去做复杂了自动化了。
---------------------好啦,正文开始了!----------------------
今天发现了一个简单易用的python插件 Pyscript,可以完全替代自动化里的HA模版!
pyscript对HA的实体保持了一样的结构,可以直接调用,访问,赋值给实体,比如直接调用sun.sun,light.kitchen_light
用python写script和自动化,直接无视HA yaml和模版(Jinja)不能简单实现if-else, for,变量赋值,正则表达式等各种臭毛病。当然我个人对于yaml没太多意见,只是yaml太多功能太局限,有时必须用jinja才行。本身python作为脚本语言自带的文本能力处理模版简直比HA模版(jinja2)好太多了。但我模版本身就用的少,一要做点复杂的东西就成了我的大敌,所以我会倾向把所有的jinja都换成pyscript。毕竟网上python的资源比jinja多太多了,python比起jinja更容易学习,而且我平时工作时偶尔也能用到python,一举两得。(不做网络前端的我,实在用不到jinja)
下面举个yaml局限性的例子。在HA里面yaml并不能使用以下调用变量的语法
- condition: state
entity_id: switch.gaming_pc
state: "on"
for:
hours: input_number.gaming_pc_shutdown_timer_in_hour
这样也不行
- condition: state
entity_id: switch.gaming_pc
state: "on"
for:
hours: "{{ states('input_number.gaming_pc_shutdown_timer_in_hour')|int }}"
只能用比较麻烦的jinja来写
- condition: template
value_template: >
{% set num_seconds_ago = now().timestamp() - states.switch.gaming_pc.last_changed.timestamp() %}
{% set num_hours_ago = (num_seconds_ago/3600)|int %}
{% set timer = states('input_number.gaming_pc_shutdown_timer_in_hour')|int %}
{{num_hours_ago >= timer and is_state('switch.gaming_pc', 'on')}}
然而同样的jinja的内容也可以用python实现
@service
def turn_off_gaming_pc_based_on_a_timer():
# Duration of the current gaming_pc state
from datetime import datetime as dt
from datetime import timezone as timezone
num_seconds_ago = (dt.now(tz=timezone.utc) - switch.gaming_pc.last_changed).total_seconds()
num_hours_ago= int(num_seconds_ago/3600)
# Cast of timer value
timer = int(float(input_number.gaming_pc_shutdown_timer_in_hour))
# If the gaming PC is on for more than timer value, turn off the pc
if num_hours_ago >= timer and switch.gaming_pc == 'on':
#some actions
最近我的一个帖子也用pyscript写的 https://bbs.hassbian.com/thread-17553-1-1.html。晚上睡觉前写出来的,如果用yaml+jinja写的话估计我几晚上都搞不定。。
官方里还有很多复杂的例子,为避免这帖子整太长了,请自行跳转 https://github.com/custom-compon ... sistant-Automations
从官方例子里发现,不但pyscript可以用来代替jinja,还可以代替yaml写出自动化和script。生成的service可以直接被调用。详细见官方文档 https://hacs-pyscript.readthedocs.io/en/stable/tutorial.html
再来个复杂的例子,我最近写的用来判断房间的人的状态是 ”无人/刚进入/待了很久/已经睡着“的服务。基本上就是一个状态机在4种状态中的状态变化(c0到c10)。yaml对 if/else/case等语法支持太复杂,代码太多,故用pyscript代替:
import datetime
# Get the number of seconds this entity has been current state
def get_sec_of_cur_state(entity_name):
last_time_cur_entity_changed = state.get(entity_name + '.last_changed')
return (datetime.datetime.now(tz=datetime.timezone.utc) - last_time_cur_entity_changed).total_seconds()
def now_is_before(hour, minute, second):
return datetime.datetime.now().time() < datetime.time(hour, minute, second)
def now_is_after(hour, minute, second):
return datetime.datetime.now().time() > datetime.time(hour, minute, second)
################################
# Room Occupancy
#
# outside --c0---> just_entered ----c2----> stayed ---c7-----> in_sleep
# |<----c3---------| | |
# |<-------------------c5-----------------|<------c10--------|
# |<----------------------------------------------c8---------|
# |c1| |c4| |c6| |c9|
#
# State machine changing conditions:
#
# c0. outside -> just_entered:
# c1. outside -> outside:
# c2. just_entered -> stayed:
# c3. just_entered -> outside:
# c4. just_entered -> just_entered
# c5. stayed -> outside:
# c6. stayed -> stayed:
# c7. stayed -> in_sleep:
# c8. in_sleep -> outside:
# c9. in_sleep -> in_sleep
#
################################
@service
def room_occupancy_state_machine(occupancy_entity_str,
motion_str,
motion_on_ratio_for_x_min_str,
motion_on_ratio_for_2x_min_str,
room_type):
#percentage_for_largely_def = 0.4
#percentage_for_fully_def = 0.8
# Get state based on string
cur_state = state.get(occupancy_entity_str)
motion = state.get(motion_str)
motion_state_lasts_for = get_sec_of_cur_state(motion_str)
motion_off_for = motion_state_lasts_for if motion == 'off' else 0
motion_on_ratio_for_x_min= float(state.get(motion_on_ratio_for_x_min_str))
motion_on_ratio_for_2x_min = float(state.get(motion_on_ratio_for_2x_min_str))
motion_off_ratio_for_x_min = 1 - motion_on_ratio_for_x_min
motion_off_ratio_for_2x_min= 1 - motion_on_ratio_for_2x_min
nxt_state = ''
stay_inside_for = get_sec_of_cur_state(occupancy_entity_str) if cur_state == 'Stayed Inside' else 0
now_is_sleep_time = now_is_before(9,30,0) or now_is_after(21,0,0)
# Outside -> xxx
if cur_state == 'Outside':
# c0. Outside -> Just Entered:
# currently on
if motion == 'on':
nxt_state = "Just Entered"
# c1. Outside -> Outside:
# currently off for 5 Min & previously off in
# OR all other condition
else:
nxt_state = 'Outside'
# Just Entered -> xxx
elif cur_state == 'Just Entered':
# c2. Just Entered -> Stayed Inside:
# currently on & previously largely on in or
if motion == 'on' and \
(motion_on_ratio_for_x_min>= 0.6 or
motion_on_ratio_for_2x_min >= 0.4):
nxt_state = "Stayed Inside"
# c3. Just Entered -> Outside:
# (currently off for 5min) & largely off in
elif motion == 'off' and \
motion_state_lasts_for >= 5*60 and \
motion_off_ratio_for_2x_min >= 0.5:
nxt_state = "Outside"
# c4. just_entered -> just_entered
# all other conditions
else:
nxt_state = "Just Entered"
# Stayed Inside -> xxx
elif cur_state == 'Stayed Inside':
# c7. Stayed Inside -> In Sleep:
# People is inside the room for an hour in the night time
# would assume they are in bed and ready for sleep
if room_type == 'bedroom' and \
stay_inside_for > 60*60 and \
now_is_sleep_time:
nxt_state = "In Sleep"
# c5. Stayed Inside -> Outside:
# (currently off for 5min) & largely off in
elif motion_off_for >=5*60 and \
motion_off_ratio_for_2x_min >= 0.7:
nxt_state = "Outside"
# c6. Stayed Inside -> Stayed Inside:
# all other condition
else:
nxt_state = "Stayed Inside"
# In Sleep -> xxx
elif cur_state == 'In Sleep':
# c8. In Sleep -> Stayed Inside:
# Assuming people will wake up once it is not sleep time anymore
if not now_is_sleep_time:
nxt_state = "Stayed Inside"
# c9. In Sleep -> Outside:
# No motions for an hour and half means people are outside during sleep time
elif motion_off_for >= 90*60:
nxt_state = "Outside"
# c10. In Sleep -> In Sleep:
else:
nxt_state = "In Sleep"
# Set next state
state.set(occupancy_entity_str, nxt_state)
------------------------ pyscript 安装教程 --------------------
**** Hidden Message *****
-------------------- 图形界面 JupyterLab 连接 pyscript 教程 --------------
JupyterLab是个很棒的IDE和GUI,通过网页可以直接把pyscript执行,功能类似于向HA里developer tools里的template editor的功能,可以尝试各种python代码去控制HA,也可以直接执行各种service和自动化。
安装
**** Hidden Message *****
安装好后
打开一个新的hass pyscript
就可以开始你的旅程啦
---------------------防杠线--------------------
HA模版(Jinja2)在很小的script里肯定还是很好用的,而且使用模版创造的实体和lovelace的一些卡片也还是只能用jinja2(坐等作者升级pyscript;P)但是对平时没有使用过jinja2的人,复杂的HA模版需要很高的学习成本在jinja2这个语言上。。
取这个题目只是为了跟随论坛近期的新潮流 哈哈哈哈哈哈:P
dalao学习了 好东西,python更灵活 感谢分享,这个可以试试 的确, jinja2 调试非常麻烦, 而且HA自动化限制太多:没有全局变量, 触发器不能使用变量和状态机...,
AppDaemon太重了,占CPU太多, 盒子上跑太费油.
Blueprint是一个好东西, 一直想用它来复用, 没想到就简单的一个开关灯蓝图就写得我头大,Pyscript 装了一直没用, 现在支持蓝图么? 本帖最后由 relliky 于 2022-4-25 09:04 编辑
riceball 发表于 2022-4-25 08:32
的确, jinja2 调试非常麻烦, 而且HA自动化限制太多:没有全局变量, 触发器不能使用变量和状态机...,
AppDa ...
Pyscript 自己写函数(def)就可以当蓝图用啊。因为函数可以调用函数啊。如果你要在蓝图里面调用pyscript也可以啊,可以传递参数过去的 (只不过就不好分享了,毕竟不是每个人都装了pyscript)我自己也用jinji2和yaml写过蓝图,写的真的头大,而且我用了很多jinji2功能把蓝图弄的通用一点,结果完全是给自己找麻烦,根本查错不了。直接放弃复杂的蓝图了。
今天写了一个pyscript,真好用。
我想写一个自动化,其中一部分是电脑开机一段被变量控制的时间后,自动关机
用HA写就是这样
- condition: state
entity_id: switch.gaming_pc
state: "on"
for:
hours: "{{ states('input_number.gaming_pc_shutdown_timer_in_hour')|int }}"
- service: switch.turn_off
entity_id: switch.gaming_pc
然而上面的代码并不能执行,因为condition不支持在for上用模版。只能用jinji2写类似于下面pyscirpt的版本。
所以刚刚写了个pyscript版本,抄了一下检测for的代码,一下子就弄好了,真是方便
@service
def turn_off_gaming_pc_based_on_a_timer():
# Duration of the current gaming_pc state
from datetime import datetime as dt
from datetime import timezone as timezone
num_seconds_ago = (dt.now(tz=timezone.utc) - switch.gaming_pc.last_changed).total_seconds()
num_hours_ago= int(num_seconds_ago/3600)
# Cast of timer value
timer = int(float(input_number.gaming_pc_shutdown_timer_in_hour))
# If the gaming PC is on for more than timer value, turn off the pc
if num_hours_ago >= timer and switch.gaming_pc == 'on':
switch.gaming_pc.turn_off()
relliky 发表于 2022-4-25 08:36
Pyscript 自己写函数(def)就可以当蓝图用啊。因为函数可以调用函数啊。我自己也用jinji2和yaml写过蓝图 ...
在我眼里的蓝图是: 可以简化自动化的可视化参数调整,能够在界面上. 用代码函数只能叫复用,称不上蓝图.
不过 pyscirpt 修改代码后,是否需要重启 HA Core呢? 本帖最后由 relliky 于 2022-4-25 08:56 编辑
riceball 发表于 2022-4-25 08:51
在我眼里的蓝图是: 可以简化自动化的可视化参数调整,能够在界面上. 用代码函数只能叫复用,称不上蓝图.
...
嗯,确实对于分享和易用来说是这样。。。但实际上我去每次去下官网大家分享的蓝图,拿回来后我必须得改一改最后才好用。。。所以我很少直接不看代码用蓝图。。毕竟很多分享出来的蓝图你不去看代码,完全不知道为啥它不按照你理解的方式工作。
不需要重启,甚至不需要手动reload,只要保存py文件就会自动reload。 不需要重启,甚至不需要手动reload,只要保存py文件就会自动reload。
这个好!
说实话,我都有想重写 HA自动化的冲动了(现在缺时间), 只要 HA 专心搞好事件状态机, 将自动化从核心剥离作为插件接入.
自动化不是它那样子搞的,在我看来这应该是基于数据知识库的推理器,是智能中枢的大脑,按照知识分类组织规则和事实进行驱动.
nodered不好用吗