yangqian / hass-cozylife

third party cozylife integration
MIT License
53 stars 12 forks source link

2-gang switch fixed #31

Open BonefaceJohnson opened 2 weeks ago

BonefaceJohnson commented 2 weeks ago

took me a whole day and some help of AI. But I finally made it to control a two gang switch with two seperate entities!

Split your configuration.yaml in two parts: "switches" for sockets etc. "switches2" for 2-gang wall switches

switch:
- platform: cozylife
  switches:
  - ip: 192.168.178.10
    did: 51039349083a8d53f900 #Sekretär
    pid: esEM1c
    dmn: Metering Socket
    dpid: [1, 2, 3, 18, 19, 20, 21, 26, 27, 28, 29, 30, 31, 32]
  switches2:
  - ip: 192.168.178.12
    did: 4324820980646f4c6700 #Wohnzimmer
    pid: e5aHVS
    dmn: Smart switch
    dpid: [1, 2, 3, 4, 5]

replace your custom_components/cozylife/switch.py with this:

"""Platform for sensor integration."""
from __future__ import annotations
import logging
from .tcp_client import tcp_client
from datetime import timedelta
import asyncio

from homeassistant.components.sensor import SensorEntity
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.event import async_track_time_interval

from typing import Any, Final, Literal, TypedDict, final
from .const import (
    DOMAIN,
    SWITCH_TYPE_CODE,
    LIGHT_TYPE_CODE,
    LIGHT_DPID,
    SWITCH,
    WORK_MODE,
    TEMP,
    BRIGHT,
    HUE,
    SAT,
)

SCAN_INTERVAL = timedelta(seconds=20)

_LOGGER = logging.getLogger(__name__)
_LOGGER.info(__name__)

SCAN_INTERVAL = timedelta(seconds=1)

async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_devices: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None
) -> None:
    """Set up the sensor platform."""
    # We only want this platform to be set up via discovery.
    # logging.info('setup_platform', hass, config, add_entities, discovery_info)
    _LOGGER.info('setup_platform')
    #_LOGGER.info(f'ip={hass.data[DOMAIN]}')

    #if discovery_info is None:
    #    return

    switches = []
    for item in config.get('switches') or []:
        client = tcp_client(item.get('ip'))
        client._device_id = item.get('did')
        client._pid = item.get('pid')
        client._dpid = item.get('dpid')
        client._device_model_name = item.get('dmn')
        switches.append(CozyLifeSwitch(client, hass, 'wippe1'))

    for item in config.get('switches2') or []:
        client = tcp_client(item.get('ip'))
        client._device_id = item.get('did')
        client._pid = item.get('pid')
        client._dpid = item.get('dpid')
        client._device_model_name = item.get('dmn')

        # Create two entities for each switch, one for each rocker
        switches.append(CozyLifeSwitch(client, hass, 'wippe1'))
        switches.append(CozyLifeSwitch(client, hass, 'wippe2'))

    async_add_devices(switches)
    for switch in switches:
        await hass.async_add_executor_job(switch._tcp_client._initSocket)
        await asyncio.sleep(0.01)

    async def async_update(now=None):
        for switch in switches:
            await hass.async_add_executor_job(switch._refresh_state)
            await asyncio.sleep(0.01)
    async_track_time_interval(hass, async_update, SCAN_INTERVAL)

class CozyLifeSwitch(SwitchEntity):
    _tcp_client = None
    _attr_is_on = True
    _wippe = None  # Add a new attribute to track the rocker

    def __init__(self, tcp_client: tcp_client, hass, wippe: str) -> None:
        """Initialize the sensor."""
        _LOGGER.info('__init__')
        self.hass = hass
        self._tcp_client = tcp_client
        self._unique_id = tcp_client.device_id + '_' + wippe
        self._name = tcp_client.device_id[-4:] + ' ' + wippe
        self._wippe = wippe  # Set the rocker attribute
        self._refresh_state()

    @property
    def unique_id(self) -> str | None:
        """Return a unique ID."""
        return self._unique_id

    async def async_update(self):
        await self.hass.async_add_executor_job(self._refresh_state)

    def _refresh_state(self):
        self._state = self._tcp_client.query()
        _LOGGER.info(f'_name={self._name},_state={self._state}')
        if self._state:
            if self._wippe == 'wippe1':
                self._attr_is_on = (self._state['1'] & 0x01) == 0x01
            elif self._wippe == 'wippe2':
                self._attr_is_on = (self._state['1'] & 0x02) == 0x02

    @property
    def name(self) -> str:
        return 'cozylife:' + self._name

    @property
    def available(self) -> bool:
        """Return if the device is available."""
        if self._tcp_client._connect:
            return True
        else:
            return False

    @property
    def is_on(self) -> bool:
        """Return True if entity is on."""
        return self._attr_is_on

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the entity on."""
        self._attr_is_on = True

        _LOGGER.info(f'turn_on:{kwargs}')

        if self._wippe == 'wippe1':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] | 0x01
            })
        elif self._wippe == 'wippe2':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] | 0x02
            })

        return None

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the entity off."""
        self._attr_is_on = False

        _LOGGER.info('turn_off')

        if self._wippe == 'wippe1':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] & ~0x01
            })
        elif self._wippe == 'wippe2':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] & ~0x02
            })

        return None

