【踩坑系列】Home Assistant中鸿雁IHC8340B插排开关复位

趁着618活动入了中草已久的鸿雁插排IHC8340B玩玩,4个独立分控,wifi插线板比wifi插座划算多了。看论坛以前的帖子,用官方的broadlink MP1插件即可。到手之后用broadlink的易控APP设置好网络,按官网的配置指引配置好后,发现控制是可以控制了,不过有点诡异,点开关图标经常回复先前的状态,控制倒是正常。比如开关是“关”状态,点开之后插线板接通了,但HA的开关状态立刻切回“关”状态,等一会后状态就切回为“开”了。于是又是一番研究代码折腾,算是找到初步解决方法了。

看了调试信息没发现什么异常,而且通过app学习、使用红外码就正常。孤寂了一段时间,意外在domoticz论坛一个[帖子][1]发现了[抓包分析方法][2],于是继续分析,算是找到了原因和临时解决办法。


1.准备

  • IHC8340B插线板,固件版本v10028
  • Ubuntu 18.04 + HA 0.70.1,
  • HA的configuration.yaml配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    switch:        
    - platform: broadlink
    scan_interval: 15 #自动更新的间隔,默认30s
    host: 设备ip地址
    mac: '设备mac地址'
    type: mp1
    slots:
    slot_1: 'slot1' #HA会实例化一个switch.slot1设备,这里不要用中文,否则HA实例化的设备名称是switch.x(x是数字),有一定随机性
    slot_2: 'slot2'
    slot_3: 'slot3'
    slot_4: 'slot4'

2.过程

2.1.工作流程分析

1)web页面点击开关按钮,触发turn_on或turn_off service(前端js)

2)进行通信、发送控制指令(插件turn_on方法)

3)设置开关状态并调用schedule_update_ha_state通告状态变更(插件turn_on方法)

4)service调用完毕后触发通信查询状态并更新开关状态(系统entity组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    #ha安装目录/homeassistant/components/switch/__init__.py
    #SwitchDevice类async def async_setup(hass, config)函数 部分代码
    async def async_handle_switch_service(service):
        """Handle calls to the switch services."""
        target_switches = component.async_extract_from_service(service)

        update_tasks = []
        for switch in target_switches:
            if service.service == SERVICE_TURN_ON:
                await switch.async_turn_on()    #步骤1
            elif service.service == SERVICE_TOGGLE:
                await switch.async_toggle()
            else:
                await switch.async_turn_off()

            if not switch.should_poll:
                continue
            update_tasks.append(switch.async_update_ha_state(True)) #步骤4,后续会调用broadlink.py中update方法

        if update_tasks:
            await asyncio.wait(update_tasks, loop=hass.loop)
1
2
3
4
5
6
7
8
9
10
11
12
13
    #ha安装目录/homeassistant/components/switch/broadlink.py
    #BroadlinkRMSwitch类 部分代码
        def turn_on(self, **kwargs):
        """Turn the device on."""
        if self._sendpacket(self._command_on):     #步骤2
            self._state = True                #步骤3
            self.schedule_update_ha_state() #步骤3

        def turn_off(self, **kwargs):             #流程同turn_on
        """Turn the device off."""
        if self._sendpacket(self._command_off): 
            self._state = False                    
            self.schedule_update_ha_state()

INFO:async_update_ha_state(True)参数带true,会调用update方法(会与设备通信)去更新state。
INFO:HA某个版本(0.80.0左右吧)后,entity的service处理方法统一改用EntityComponent.async_register_entity_service()注册了,相关的service处理代码改到了helpers.service.py的_handle_service_platform_call()方法。不过总体处理逻辑没变,调用完entity相对应的service操作代码后,会根据entity的should_poll属性调用一次entity.async_update_ha_state(True)更新状态。

2.2.解决方法

去掉BroadlinkMP1Switch类中的@Throttle(TIME_BETWEEN_UPDATES)即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class BroadlinkMP1Switch(object):
"""Representation of a Broadlink switch - To fetch states of all slots."""

def __init__(self, device):
"""Initialize the switch."""
self._device = device
self._states = None

def get_outlet_status(self, slot):
"""Get status of outlet from cached status list."""
return self._states['s{}'.format(slot)]
#注释掉不生效
#@Throttle(TIME_BETWEEN_UPDATES)
def update(self):
"""Fetch new state data for this device."""
self._update()

INFO:Throttle的存在,限制update()更新频率为5s,造成2种情况下开关复位:1、执行一次开关操作5s内执行另外一次开关操作。2、HA的周期更新(会调用update())后5s内,刚好执行开关操作。
INFO:去掉Throttle副作用就是一个周期内,每个slot更新都会调用1次update(),建议适当调大scan_interval周期。


3.小结

  • 之前错误分析问题原因:由于步骤3、步骤4执行时间基本一致,导致步骤4去向插线板获取状态时,插线板返回的状态有一定的滞后性,HA获取仍然是旧状态,最后导致开关状态复原,要等待一个scan_interval更新周期状态才会重新同步。还是要对插座单片机的处理性能有信心。
  • 一个插座有4个slot,组件会将每一个slot实例化一个switch。当switch更新状态会调用父设备(即BroadlinkMP1Switch)的update()方法,插件作者设置TIME_BETWEEN_UPDATES就是为了限制不必要的通信。
  • HA有周期性的更新platform里的entities功能,配置文件通过scan_interval设置更新时间。scan_interval默认是15s(entity_component.py中定义DEFAULT_SCAN_INTERVAL),switch类默认是30s(switch/__init__.py中定义)