From d3f6903101aed9b5075728e3ef459f66ace9a90b Mon Sep 17 00:00:00 2001 From: MrBearPresident Date: Sun, 29 Sep 2024 08:57:10 +0200 Subject: [PATCH] V1.1.2 Add Cert and Key for HTTPS requests --- custom_components/jbl_integration/Cert.pem | 25 +++ custom_components/jbl_integration/Key.pem | 32 ++++ .../jbl_integration/coordinator.py | 148 ++++++++++++------ 3 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 custom_components/jbl_integration/Cert.pem create mode 100644 custom_components/jbl_integration/Key.pem diff --git a/custom_components/jbl_integration/Cert.pem b/custom_components/jbl_integration/Cert.pem new file mode 100644 index 0000000..8732e4e --- /dev/null +++ b/custom_components/jbl_integration/Cert.pem @@ -0,0 +1,25 @@ +Bag Attributes + friendlyName: 1 + localKeyID: 54 69 6D 65 20 31 37 32 37 35 34 39 32 30 38 36 38 31 +subject=C=US, ST=Some-State, O=Linkplay, OU=Linkplay, emailAddress=harman@linkplay.com +issuer=C=US, ST=Some-State, O=Linkplay, OU=Linkplay, emailAddress=harman@linkplay.com +-----BEGIN CERTIFICATE----- +MIIDTDCCAjQCAQEwDQYJKoZIhvcNAQELBQAwbDELMAkGA1UEBhMCVVMxEzARBgNV +BAgMClNvbWUtU3RhdGUxETAPBgNVBAoMCExpbmtwbGF5MREwDwYDVQQLDAhMaW5r +cGxheTEiMCAGCSqGSIb3DQEJARYTaGFybWFuQGxpbmtwbGF5LmNvbTAeFw0yMDA1 +MTIwMjQ3MzNaFw00NzA5MjcwMjQ3MzNaMGwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI +DApTb21lLVN0YXRlMREwDwYDVQQKDAhMaW5rcGxheTERMA8GA1UECwwITGlua3Bs +YXkxIjAgBgkqhkiG9w0BCQEWE2hhcm1hbkBsaW5rcGxheS5jb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDwSU/lDQtH0IAkcbMyYGRMnAvW5Q71pTu7 +Cn6fNRJS35okajb2oLsWrkkNeFX1989cxdHl2iDiGabQ3napGVyzyatM0XQWClVi +/EGq5m90D7wlU0WhL3LtpPA2tdtnxDBK2ax+jzEavyzbcaTtDJqNMEf3qLyXafxX +RZ0eaTgb8cp6QzMOXIfeC3fCNVqJ7HCqYRwG6iP+uZznQ5IPMdw2sVTsOo8McXXR +EfxY2J7QWi9QG4ahpkcBAwCF8BFRQZbdCO+rzVY9O3094a8vjzcawndVfpBze2Jj +FVtmBCUbZqMEodhzCMr6zfqrpXFOpnM+zZJyELiRJiIP6KiEKSutAgMBAAEwDQYJ +KoZIhvcNAQELBQADggEBAEdCCU2thJxExluRPBAZF4rZKJyDsdisbRrDqnIEA3Np +QJMH+NPlP+A6Ft2xz4ByL60Hs7i0G/jIRzqM7dKhmbJcP//z32FrBLGmOuCEAYtf +GN8WYGsSoK/GjV4DfT/gdukq7dgPLHwDY5UuxJfmkJ4bvvplbXEPc8E5giltxmsi +1zQXol2sVb3Ul5U27m0rpu+Mqdrszst1HFqUltOaz/g5R7t9H5Pd5/k1ntR2HJjK +0+p/lGrrumDGs1gEpWfnSsvIPTDUf6uNoA6LgRBgSwoZvVHO5pHpFz2Rm2M5Nh96 +v/Do6XDLJJsLIAxx1CbpnNwk4SEEExLoRsuCgwSBG8U= +-----END CERTIFICATE----- diff --git a/custom_components/jbl_integration/Key.pem b/custom_components/jbl_integration/Key.pem new file mode 100644 index 0000000..16b0bad --- /dev/null +++ b/custom_components/jbl_integration/Key.pem @@ -0,0 +1,32 @@ +Bag Attributes + friendlyName: 1 + localKeyID: 54 69 6D 65 20 31 37 32 37 35 34 39 32 30 38 36 38 31 +Key Attributes: +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDwSU/lDQtH0IAk +cbMyYGRMnAvW5Q71pTu7Cn6fNRJS35okajb2oLsWrkkNeFX1989cxdHl2iDiGabQ +3napGVyzyatM0XQWClVi/EGq5m90D7wlU0WhL3LtpPA2tdtnxDBK2ax+jzEavyzb +caTtDJqNMEf3qLyXafxXRZ0eaTgb8cp6QzMOXIfeC3fCNVqJ7HCqYRwG6iP+uZzn +Q5IPMdw2sVTsOo8McXXREfxY2J7QWi9QG4ahpkcBAwCF8BFRQZbdCO+rzVY9O309 +4a8vjzcawndVfpBze2JjFVtmBCUbZqMEodhzCMr6zfqrpXFOpnM+zZJyELiRJiIP +6KiEKSutAgMBAAECggEBANNY6HkjXASyk8N6bo+k0RPBPXiqyNmvmDYQKQeH+rIC +EuZstiN/hI+ShJbgfVt3uGB1bwWpMrssrNmSkvRxZmSMwaszn9OzCx+hmXDkdquz +G14JPHll7sSwCslUc8N1gLSVeW9oK1zHQoFSGCqYp2gAS4y+UgMsKdPpWyVgjwWj +lm5FwWOAzQ+gWlGX2Ist0AgskId4M/glWV/fAHP7op8d0UTeEHS3jJi8PfnNQhrb +npCVkJC6U869EXLYAiPE71GDnIwWOJk7V5VODlK1AndD8tqcNPHjYtyhoTMroQVR +Md2ykfm9Oz5FnBiYCjUxIS4Mbd5erA6pfaiA3CSRKFECgYEA/nVKnBex+vg6nr49 +2SwrDnACkA5OVUmhrVe5JHQAQ+43jrJoETqGPzomaQEcr2TN5ULKu5FGELm7Pi9w +BfT2NJZN6SBLFWa5r/vK97/lXYWgDE/bzK0kDRyQWtbHHGCuRK3sFYevOlgb/b3C +Oi0r7uuqDyvLV8smd0NpiA8cpFcCgYEA8b4JqaDNVIetT4eXCURJ8M/+ne47J6a3 +Zfi0A0Gk2vChNYrP7XSoHKuYEl2QblzU4qC9fL/I10DBx9heKeXvh3s/Dsn0mTMT +m1Z+Fo9ksEwc4kwcTzbpVXP52NXP2rvPzZnSPSdKMc/48gw6bNgiE317QCYdijYe +l97+5uCIzZsCgYASmRQI8Jprk3UFYTY4B0hmV714NfN3vFf6yWyYw3m5fVHGNjfw ++mwRdviTuCcWkrGRzh3vM6EBW/HZi7IOXWcZVNsA7QFP4SA1QpwFG5tyCHA4NiYE +gase4jWSzhvjcRWLo4Kb2DzwcLwrAZGOmvqZDdRyI2tLUWfQU7cE4MXhJQKBgQDQ +/p3189pwsRfpwOyYC1ztf7S+Lx8fSagW1awzgIYY7p5A3vCidw98MfG4NwHOGB3I +jHUlq9zkE800jF/kUzEBbVD35Su9YwYZbu51bKT9MeBq2KhE59FUmn6vszIPBf5C +3zB+xEAFzqqIAIBmZ3kWZo6uyAUT33QVkqnHSuma7wKBgDFaQVpvQLx6O9XdzrG9 +EL4tWJCRwT+Ez8r4+zt+Lk08nUnILSObPgQ5HgRaRXT7gQ5GzSZAvqEoCC2O3g0P +rHFa8468QPClkJ1LSkOjXv+5oXu27XxQVMX465P3wM6PyD+j+WAveEnV64O7Iwu7 +NsRkLXJHFMQmFXczYZUpgc57 +-----END PRIVATE KEY----- diff --git a/custom_components/jbl_integration/coordinator.py b/custom_components/jbl_integration/coordinator.py index 08acb57..f6b4b39 100644 --- a/custom_components/jbl_integration/coordinator.py +++ b/custom_components/jbl_integration/coordinator.py @@ -1,14 +1,19 @@ """Sensor platform for JBL integration.""" import aiohttp import async_timeout +import requests import json import logging +import urllib3 +import ssl +import certifi from datetime import timedelta from homeassistant.helpers.entity import Entity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -20,8 +25,13 @@ def __init__(self, hass, entry, address, pollingRate): """Initialize the coordinator.""" self._entry = entry self.address = address + self.hass = hass self.pollingRate = pollingRate self.data = {} + ssl_context = ssl.create_default_context(cafile=certifi.where()) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + self.sslcontext = ssl_context super().__init__( hass, _LOGGER, @@ -31,20 +41,38 @@ def __init__(self, hass, entry, address, pollingRate): ) async def _SetupDeviceInfo(self): + #Setting up cert + cert_path = self.hass.config.path("custom_components/jbl_integration/Cert.pem") + key_path = self.hass.config.path("custom_components/jbl_integration/Key.pem") + self.sslcontext.load_cert_chain(certfile=cert_path, keyfile=key_path) + device_info = await self.getDeviceInfo() + device_Type = await self.getDeviceType() + + # Ensure device_info has all the expected keys and provide fallback values if necessary mac_address = device_info.get("wlan0_mac", "unknown_mac") uuid = device_info.get("uuid", "unknown_uuid") - device_name = device_info.get("name", "JBL Bar 800") + device_name = device_info.get("name", "Unknow_name") serial_number = device_info.get("serial_number", "unknown_serial") firmware_version = device_info.get("firmware", "unknown_firmware") + ip_address = device_info.get("apcli0", "unknown_address") + model = device_Type.get("hm_product_name", "unknown_product") + hw_version = device_Type.get("hardware", "unknown_hardware") + self._device_info = { - "identifiers": {(DOMAIN, self._entry.entry_id, mac_address, uuid, str(uuid).replace("-", "") , self.address)}, + "identifiers": { + (DOMAIN, self._entry.entry_id), + (DOMAIN, mac_address,uuid), + (DOMAIN, str(uuid).replace("-", "")), + (DOMAIN, self.address), + }, "name": device_name, "manufacturer": "HARMAN International Industries", - "model": "JBL Bar 800", + "model": model, + "hw_version": hw_version, "sw_version": firmware_version, "serial_number": serial_number, } @@ -58,12 +86,15 @@ async def _UpdatePollingrate(self,pollingRate): self.update_interval = pollingRate async def _send_command(self, command): + # Disable SSL warnings + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + """Send a command to the device.""" - url = f'http://{self.address}/httpapi.asp' + url = f'https://{self.address}/httpapi.asp' payload = f'command=sendAppController&payload={{"key_pressed": "{command}"}}' async with aiohttp.ClientSession() as session: with async_timeout.timeout(10): - async with session.post(url, data=payload) as response: + async with session.post(url, data=payload, ssl=self.sslcontext) as response: if response.status != 200: _LOGGER.error("Failed to send command: %s", response.status) @@ -79,17 +110,19 @@ async def _async_update_data(self): self.data.update(combined_data) return combined_data - - async def getDeviceInfo(self): - url = f'http://{self.address}/httpapi.asp?command=getDeviceInfo' + # Disable SSL warnings + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + url = f'https://{self.address}/httpapi.asp?command=getDeviceInfo' headers = { 'Accept-Encoding': "gzip", } + async with aiohttp.ClientSession() as session: try: with async_timeout.timeout(10): - async with session.get(url, headers=headers) as response: + async with session.get(url, headers=headers, ssl=self.sslcontext) as response: if response.status == 200: response_text = await response.text() response_json = json.loads(response_text) @@ -106,50 +139,58 @@ async def getDeviceInfo(self): async def getDeviceType(self): """Fetch data from the API.""" - url = f'http://{self.address}:59152/upnp/control/rendercontrol1' + url = f"http://{self.address}:59152/upnp/control/rendercontrol1" + + payload = """ + + + + 0 + """ + headers = { - "Content-type": 'text/xml;charset="utf-8"', - "Soapaction": '"urn:schemas-upnp-org:service:AVTransport:1#GetInfoEx"' + 'Content-type': "text/xml;charset=\"utf-8\"", + 'Soapaction': "\"urn:schemas-upnp-org:service:RenderingControl:1#GetControlDeviceInfo\"" } - payload = """ - - - - - 0 - - - - """ - async with aiohttp.ClientSession() as session: - try: - with async_timeout.timeout(10): - async with session.post(url, headers=headers, data=payload) as response: - if response.status == 200: - response_text = await response.text() - #_LOGGER.debug("Response text: %s", response_text) - # Parse the XML response manually, as it's not JSON - from xml.etree import ElementTree as ET - root = ET.fromstring(response_text) - namespaces = { - 's': 'http://schemas.xmlsoap.org/soap/envelope/', - 'u': 'urn:schemas-upnp-org:service:RenderingControl:1' - } - try: - status = root.find('.//u:GetInfoExResponse/Status', namespaces).text - deviceType = status.get("hm_product_name", "unknown_product") - return deviceType - except AttributeError: - _LOGGER.error("Could not find necessary device type in the response") - return {} + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url, data=payload, headers=headers) as response: + if response.status == 200: + response_text = await response.text() + + # Parse XML response + namespace = { + 's': 'http://schemas.xmlsoap.org/soap/envelope/', + 'u': 'urn:schemas-upnp-org:service:RenderingControl:1' + } + from xml.etree import ElementTree as ET + root = ET.fromstring(response_text) + + # Find the Status element + status_element = root.find('.//u:GetControlDeviceInfoResponse/Status', namespace) + + if status_element is not None: + # Get the text content of the Status element (which is a JSON string) + status_json_str = status_element.text + + # Parse the JSON string into a Python dictionary + status_data = json.loads(status_json_str) + + # Output the status data + return status_data else: _LOGGER.error("Failed to fetch data: %s", response.status) return {} - except Exception as e: - _LOGGER.error("Error fetching data: %s", str(e)) - return {} + except Exception as e: + _LOGGER.error("Error fetching data: %s", str(e)) + return {} + async def requestInfo(self): + # Disable SSL warnings + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + """Fetch data from the API.""" url = f'http://{self.address}:59152/upnp/control/rendertransport1' headers = { @@ -234,14 +275,17 @@ async def setVolume(self, value: float): return {} async def getEQ(self): - url = f'http://{self.address}/httpapi.asp?command=getEQ' + # Disable SSL warnings + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + url = f'https://{self.address}/httpapi.asp?command=getEQ' headers = { 'Accept-Encoding': "gzip", } async with aiohttp.ClientSession() as session: try: with async_timeout.timeout(10): - async with session.get(url, headers=headers) as response: + async with session.get(url, headers=headers, ssl=self.sslcontext) as response: if response.status == 200: response_text = await response.text() response_json = json.loads(response_text) @@ -262,8 +306,11 @@ async def getEQ(self): return {} async def setEQ(self, value: float, frequency): + # Disable SSL warnings + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + """Fetch data from the API.""" - url = f'http://{self._entry.data["ip_address"]}/httpapi.asp' + url = f'https://{self._entry.data["ip_address"]}/httpapi.asp' headers = { 'Accept-Encoding': "gzip", } @@ -276,7 +323,7 @@ async def setEQ(self, value: float, frequency): async with aiohttp.ClientSession() as session: try: with async_timeout.timeout(10): - async with session.post(url, headers=headers, data=payload) as response: + async with session.post(url, headers=headers, data=payload, ssl=self.sslcontext) as response: if response.status != 200: _LOGGER.error("Failed to set EQ: %s", response.status) return {} @@ -284,7 +331,6 @@ async def setEQ(self, value: float, frequency): _LOGGER.error("Error setting EQ: %s", str(e)) return {} - def merge_two_dicts(self,x, y): z = x.copy() # start with keys and values of x z.update(y) # modifies z with keys and values of y