diff --git a/.gitignore b/.gitignore index 4d6b6a0..1cd4b82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store *.pyc +/.vs +/custom_components/climate_ip/ac14k_m.pem diff --git a/custom_components/climate_ip/ac14k_m.pem b/custom_components/climate_ip/ac14k_m_new.pem similarity index 91% rename from custom_components/climate_ip/ac14k_m.pem rename to custom_components/climate_ip/ac14k_m_new.pem index 04f6f8f..344a5cb 100644 --- a/custom_components/climate_ip/ac14k_m.pem +++ b/custom_components/climate_ip/ac14k_m_new.pem @@ -1,6 +1,6 @@ Bag Attributes friendlyName: ac14k_m - localKeyID: 54 69 6D 65 20 31 34 35 35 34 39 32 33 35 32 36 39 36 + localKeyID: 54 69 6D 65 20 31 36 37 39 37 34 37 31 35 34 30 39 37 Key Attributes: -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDeXvhcsqRFWfQt @@ -32,9 +32,9 @@ NTANFxBk7alY0G7ZUhuzWkg6 -----END PRIVATE KEY----- Bag Attributes friendlyName: ac14k_m - localKeyID: 54 69 6D 65 20 31 34 35 35 34 39 32 33 35 32 36 39 36 -subject=/C=KR/O=Samsung Electronics/CN=AC14K_M/emailAddress=AC14K_M@samsung.com -issuer=/C=KR/O=Samsung Electronics/CN=RemoteAccessCA(CE) + localKeyID: 54 69 6D 65 20 31 36 37 39 37 34 37 31 35 34 30 39 37 +subject=C = KR, O = Samsung Electronics, CN = AC14K_M, emailAddress = AC14K_M@samsung.com +issuer=C = KR, O = Samsung Electronics, CN = RemoteAccessCA(CE) -----BEGIN CERTIFICATE----- MIIDmzCCAoOgAwIBAgIBCTANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQGEwJLUjEc MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEbMBkGA1UEAwwSUmVtb3RlQWNj @@ -59,8 +59,8 @@ OCgygA+hmnBVnRDA8Jzu -----END CERTIFICATE----- Bag Attributes friendlyName: CN=RemoteAccessCA(CE),O=Samsung Electronics,C=KR -subject=/C=KR/O=Samsung Electronics/CN=RemoteAccessCA(CE) -issuer=/C=KR/O=Samsung Electronics/CN=CECA +subject=C = KR, O = Samsung Electronics, CN = RemoteAccessCA(CE) +issuer=C = KR, O = Samsung Electronics, CN = CECA -----BEGIN CERTIFICATE----- MIIDUTCCAjmgAwIBAgIBADANBgkqhkiG9w0BAQUFADA6MQswCQYDVQQGEwJLUjEc MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczENMAsGA1UEAwwEQ0VDQTAiGA8x @@ -83,8 +83,8 @@ NPCCEpp9rsIieZ58jsnc506Uc+1Vp+NmBI2A/ecypZxSb6v9gg== -----END CERTIFICATE----- Bag Attributes friendlyName: CN=CECA,O=Samsung Electronics,C=KR -subject=/C=KR/O=Samsung Electronics/CN=CECA -issuer=/C=KR/O=Samsung Electronics/CN=ROOTCA +subject=C = KR, O = Samsung Electronics, CN = CECA +issuer=C = KR, O = Samsung Electronics, CN = ROOTCA -----BEGIN CERTIFICATE----- MIIDRTCCAi2gAwIBAgIBBDANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJLUjEc MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEPMA0GA1UEAwwGUk9PVENBMCIY @@ -107,8 +107,8 @@ nXrSOPSkcIgvS6WYw2Rii+e6lfVzqmhAmg== -----END CERTIFICATE----- Bag Attributes friendlyName: CN=ROOTCA,O=Samsung Electronics,C=KR -subject=/C=KR/O=Samsung Electronics/CN=ROOTCA -issuer=/C=KR/O=Samsung Electronics/CN=ROOTCA +subject=C = KR, O = Samsung Electronics, CN = ROOTCA +issuer=C = KR, O = Samsung Electronics, CN = ROOTCA -----BEGIN CERTIFICATE----- MIIDRzCCAi+gAwIBAgIBADANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJLUjEc MBoGA1UECgwTU2Ftc3VuZyBFbGVjdHJvbmljczEPMA0GA1UEAwwGUk9PVENBMCIY @@ -128,4 +128,4 @@ zM2eoDVfS49+i/cwoi/A7fcZmIYggZho2UJR/GvKc79g6EAhT7/i5alBZF0enMsA 9okzakb/aohQE9SzsEHnhVKpGAjvu0/TJK9WwX6mkiIEJY+mzQMWgEeQt6WWIgAb gSX9NueH80tpZ9KqFnqnOoLxTAa7k0RPBRwyUO9CDhSnlWIEcsD9sqR2M+niOFnT KBHcLDDiEU3llprD8FRV3unYrl0F0B2GGdRk ------END CERTIFICATE----- \ No newline at end of file +-----END CERTIFICATE----- diff --git a/custom_components/climate_ip/climate.py b/custom_components/climate_ip/climate.py index 4c10ce7..e343ffa 100755 --- a/custom_components/climate_ip/climate.py +++ b/custom_components/climate_ip/climate.py @@ -29,8 +29,8 @@ ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, - HVAC_MODE_OFF, ClimateEntity, + HVAC_MODE_OFF, ) from homeassistant.components.climate.const import ( ATTR_MAX_TEMP, @@ -48,16 +48,18 @@ CONF_TOKEN, STATE_OFF, STATE_ON, - STATE_UNAVAILABLE, - STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, + STATE_UNKNOWN, + STATE_UNAVAILABLE, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.service import extract_entity_ids +#from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.unit_conversion import TemperatureConverter + from .controller import ATTR_POWER, ClimateController, create_controller from .yaml_const import ( CONF_CERT, @@ -111,12 +113,13 @@ vol.Optional( CONFIG_DEVICE_UPDATE_DELAY, default=DEFAULT_UPDATE_DELAY ): cv.string, - vol.Optional(CONF_DEVICE_ID, default="032000000"): cv.string, + vol.Optional(CONF_DEVICE_ID, default='032000000'): cv.string, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + _LOGGER.setLevel(logging.INFO if config.get("debug", False) else logging.ERROR) _LOGGER.info("climate_ip: async setup platform") @@ -127,7 +130,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= except Exception as e: _LOGGER.error("climate_ip: error while creating controller!") import traceback - _LOGGER.error(traceback.format_exc()) _LOGGER.error(e) raise @@ -189,6 +191,8 @@ class ClimateIP(ClimateEntity): def __init__(self, rac_controller, config): self.rac = rac_controller self._name = config.get(CONFIG_DEVICE_NAME, None) + _LOGGER.info("self._name: {}".format(self._name)) + self._poll = None self._unique_id = None str_poll = config.get(CONFIG_DEVICE_POLL, "") @@ -203,6 +207,8 @@ def __init__(self, rac_controller, config): if f in self.rac.operations: features |= SUPPORTED_FEATURES_MAP[f] for f in SUPPORTED_FEATURES_MAP.keys(): + _LOGGER.info("Feature: {}".format(f)) + if f in self.rac.attributes: features |= SUPPORTED_FEATURES_MAP[f] self._supported_features = features @@ -247,7 +253,7 @@ def unique_id(self): if self._unique_id is None and self.rac.unique_id is not None: _LOGGER.info("About to set unique id {}".format(self.rac.unique_id)) self._unique_id = "climate_ip_" + self.rac.unique_id - + _LOGGER.info("Returning unique id of {}".format(self._unique_id)) return self._unique_id @@ -262,8 +268,8 @@ def name(self): @property def state_attributes(self): - _LOGGER.info("state_attributes") attrs = self.rac.state_attributes + _LOGGER.info("state_attributes: {0}".format(attrs)) attrs.update(super(ClimateIP, self).state_attributes) if self._name is not None: attrs[ATTR_NAME] = self._name @@ -300,12 +306,7 @@ def target_temperature_low(self): @property def hvac_mode(self): - return ( - HVAC_MODE_OFF - if self.rac.get_property(ATTR_HVAC_MODE) - in [STATE_UNKNOWN, STATE_UNAVAILABLE, ""] - else self.rac.get_property(ATTR_HVAC_MODE) - ) + return HVAC_MODE_OFF if self.rac.get_property(ATTR_HVAC_MODE) in [STATE_UNKNOWN, STATE_UNAVAILABLE, ''] else self.rac.get_property(ATTR_HVAC_MODE) @property def hvac_modes(self): diff --git a/custom_components/climate_ip/connection_request.py b/custom_components/climate_ip/connection_request.py index a23e4cc..69ee0c8 100755 --- a/custom_components/climate_ip/connection_request.py +++ b/custom_components/climate_ip/connection_request.py @@ -2,12 +2,12 @@ import json import logging import os -import ssl import time import traceback +import ssl -from homeassistant.const import CONF_IP_ADDRESS, CONF_MAC, CONF_PORT, CONF_TOKEN from requests.adapters import HTTPAdapter +from homeassistant.const import CONF_IP_ADDRESS, CONF_MAC, CONF_PORT, CONF_TOKEN from .connection import Connection, register_connection from .yaml_const import ( @@ -20,7 +20,6 @@ CONNECTION_TYPE_REQUEST = "request" CONNECTION_TYPE_REQUEST_PRINT = "request_print" - class SamsungHTTPAdapter(HTTPAdapter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -31,7 +30,6 @@ def init_poolmanager(self, *args, **kwargs): kwargs["ssl_context"] = ssl_context return super().init_poolmanager(*args, **kwargs) - class ConnectionRequestBase(Connection): def __init__(self, hass_config, logger): super(ConnectionRequestBase, self).__init__(hass_config, logger) @@ -41,7 +39,7 @@ def __init__(self, hass_config, logger): self.update_configuration_from_hass(hass_config) self._condition_template = None self._thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) - + def __del__(self): self._thread_pool.shutdown(wait=False) @@ -119,7 +117,7 @@ def execute_internal(self, template, value, device_state) -> (json, bool, int): warnings.filterwarnings("ignore", category=InsecureRequestWarning) with requests.sessions.Session() as session: self.logger.info("Setting up HTTP Adapter and ssl context") - session.mount("https://", SamsungHTTPAdapter()) + session.mount('https://', SamsungHTTPAdapter()) self.logger.info(self._params) try: @@ -133,9 +131,7 @@ def execute_internal(self, template, value, device_state) -> (json, bool, int): try: resp = future.result() except: - self.logger.error( - "Request result exception: {}".format(future.exception()) - ) + self.logger.error("Request result exception: {}".format(future.exception())) return (None, False, 0) self.logger.info( diff --git a/custom_components/climate_ip/controller.py b/custom_components/climate_ip/controller.py index 49395b4..fc0f0ce 100755 --- a/custom_components/climate_ip/controller.py +++ b/custom_components/climate_ip/controller.py @@ -49,12 +49,12 @@ def service_schema_map(self): @property def operations(self): - """Return a list of available operations""" + """ Return a list of available operations """ return [] @property def attributes(self): - """Return a list of available attributes""" + """ Return a list of available attributes """ return [] diff --git a/custom_components/climate_ip/controller_yaml.py b/custom_components/climate_ip/controller_yaml.py index 6fc1cf0..f60c5e5 100755 --- a/custom_components/climate_ip/controller_yaml.py +++ b/custom_components/climate_ip/controller_yaml.py @@ -1,5 +1,8 @@ import logging import os +import time +from datetime import datetime +import json import homeassistant.helpers.config_validation as cv import homeassistant.helpers.entity_component @@ -86,7 +89,7 @@ def __init__(self, config, logger): self._logger.setLevel(logging.INFO if self._debug else logging.ERROR) self._yaml = config.get(CONF_CONFIG_FILE) self._ip_address = config.get(CONF_IP_ADDRESS, None) - self._device_id = config.get(CONF_DEVICE_ID, "032000000") + self._device_id = config.get(CONF_DEVICE_ID, '032000000') self._token = config.get(CONF_TOKEN, None) self._config = config self._retries_count = 0 @@ -98,7 +101,7 @@ def __init__(self, config, logger): @property def poll(self): return self._poll - + @property def unique_id(self): return self._unique_id @@ -125,9 +128,7 @@ def initialize(self): with open(file, "r") as stream: try: yaml_device = yaml.load( - StreamWrapper( - stream, self._token, self._ip_address, self._device_id - ), + StreamWrapper(stream, self._token, self._ip_address, self._device_id), Loader=yaml.FullLoader, ) except yaml.YAMLError as exc: @@ -179,10 +180,8 @@ def initialize(self): prop = create_property(key, nodes[key], connection) if prop is not None: self._properties[prop.id] = prop - - unique_id_prop = create_property( - CONFIG_DEVICE_UNIQUE_ID, ac.get(CONFIG_DEVICE_UNIQUE_ID, {}), connection - ) + + unique_id_prop = create_property(CONFIG_DEVICE_UNIQUE_ID, ac.get(CONFIG_DEVICE_UNIQUE_ID, {}), connection) if unique_id_prop is not None: self._uniqe_id_prop = unique_id_prop @@ -194,6 +193,8 @@ def initialize(self): ops = {} device_state = self._state_getter.value for op in self._operations.values(): + self._logger.info("Removing invalid operation '{}'".format(op.id)) + if op.is_valid(device_state): ops[op.id] = op else: @@ -203,6 +204,19 @@ def initialize(self): self._operations_list = [v for v in self._operations.keys()] self._properties_list = [v for v in self._properties.keys()] + self._properties_list.append("last_sync") + self._properties_list.append("AC_FUN_ENABLE") + self._properties_list.append("AC_FUN_COMODE") + self._properties_list.append("AC_FUN_ERROR") + self._properties_list.append("AC_SG_WIFI") + self._properties_list.append("AC_SG_INTERNET") + self._properties_list.append("AC_ADD2_USEDWATT") + self._properties_list.append("AC_ADD2_VERSION") + self._properties_list.append("AC_ADD2_PANEL_VERSION") + self._properties_list.append("AC_ADD2_OUT_VERSION") + self._properties_list.append("AC_ADD2_OPTIONCODE") + self._properties_list.append("AC_ADD2_USEDTIME") + self._properties_list.append("AC_ADD2_FILTER_USE_TIME") return (len(self._operations) + len(self._properties)) > 0 @@ -224,10 +238,11 @@ def update_state(self): self._logger.info("Updating state...") if self._state_getter is not None: self._attributes = {ATTR_NAME: self.name} - self._logger.info("Updating getter...") + self._logger.info("Updating getter...") self._state_getter.update_state(self._state_getter.value, debug) device_state = self._state_getter.value self._logger.info("Getter updated with value: {}".format(device_state)) + if device_state is None and self._retries_count > 0: --self._retries_count device_state = self._last_device_state @@ -237,8 +252,32 @@ def update_state(self): else: self._retries_count = CONST_MAX_GET_STATUS_RETRIES self._last_device_state = device_state - if debug: - self._attributes.update(self._state_getter.state_attributes) + + #[lucadjc]: preferred to have always the attributed evaluated, removed condition on debug + #if debug: + self._attributes.update(self._state_getter.state_attributes) + + #[lucadjc]: added last sync date to send some alerts from hassio in case of connection error + self._attributes['last_sync'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + toJSON = json.dumps(self._state_getter.state_attributes['device_state']) + try: + json_data = json.loads(json.loads(toJSON)) + self._attributes['AC_FUN_ENABLE'] = json_data['AC_FUN_ENABLE'] + self._attributes['AC_FUN_COMODE'] = json_data['AC_FUN_ENABLE'] + self._attributes['AC_FUN_ERROR'] = json_data['AC_FUN_ERROR'] + self._attributes['AC_SG_WIFI'] = json_data['AC_SG_WIFI'] + self._attributes['AC_SG_INTERNET'] = json_data['AC_SG_INTERNET'] + self._attributes['AC_ADD2_USEDWATT'] = json_data['AC_ADD2_USEDWATT'] + self._attributes['AC_ADD2_VERSION'] = json_data['AC_ADD2_VERSION'] + self._attributes['AC_ADD2_PANEL_VERSION'] = json_data['AC_ADD2_PANEL_VERSION'] + self._attributes['AC_ADD2_OUT_VERSION'] = json_data['AC_ADD2_OUT_VERSION'] + self._attributes['AC_ADD2_OPTIONCODE'] = json_data['AC_ADD2_OPTIONCODE'] + self._attributes['AC_ADD2_USEDTIME'] = json_data['AC_ADD2_USEDTIME'] + self._attributes['AC_ADD2_FILTER_USE_TIME'] = json_data['AC_ADD2_FILTER_USE_TIME'] + except: + self._logger.info("Error: update_state") + self._logger.info("Updating operations...") for op in self._operations.values(): op.update_state(device_state, debug) @@ -247,21 +286,23 @@ def update_state(self): for prop in self._properties.values(): prop.update_state(device_state, debug) self._attributes.update(prop.state_attributes) + for p in prop.state_attributes: + self._logger.info(p) if self._unique_id is None and self._uniqe_id_prop is not None: self._unique_id = self._uniqe_id_prop.update_state(device_state, debug) def set_property(self, property_name, new_value): - print("SETTING UP property {} to {}".format(property_name, new_value)) + self._logger.info("SETTING UP property {} to {}".format(property_name, new_value)) op = self._operations.get(property_name, None) if op is not None: result = op.set_value(new_value) - print( + self._logger.info( "SETTING UP property {} to {} -> FINISHED with result {}".format( property_name, new_value, result ) ) return result - print( + self._logger.info( "SETTING UP property {} to {} -> FAILED - wrong property".format( property_name, new_value ) @@ -292,10 +333,10 @@ def service_schema_map(self): @property def operations(self): - """Return a list of available operations""" + """ Return a list of available operations """ return self._operations_list @property def attributes(self): - """Return a list of available attributes""" + """ Return a list of available attributes """ return self._properties_list diff --git a/custom_components/climate_ip/manifest.json b/custom_components/climate_ip/manifest.json index b22a54a..81917d0 100644 --- a/custom_components/climate_ip/manifest.json +++ b/custom_components/climate_ip/manifest.json @@ -1,25 +1,18 @@ { "domain": "climate_ip", "name": "Climate IP", + "documentation": "https://github.com/atxbyea/samsungrac", + "issue_tracker": "https://github.com/atxbyea/samsungrac/issues", + "dependencies": [], + "config_flow": false, + "version": "3.5.2", "codeowners": [ - "@atxbyea", "@SebuZet" ], - "config_flow": false, - "dependencies": [], - "documentation": "https://github.com/atxbyea/samsungrac", - "integration_type": "device", - "iot_class": "cloud_polling", - "issue_tracker": "https://github.com/atxbyea/samsungrac/issues", "requirements": [ "requests>=2.21.0", "xmljson>=0.2.0" - ], - "version": "3.5.2" - - - - + ] } diff --git a/custom_components/climate_ip/properties.py b/custom_components/climate_ip/properties.py index 18ecd2e..b71ef21 100755 --- a/custom_components/climate_ip/properties.py +++ b/custom_components/climate_ip/properties.py @@ -8,7 +8,9 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.util.temperature import convert as convert_temperature + +#from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from .connection import Connection from .yaml_const import ( @@ -370,7 +372,6 @@ def state_attributes(self): data[self.name + "_modes"] = self.values return data - @register_property class UniqueIdProperty(DeviceProperty): def __init__(self, name, connection): @@ -500,7 +501,7 @@ def update_state(self, device_state, debug): def convert_dev_to_hass(self, dev_value): """Convert device state value to HASS.""" - return convert_temperature(float(dev_value), self._unit, TEMP_CELSIUS) + return TemperatureConverter.convert(float(dev_value), self._unit, TEMP_CELSIUS) def convert_hass_to_dev(self, hass_value): v = hass_value @@ -510,4 +511,4 @@ def convert_hass_to_dev(self, hass_value): if self._max is not None and hass_value > self._max: v = self._max - return convert_temperature(float(v), TEMP_CELSIUS, self._unit) + return TemperatureConverter.convert(float(v), TEMP_CELSIUS, self._unit) diff --git a/custom_components/climate_ip/samsung_2878.py b/custom_components/climate_ip/samsung_2878.py index 463202b..1446bfa 100644 --- a/custom_components/climate_ip/samsung_2878.py +++ b/custom_components/climate_ip/samsung_2878.py @@ -116,7 +116,7 @@ def load_from_yaml(self, node, connection_base): ) return False if self._cfg.cert is None: - self.logger.warning( + self.logger.info( "WARNING: 'cert' parameter is empty, skipping certificate validation" ) self.logger.info( @@ -179,9 +179,13 @@ def handle_response_auth_success(self, sslSocket, response): self.logger.info("Status request sent") def handle_response_status_update(self, sslSocket, response): + self.logger.info("handle_response_status_update") + attrs = response.split("><") for attr in attrs: f = re.match('Attr ID="(.*)" Value="(.*)"', attr) + self.logger.info("Attr: {0}".format(f)) + if f: k, v = f.group(1, 2) self._device_status[k] = v @@ -245,12 +249,13 @@ def create_connection(self): self.logger.info("Creating ssl context") sslContext = ssl.SSLContext(ssl.PROTOCOL_TLSv1) self.logger.info("Setting up ciphers") - sslContext.set_ciphers("ALL:@SECLEVEL=0") + sslContext.set_ciphers("HIGH:!DH:!aNULL") self.logger.info("Setting up verify mode") sslContext.verify_mode = ( ssl.CERT_REQUIRED if cfg.cert is not None else ssl.CERT_NONE ) if cfg.cert is not None: + sslContext.set_ciphers("ALL:@SECLEVEL=0") self.logger.info("Setting up verify location: {}".format(cfg.cert)) sslContext.load_verify_locations(cafile=cfg.cert) self.logger.info("Setting up load cert chain: {}".format(cfg.cert)) diff --git a/custom_components/climate_ip/samsung_2878.yaml b/custom_components/climate_ip/samsung_2878.yaml index db22733..d280ace 100755 --- a/custom_components/climate_ip/samsung_2878.yaml +++ b/custom_components/climate_ip/samsung_2878.yaml @@ -112,4 +112,4 @@ device: status_template: '32' used_power: type: number - status_template: '{% for key, value in device_state.items() %}{% if key == "AC_ADD2_USEDPOWER" %}{{value}}{% endif %}{% endfor %}' + status_template: '{% for key, value in device_state.items() %}{% if key == "AC_ADD2_USEDPOWER" %}{{value}}{% endif %}{% endfor %}' \ No newline at end of file diff --git a/custom_components/climate_ip/samsungrac.yaml b/custom_components/climate_ip/samsungrac.yaml index a1b83f9..08e3bd9 100755 --- a/custom_components/climate_ip/samsungrac.yaml +++ b/custom_components/climate_ip/samsungrac.yaml @@ -143,4 +143,4 @@ device: max_temp: type: number status_template: '30' - unit_template: '{{ device_state.Devices.0.Temperatures.0.unit }}' + unit_template: '{{ device_state.Devices.0.Temperatures.0.unit }}' \ No newline at end of file