Open BonefaceJohnson opened 5 months 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
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.
Maybe the interval, I changed to 3 seconds and don't receive anymore.
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
yes, very often, about 4000 times in two hours. But i belief it's nothing dramatic.
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.
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
I further improved the code, so the state of switches get updated very fast and they reconnect quickly, if the connection was lost.
USE THIS CODE:
from __future__ import annotations
import logging
from .tcp_client import tcp_client
from datetime import timedelta
import asyncio
from homeassistant.components.switch import SwitchEntity
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
from .const import (
DOMAIN,
SWITCH_TYPE_CODE,
)
SCAN_INTERVAL = timedelta(seconds=0.7)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_devices: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None
) -> None:
"""Set up the sensor platform."""
_LOGGER.info('setup_platform')
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')
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 switch.async_update()
await asyncio.sleep(0.01)
async_track_time_interval(hass, async_update, SCAN_INTERVAL)
class CozyLifeSwitch(SwitchEntity):
def __init__(self, tcp_client: tcp_client, hass: HomeAssistant, wippe: str) -> None:
"""Initialize the sensor."""
self.hass = hass
self._tcp_client = tcp_client
self._unique_id = f"{tcp_client.device_id}_{wippe}"
self._name = f"cozylife:{tcp_client.device_id[-4:]}_{wippe}"
self._wippe = wippe
self._attr_is_on = False
self._state = {}
self._available = True
self._reconnect_delay = 0.5 # Initial reconnect delay
# Event listener setup
self._event_listener_task: asyncio.Task | None = None
self.async_on_remove(self.stop_event_listener)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
self.start_event_listener()
def start_event_listener(self) -> None:
"""Start the event listener."""
if self._event_listener_task is None:
self._event_listener_task = self.hass.loop.create_task(self._listen_for_events())
async def stop_event_listener(self) -> None:
"""Stop the event listener."""
if self._event_listener_task:
self._event_listener_task.cancel()
try:
await self._event_listener_task
except asyncio.CancelledError:
pass
self._event_listener_task = None
async def _listen_for_events(self) -> None:
"""Listen for events from the CozyLife device."""
while True:
try:
event = await self.hass.async_add_executor_job(self._tcp_client.query)
if event:
self._state = event
self._update_state()
self.async_write_ha_state()
await asyncio.sleep(0.1)
except Exception as e:
_LOGGER.error(f"Error listening for events: {e}")
self._available = False
self.async_write_ha_state()
await self._tcp_client._initSocket() # Reinitialize socket
def _update_state(self) -> None:
"""Update the switch state based on the received event."""
if self._wippe == 'wippe1':
self._attr_is_on = (self._state.get('1', 0) & 0x01) == 0x01
elif self._wippe == 'wippe2':
self._attr_is_on = (self._state.get('1', 0) & 0x02) == 0x02
async def async_update(self) -> None:
"""Fetch new state data for the sensor."""
await self.hass.async_add_executor_job(self._refresh_state)
async def _refresh_state(self) -> None:
"""Refresh the switch state."""
try:
self._state = self._tcp_client.query()
_LOGGER.info(f'_name={self._name}, _state={self._state}')
if self._state:
self._update_state()
self._available = True
else:
self._available = False
await self._tcp_client._initSocket() # Reinitialize socket
except Exception as e:
_LOGGER.error(f"Error refreshing state: {e}")
self._available = False
await self._tcp_client._initSocket() # Reinitialize socket
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@property
def available(self) -> bool:
"""Return if the device is available."""
return self._available
@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."""
new_state = self._state.get('1', 0)
if self._wippe == 'wippe1':
new_state |= 0x01
elif self._wippe == 'wippe2':
new_state |= 0x02
await self.hass.async_add_executor_job(self._tcp_client.control, {'1': new_state})
await self.async_update()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
new_state = self._state.get('1', 0)
if self._wippe == 'wippe1':
new_state &= ~0x01
elif self._wippe == 'wippe2':
new_state &= ~0x02
await self.hass.async_add_executor_job(self._tcp_client.control, {'1': new_state})
await self.async_update()
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.
fixed this in the improved code
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
check out my updated code, should be fixed
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.
check out the updated code
Thank you, but I removed this plugin and tested it with the HomeKit integration, which works much better. I’m also sharing it with MatterBridge.
UPDATED, CHECK POST BELOW!!
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
_replace your custom_components/cozylife/switch.py with this: