Skip to content

Commit

Permalink
Work with HASS 2022.3.0 (#39)
Browse files Browse the repository at this point in the history
* Add version

For the compatibility with the future version of HASS

* Compatibility with Home Assistant 2022.3.0
Import Coco Submodule in this module

* Revert "Merge branch 'master' of https://github.com/filipvh/hass-nhc2 into filipvh-master"

This reverts commit dc11149, reversing
changes made to d59f22d.

* Update fan.py

Patch SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM error in HASS 2022.4.0

Co-authored-by: cebos <cedric.bosch@chc.be>
  • Loading branch information
boced66 and cebos committed Apr 9, 2022
1 parent 6efc1b4 commit 57ca5dc
Show file tree
Hide file tree
Showing 26 changed files with 1,210 additions and 23 deletions.
4 changes: 1 addition & 3 deletions custom_components/nhc2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
from .const import DOMAIN, KEY_GATEWAY, CONF_SWITCHES_AS_LIGHTS
from .helpers import extract_versions

REQUIREMENTS = ['nhc2-coco==1.4.1']

_LOGGER = logging.getLogger(__name__)

DOMAIN = DOMAIN
Expand Down Expand Up @@ -69,7 +67,7 @@ async def async_setup(hass, config):

async def async_setup_entry(hass, entry):
"""Create a NHC2 gateway."""
from nhc2_coco import CoCo
from .coco import CoCo
coco = CoCo(
address=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
Expand Down
6 changes: 3 additions & 3 deletions custom_components/nhc2/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
SUPPORT_TARGET_TEMPERATURE_RANGE
)

from nhc2_coco import CoCo
from nhc2_coco.coco_climate import CoCoThermostat
from nhc2_coco.coco_device_class import CoCoDeviceClass
from .coco import CoCo
from .coco_climate import CoCoThermostat
from .coco_device_class import CoCoDeviceClass

from .const import DOMAIN, KEY_GATEWAY, BRAND, CLIMATE
from .helpers import nhc2_entity_processor
Expand Down
222 changes: 222 additions & 0 deletions custom_components/nhc2/coco.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import json
import logging
import os
import threading
from time import sleep
from typing import Callable

import paho.mqtt.client as mqtt

from .coco_device_class import CoCoDeviceClass
from .coco_fan import CoCoFan
from .coco_light import CoCoLight
from .coco_shutter import CoCoShutter
from .coco_switch import CoCoSwitch
from .coco_switched_fan import CoCoSwitchedFan
from .coco_climate import CoCoThermostat
from .coco_generic import CoCoGeneric

from .const import *
from .helpers import *

_LOGGER = logging.getLogger(__name__)
sem = threading.Semaphore()
DEVICE_SETS = {
CoCoDeviceClass.SWITCHED_FANS: {INTERNAL_KEY_CLASS: CoCoSwitchedFan, INTERNAL_KEY_MODELS: LIST_VALID_SWITCHED_FANS},
CoCoDeviceClass.FANS: {INTERNAL_KEY_CLASS: CoCoFan, INTERNAL_KEY_MODELS: LIST_VALID_FANS},
CoCoDeviceClass.SHUTTERS: {INTERNAL_KEY_CLASS: CoCoShutter, INTERNAL_KEY_MODELS: LIST_VALID_SHUTTERS},
CoCoDeviceClass.SWITCHES: {INTERNAL_KEY_CLASS: CoCoSwitch, INTERNAL_KEY_MODELS: LIST_VALID_SWITCHES},
CoCoDeviceClass.LIGHTS: {INTERNAL_KEY_CLASS: CoCoLight, INTERNAL_KEY_MODELS: LIST_VALID_LIGHTS},
CoCoDeviceClass.THERMOSTATS: {INTERNAL_KEY_CLASS: CoCoThermostat, INTERNAL_KEY_MODELS: LIST_VALID_THERMOSTATS},
CoCoDeviceClass.GENERIC: {INTERNAL_KEY_CLASS: CoCoGeneric, INTERNAL_KEY_MODELS: LIST_VALID_GENERICS}
}


class CoCo:
def __init__(self, address, username, password, port=8883, ca_path=None, switches_as_lights=False):

if switches_as_lights:
DEVICE_SETS[CoCoDeviceClass.LIGHTS] = {INTERNAL_KEY_CLASS: CoCoLight,
INTERNAL_KEY_MODELS: LIST_VALID_LIGHTS + LIST_VALID_SWITCHES}
DEVICE_SETS[CoCoDeviceClass.SWITCHES] = {INTERNAL_KEY_CLASS: CoCoSwitch, INTERNAL_KEY_MODELS: []}
# The device control buffer fields
self._keep_thread_running = True
self._device_control_buffer = {}
self._device_control_buffer_size = DEVICE_CONTROL_BUFFER_SIZE
self._device_control_buffer_command_size = DEVICE_CONTROL_BUFFER_COMMAND_SIZE
self._device_control_buffer_command_count = 0
self._device_control_buffer_thread = threading.Thread(target=self._publish_device_control_commands)
self._device_control_buffer_thread.start()

if ca_path is None:
ca_path = os.path.dirname(os.path.realpath(__file__)) + MQTT_CERT_FILE
client = mqtt.Client(protocol=MQTT_PROTOCOL, transport=MQTT_TRANSPORT)
client.username_pw_set(username, password)
client.tls_set(ca_path)
client.tls_insecure_set(True)
self._client = client
self._address = address
self._port = port
self._profile_creation_id = username
self._all_devices = None
self._device_callbacks = {}
self._devices = {}
self._devices_callback = {}
self._system_info = None
self._system_info_callback = lambda x: None

def __del__(self):
self._keep_thread_running = False
self._client.disconnect()

def connect(self):

def _on_message(client, userdata, message):
topic = message.topic
response = json.loads(message.payload)

if topic == self._profile_creation_id + MQTT_TOPIC_PUBLIC_RSP and \
response[KEY_METHOD] == MQTT_METHOD_SYSINFO_PUBLISH:
self._system_info = response
self._system_info_callback(self._system_info)

elif topic == (self._profile_creation_id + MQTT_TOPIC_SUFFIX_RSP) and \
response[KEY_METHOD] == MQTT_METHOD_DEVICES_LIST:
self._client.unsubscribe(self._profile_creation_id + MQTT_TOPIC_SUFFIX_RSP)
self._process_devices_list(response)

elif topic == (self._profile_creation_id + MQTT_TOPIC_SUFFIX_SYS_EVT) and \
response[KEY_METHOD] == MQTT_METHOD_SYSINFO_PUBLISHED:
# If the connected controller publishes sysinfo... we expect something to have changed.
client.subscribe(self._profile_creation_id + MQTT_TOPIC_SUFFIX_RSP, qos=1)
client.publish(self._profile_creation_id + MQTT_TOPIC_SUFFIX_CMD,
json.dumps({KEY_METHOD: MQTT_METHOD_DEVICES_LIST}), 1)

elif topic == (self._profile_creation_id + MQTT_TOPIC_SUFFIX_EVT) \
and (response[KEY_METHOD] == MQTT_METHOD_DEVICES_STATUS or response[
KEY_METHOD] == MQTT_METHOD_DEVICES_CHANGED):
devices = extract_devices(response)
for device in devices:
try:
if KEY_UUID in device:
self._device_callbacks[device[KEY_UUID]][INTERNAL_KEY_CALLBACK](device)
except:
pass

def _on_connect(client, userdata, flags, rc):
if rc == 0:
_LOGGER.info('Connected!')
client.subscribe(self._profile_creation_id + MQTT_TOPIC_SUFFIX_RSP, qos=1)
client.subscribe(self._profile_creation_id + MQTT_TOPIC_PUBLIC_RSP, qos=1)
client.subscribe(self._profile_creation_id + MQTT_TOPIC_SUFFIX_EVT, qos=1)
client.subscribe(self._profile_creation_id + MQTT_TOPIC_SUFFIX_SYS_EVT, qos=1)
client.publish(self._profile_creation_id + MQTT_TOPIC_PUBLIC_CMD,
json.dumps({KEY_METHOD: MQTT_METHOD_SYSINFO_PUBLISH}), 1)
client.publish(self._profile_creation_id + MQTT_TOPIC_SUFFIX_CMD,
json.dumps({KEY_METHOD: MQTT_METHOD_DEVICES_LIST}), 1)
elif MQTT_RC_CODES[rc]:
raise Exception(MQTT_RC_CODES[rc])
else:
raise Exception('Unknown error')

def _on_disconnect(client, userdata, rc):
_LOGGER.warning('Disconnected')
for uuid, device_callback in self._device_callbacks.items():
offline = {'Online': 'False', KEY_UUID: uuid}
device_callback[INTERNAL_KEY_CALLBACK](offline)

self._client.on_message = _on_message
self._client.on_connect = _on_connect
self._client.on_disconnect = _on_disconnect

self._client.connect_async(self._address, self._port)
self._client.loop_start()

def disconnect(self):
self._client.loop_stop()
self._client.disconnect()

def get_systeminfo(self, callback):
self._system_info_callback = callback
if self._system_info:
self._system_info_callback(self._system_info)

def get_devices(self, device_class: CoCoDeviceClass, callback: Callable):
self._devices_callback[device_class] = callback
if self._devices and device_class in self._devices:
self._devices_callback[device_class](self._devices[device_class])

def _publish_device_control_commands(self):
while self._keep_thread_running:
device_commands_to_process = None
sem.acquire()
if len(self._device_control_buffer.keys()) > 0:
device_commands_to_process = self._device_control_buffer
self._device_control_buffer = {}
self._device_control_buffer_command_count = 0
sem.release()
if device_commands_to_process is not None:
command = process_device_commands(device_commands_to_process)
self._client.publish(self._profile_creation_id + MQTT_TOPIC_SUFFIX_CMD, json.dumps(command), 1)
sleep(0.05)

def _add_device_control(self, uuid, property_key, property_value):
while len(self._device_control_buffer.keys()) >= self._device_control_buffer_size or \
self._device_control_buffer_command_count >= self._device_control_buffer_command_size:
pass
sem.acquire()
self._device_control_buffer_command_count += 1
if uuid not in self._device_control_buffer:
self._device_control_buffer[uuid] = {}
self._device_control_buffer[uuid][property_key] = property_value
sem.release()

# Processes response on devices.list
def _process_devices_list(self, response):

# Only add devices that are actionable
actionable_devices = list(
filter(lambda d: d[KEY_TYPE] == DEV_TYPE_ACTION, extract_devices(response)))
actionable_devices.extend(list(
filter(lambda d: d[KEY_TYPE] == "thermostat", extract_devices(response))))

# Only prepare for devices that don't already exist
# TODO - Can't we do this when we need it (in initialize_devices ?)
existing_uuids = list(self._device_callbacks.keys())
for actionable_device in actionable_devices:
if actionable_device[KEY_UUID] not in existing_uuids:
self._device_callbacks[actionable_device[KEY_UUID]] = \
{INTERNAL_KEY_CALLBACK: None, KEY_ENTITY: None}

# Initialize
self.initialize_devices(CoCoDeviceClass.SWITCHED_FANS, actionable_devices)
self.initialize_devices(CoCoDeviceClass.FANS, actionable_devices)
self.initialize_devices(CoCoDeviceClass.SWITCHES, actionable_devices)
self.initialize_devices(CoCoDeviceClass.LIGHTS, actionable_devices)
self.initialize_devices(CoCoDeviceClass.SHUTTERS, actionable_devices)
self.initialize_devices(CoCoDeviceClass.THERMOSTATS, actionable_devices)
self.initialize_devices(CoCoDeviceClass.GENERIC, actionable_devices)

def initialize_devices(self, device_class, actionable_devices):

base_devices = [x for x in actionable_devices if x[KEY_MODEL]
in DEVICE_SETS[device_class][INTERNAL_KEY_MODELS]]
if device_class not in self._devices:
self._devices[device_class] = []
for base_device in base_devices:
if self._device_callbacks[base_device[KEY_UUID]] and self._device_callbacks[base_device[KEY_UUID]][
KEY_ENTITY] and \
self._device_callbacks[base_device[KEY_UUID]][KEY_ENTITY].uuid:
self._device_callbacks[base_device[KEY_UUID]][KEY_ENTITY].update_dev(base_device)
else:
self._device_callbacks[base_device[KEY_UUID]][KEY_ENTITY] = \
DEVICE_SETS[device_class][INTERNAL_KEY_CLASS](base_device,
self._device_callbacks[
base_device[
KEY_UUID]],
self._client,
self._profile_creation_id,
self._add_device_control)
self._devices[device_class].append(self._device_callbacks[base_device[KEY_UUID]][KEY_ENTITY])
if device_class in self._devices_callback:
self._devices_callback[device_class](self._devices[device_class])
68 changes: 68 additions & 0 deletions custom_components/nhc2/coco_ca.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF6jCCA9KgAwIBAgIJANTA8rXGnhG7MA0GCSqGSIb3DQEBCwUAMIGBMQswCQYD
VQQGEwJCRTEYMBYGA1UECAwPT29zdC1WbGFhbmRlcmVuMRUwEwYDVQQHDAxTaW50
LU5pa2xhYXMxDTALBgNVBAoMBE5pa28xFTATBgNVBAsMDEhvbWUgQ29udHJvbDEb
MBkGCSqGSIb3DQEJARYMaW5mb0BuaWtvLmJlMB4XDTcwMDEwMTAwMDAwNVoXDTM3
MDEyOTAwMDAwNVowgYExCzAJBgNVBAYTAkJFMRgwFgYDVQQIDA9Pb3N0LVZsYWFu
ZGVyZW4xFTATBgNVBAcMDFNpbnQtTmlrbGFhczENMAsGA1UECgwETmlrbzEVMBMG
A1UECwwMSG9tZSBDb250cm9sMRswGQYJKoZIhvcNAQkBFgxpbmZvQG5pa28uYmUw
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpKNKKHC0fCND19D96G78G
Zdj+OGLvy/DRJswbLepG8cPedqEZwXjn762fvLdTlcTX/ohkeG4QPb1mxPzjpEgl
M5aNmp2rmlAFVtLWILQx7mWir5FjG5eTyYi2fbYHnPQpx8XuVk2INENd85818R4j
RfouYLaZWSd8wc7LP20N0rVtjg5RJ/zAkQ6A7KzdgeOkKhn07wSGBWu9vDw7gCdL
+Oyeo4LQmABXB7up8nIDCl+o23QL4/aSzdrS5cBCXoPWwto7OiXw0RRcEbpumQyW
mTGS8jT2FCUNAIWAxC3pKEIXbzf03pLo7EMfFcmjsLDcvcnkB+EJX0fuATwl5CLz
SneUFY7MNTpv9xgZFX83LhoiFbycZwzWEUr/Q0pmHYZdmezm84+W6EA3E9qH+oR8
V09bwEMAMSQpbebEB8JmvvwykQHxowkpnV01bmimBEOaquAmyfiW3YSO90vJu+kg
Zrkihc0AEMFcDbLRCEKvx/u6Hs2xMmVPz0W9mPW37t5zKOV0vcrHmFgMp+9EyDAQ
vfNofLx790lD1LFp3qvD/H0+IbydQoEc7Q1/tTQDjL45TLNXwwBWQVQLIEQY5sqN
n8p2ita3MPpSnu5XU93pBcns8jUNlc6/wFIMSBDWK40RiJKzTsr/2jTGVqZX8PXA
rDnIoa0Eapt0nq87qnkQzQIDAQABo2MwYTAdBgNVHQ4EFgQU5rQ0Z/a3fq5iKhZV
qmabTvd4kFkwHwYDVR0jBBgwFoAU5rQ0Z/a3fq5iKhZVqmabTvd4kFkwDwYDVR0T
AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAFLw
6lbxex6hElSrqOZljoFQzQg78dmtUFm4BgKu5EAqn1Ug/OHOKk8LBbNMf2X0Y+4i
SO4g8yO/C9M/YPxjnM5As1ouTu7UzQL4hEJynyHoPuCBD8nZxuaKeGMX7nQYm7GD
0iaL0iP9gFwv/A2O/isQUB/sTAclhm1zKAw4f/SaBq8t22wf59e8na0xIfHui0PD
s8PfRbC4xIOKMxHkHFv+DHeMGjCbR4x20RV/z4JNx1ALEBGo6Oh7Dph/maAQWbje
x9BCstNR3V1Bhx9rUe7BjIMyJUGEItpZXG+N+qnQr2K7xDdloJl4X0flIa74sdUE
K4s0X7p+JixLMSxbu5oS6W+d3g6EG0ZgEUwwwc98D1fsm1ziNqwcnYMkI6P2601G
kEaK/54kYqCxvw6fu5+PNmsDD8ptdazoO3/UOxWvspI1U3drcpnaEHuNclEF7WeL
yqTfi+8UiL9xJgq9ivjKjZdchkdaD2THgrnzs0XxLbZnwAPeh3cHooUJQkInmKp3
O05Gv0rnSr29bH8vh/sy4/yJJCUd036pF9C8mPHAYsvNDVGaGYVmNt5P28z3PO16
YKNJCOJ0x333F6PJaqWAQQP9bGMuJThX8ZQ9Fd8KMXVUfFVKICEkb4erWpL2RIz3
9JFSC56ZtXv2losfASTyXJwCpyib7FcTZ1rJze+l
-----END CERTIFICATE-----
Loading

0 comments on commit 57ca5dc

Please sign in to comment.