adjust SCAN_INTERVAL = timedelta(seconds=1) (in my case 1 second) to improve status response speed of the switch. Going below 1 sec. could maybe decrease your homeassistant's performance. "wippe" stands for rocker

I found information about how to control certain rockers here:

https://github.com/cozylife/dpid_document/blob/main/electrician_zh.md

neyestrabelli commented 2 weeks ago

Thanks!

I modify your code to use my 3 gang switch too.

configuration.yaml


switch:
- platform: cozylife
  switches3:
  - ip: 192.168.3.20
    did: 29470490a0764e71f5b4
    pid: c3xzf5
    dmn: Bathroom 1
    dpid: [1, 2, 3, 4, 5, 6, 7]
  - ip: 192.168.3.75
    did: 97138815a0764e51621c
    pid: c3xzf5
    dmn:  Bathroom 2
    dpid: [1, 2, 3, 4, 5, 6, 7]
  switches2:
  - ip: 192.168.3.9
    did: 1275753884f7033e8320
    pid: r8i69w
    dmn: TV Room
    dpid: [1, 2, 3, 4, 5]

Here is the code of custom_components/cozylife/switch.py:

"""Platform for sensor integration."""
from __future__ import annotations
import logging
from .tcp_client import tcp_client
from datetime import timedelta
import asyncio

from homeassistant.components.sensor import SensorEntity
from homeassistant.components.switch import SwitchEntity
from homeassistant.const import TEMP_CELSIUS
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.event import async_track_time_interval

from typing import Any, Final, Literal, TypedDict, final
from .const import (
    DOMAIN,
    SWITCH_TYPE_CODE,
    LIGHT_TYPE_CODE,
    LIGHT_DPID,
    SWITCH,
    WORK_MODE,
    TEMP,
    BRIGHT,
    HUE,
    SAT,
)

SCAN_INTERVAL = timedelta(seconds=20)

_LOGGER = logging.getLogger(__name__)
_LOGGER.info(__name__)

SCAN_INTERVAL = timedelta(seconds=1)

async def async_setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    async_add_devices: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None
) -> None:
    """Set up the sensor platform."""
    # We only want this platform to be set up via discovery.
    # logging.info('setup_platform', hass, config, add_entities, discovery_info)
    _LOGGER.info('setup_platform')
    #_LOGGER.info(f'ip={hass.data[DOMAIN]}')

    #if discovery_info is None:
    #    return

    switches = []
    for item in config.get('switches') or []:
        client = tcp_client(item.get('ip'))
        client._device_id = item.get('did')
        client._pid = item.get('pid')
        client._dpid = item.get('dpid')
        client._device_model_name = item.get('dmn')
        switches.append(CozyLifeSwitch(client, hass, 'switch1'))

    for item in config.get('switches2') or []:
        client = tcp_client(item.get('ip'))
        client._device_id = item.get('did')
        client._pid = item.get('pid')
        client._dpid = item.get('dpid')
        client._device_model_name = item.get('dmn')

        # Create two entities for each switch, one for each rocker
        switches.append(CozyLifeSwitch(client, hass, 'switch1'))
        switches.append(CozyLifeSwitch(client, hass, 'switch2'))

    for item in config.get('switches3') or []:
        client = tcp_client(item.get('ip'))
        client._device_id = item.get('did')
        client._pid = item.get('pid')
        client._dpid = item.get('dpid')
        client._device_model_name = item.get('dmn')

        # Create two entities for each switch, one for each rocker
        switches.append(CozyLifeSwitch(client, hass, 'switch1'))
        switches.append(CozyLifeSwitch(client, hass, 'switch2'))
        switches.append(CozyLifeSwitch(client, hass, 'switch3'))

    async_add_devices(switches)
    for switch in switches:
        await hass.async_add_executor_job(switch._tcp_client._initSocket)
        await asyncio.sleep(0.01)

    async def async_update(now=None):
        for switch in switches:
            await hass.async_add_executor_job(switch._refresh_state)
            await asyncio.sleep(0.01)
    async_track_time_interval(hass, async_update, SCAN_INTERVAL)

class CozyLifeSwitch(SwitchEntity):
    _tcp_client = None
    _attr_is_on = True
    _wippe = None  # Add a new attribute to track the rocker

    def __init__(self, tcp_client: tcp_client, hass, wippe: str) -> None:
        """Initialize the sensor."""
        _LOGGER.info('__init__')
        self.hass = hass
        self._tcp_client = tcp_client
        self._unique_id = tcp_client.device_id + '_' + wippe
        self._name = tcp_client.device_id[-4:] + ' ' + wippe
        self._wippe = wippe  # Set the rocker attribute
        self._refresh_state()

    @property
    def unique_id(self) -> str | None:
        """Return a unique ID."""
        return self._unique_id

    async def async_update(self):
        await self.hass.async_add_executor_job(self._refresh_state)

    def _refresh_state(self):
        self._state = self._tcp_client.query()
        _LOGGER.info(f'_name={self._name},_state={self._state}')
        if self._state:
            if self._wippe == 'switch1':
                self._attr_is_on = (self._state['1'] & 0x01) == 0x01
            elif self._wippe == 'switch2':
                self._attr_is_on = (self._state['1'] & 0x02) == 0x02
            elif self._wippe == 'switch3':
                self._attr_is_on = (self._state['1'] & 0x04) == 0x04

    @property
    def name(self) -> str:
        return 'cozylife:' + self._name

    @property
    def available(self) -> bool:
        """Return if the device is available."""
        if self._tcp_client._connect:
            return True
        else:
            return False

    @property
    def is_on(self) -> bool:
        """Return True if entity is on."""
        return self._attr_is_on

    async def async_turn_on(self, **kwargs: Any) -> None:
        """Turn the entity on."""
        self._attr_is_on = True

        _LOGGER.info(f'turn_on:{kwargs}')

        if self._wippe == 'switch1':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] | 0x01
            })
        elif self._wippe == 'switch2':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] | 0x02
            })
        elif self._wippe == 'switch3':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] | 0x04
            })

        return None

    async def async_turn_off(self, **kwargs: Any) -> None:
        """Turn the entity off."""
        self._attr_is_on = False

        _LOGGER.info('turn_off')

        if self._wippe == 'switch1':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] & ~0x01
            })
        elif self._wippe == 'switch2':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] & ~0x02
            })
        elif self._wippe == 'switch3':
            await self.hass.async_add_executor_job(self._tcp_client.control, {
                '1': self._state['1'] & ~0x04
            })

        return None
BonefaceJohnson commented 2 weeks ago

Great! Sometimes while switching, i receive this error: Failed to call service switch/turn_off. 'NoneType' object is not subscriptable

does someone have a idea on how to solve this.

neyestrabelli commented 2 weeks ago

Maybe the interval, I changed to 3 seconds and don't receive anymore.

soulripper13 commented 2 weeks ago

Does anybody have this in theit logs too?

Logger: homeassistant.components.switch Source: helpers/entity_platform.py:1033 integration: Switch (documentation, issues) First occurred: 7:48:52 PM (1233 occurrences) Last logged: 11:33:45 PM

Updating cozylife switch took longer than the scheduled update interval 0:00:03

BonefaceJohnson commented 2 weeks ago

yes, very often, about 4000 times in two hours. But i belief it's nothing dramatic.

neyestrabelli commented 1 week ago

Today I conducted more tests. This message about the interval and NoneType is due to a socket timeout.

I enabled the info log, and here is what I found:

2024-06-25 11:30:10.151 WARNING (MainThread) [homeassistant.components.switch] Updating cozylife switch took longer than the scheduled update interval 0:00:03
2024-06-25 11:30:11.457 INFO (SyncWorker_28) [custom_components.cozylife.tcp_client] _only_send.recv.error:timed out
2024-06-25 11:30:11.458 INFO (SyncWorker_28) [custom_components.cozylife.switch] _name=69f8 switch2,_state=None

I created an automation to turn off all switches, but sometimes one remains on due to the timeout when checking the state.

I can't find why the socket doesn't respond.

soulripper13 commented 1 week ago

I tried fixing the error messages but no avail. If I increase the scan interval from 3 to 5 or 10 I get the following too: Failed to call service switch/turn_off. 'NoneType' object is not subscriptable but with 3 seconds I just get

WARNING (MainThread) [homeassistant.components.switch] Updating cozylife switch took longer than the scheduled update interval 0:00:03 I left it at 3 as long as it's working