diff --git a/hubitatmaker/hub.py b/hubitatmaker/hub.py index 7205361..abce19c 100644 --- a/hubitatmaker/hub.py +++ b/hubitatmaker/hub.py @@ -1,12 +1,14 @@ """Hubitat API.""" from contextlib import contextmanager from logging import getLogger +import re import socket from types import MappingProxyType from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Union from urllib.parse import ParseResult, quote, urlparse import aiohttp +import getmac from . import server from .const import ID_HSM_STATUS, ID_MODE @@ -31,6 +33,7 @@ class Hub: host: str scheme: str token: str + mac: str _server: server.Server @@ -60,10 +63,11 @@ def __init__( if not host or not app_id or not access_token: raise InvalidConfig() - self.event_url = self._get_event_url(port, event_url) - self.port = self._get_event_port(port, event_url) + self.event_url = _get_event_url(port, event_url) + self.port = _get_event_port(port, event_url) self.app_id = app_id self.token = access_token + self.mac = "" self.set_host(host) @@ -224,6 +228,7 @@ def set_host(self, host: str) -> None: self.host = host_url.netloc or host_url.path self.base_url = f"{self.scheme}://{self.host}" self.api_url = f"{self.base_url}/apps/api/{self.app_id}" + self.mac = _get_mac_address(self.host) or "" async def set_port(self, port: int) -> None: """Set the port that the event listener server will listen on. @@ -242,35 +247,6 @@ async def _check_api(self) -> None: """ await self._api_request("devices") - def _get_event_port( - self, port: Optional[int], event_url: Optional[str] - ) -> Optional[int]: - """Given an optional port and event URL, return the event port""" - if port is not None: - return port - if event_url is not None: - u = urlparse(event_url) - return u.port - return None - - def _get_event_url( - self, port: Optional[int], event_url: Optional[str] - ) -> Optional[str]: - """Given an optional port and event URL, return a complete event URL""" - if event_url is not None: - u = urlparse(event_url) - if u.port is None and port is not None: - return ParseResult( - scheme=u.scheme, - netloc=f"{u.hostname}:{port}", - path=u.path, - params=u.params, - query=u.query, - fragment=u.fragment, - ).geturl() - return event_url - return None - def _process_event(self, event: Dict[str, Any]) -> None: """Process an event received from the hub.""" try: @@ -418,3 +394,37 @@ def _open_socket(*args: Any, **kwargs: Any) -> Iterator[socket.socket]: yield s finally: s.close() + + +def _get_event_port(port: Optional[int], event_url: Optional[str]) -> Optional[int]: + """Given an optional port and event URL, return the event port""" + if port is not None: + return port + if event_url is not None: + u = urlparse(event_url) + return u.port + return None + + +def _get_event_url(port: Optional[int], event_url: Optional[str]) -> Optional[str]: + """Given an optional port and event URL, return a complete event URL""" + if event_url is not None: + u = urlparse(event_url) + if u.port is None and port is not None: + return ParseResult( + scheme=u.scheme, + netloc=f"{u.hostname}:{port}", + path=u.path, + params=u.params, + query=u.query, + fragment=u.fragment, + ).geturl() + return event_url + return None + + +def _get_mac_address(host: str) -> Optional[str]: + """Return the mac address of a remote host.""" + if re.match("\\d+\\.\\d+\\.\\d+\\.\\d+", host): + return getmac.get_mac_address(ip=host) + return getmac.get_mac_address(hostname=host) diff --git a/hubitatmaker/tests/test_hub.py b/hubitatmaker/tests/test_hub.py index eaa3410..d73579a 100644 --- a/hubitatmaker/tests/test_hub.py +++ b/hubitatmaker/tests/test_hub.py @@ -147,12 +147,17 @@ async def __aexit__(self, exc_type, exc, tb): return fake_request +def fake_get_mac_address(**kwargs: str): + return "aa:bb:cc:dd:ee:ff" + + class TestHub(TestCase): def setUp(self): global requests requests = [] load_data() + @patch("getmac.get_mac_address", new=fake_get_mac_address) def test_hub_checks_arguments(self) -> None: """The hub should check for its required inputs.""" self.assertRaises(InvalidConfig, Hub, "", "1234", "token") @@ -160,11 +165,13 @@ def test_hub_checks_arguments(self) -> None: self.assertRaises(InvalidConfig, Hub, "1.2.3.4", "1234", "") Hub("1.2.3.4", "1234", "token") + @patch("getmac.get_mac_address", new=fake_get_mac_address) def test_initial_values(self) -> None: """Hub properties should have expected initial values.""" hub = Hub("1.2.3.4", "1234", "token") self.assertEqual(list(hub.devices), []) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_start_server(self, MockServer) -> None: @@ -173,6 +180,7 @@ def test_start_server(self, MockServer) -> None: wait_for(hub.start()) self.assertTrue(MockServer.called) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_start(self, MockServer) -> None: @@ -192,6 +200,7 @@ def test_start(self, MockServer) -> None: self.assertRegex(requests[-2]["url"], "modes$") self.assertRegex(requests[-1]["url"], "hsm$") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch( "aiohttp.request", new=create_fake_request({"/hsm": FakeResponse(400, url="/hsm")}), @@ -207,6 +216,7 @@ def test_start_no_hsm(self, MockServer) -> None: self.assertRegex(requests[-2]["url"], "modes$") self.assertRegex(requests[-1]["url"], "hsm$") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_default_event_url(self, MockServer) -> None: @@ -217,6 +227,7 @@ def test_default_event_url(self, MockServer) -> None: url = unquote(requests[0]["url"]) self.assertRegex(url, r"http://127.0.0.1:81$") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_custom_event_url(self, MockServer) -> None: @@ -227,6 +238,7 @@ def test_custom_event_url(self, MockServer) -> None: url = unquote(requests[0]["url"]) self.assertRegex(url, r"http://foo\.local$") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_custom_event_url_without_port(self, MockServer) -> None: @@ -237,6 +249,7 @@ def test_custom_event_url_without_port(self, MockServer) -> None: url = unquote(requests[0]["url"]) self.assertRegex(url, r"http://foo\.local:420$") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_custom_event_port(self, MockServer) -> None: @@ -246,6 +259,7 @@ def test_custom_event_port(self, MockServer) -> None: wait_for(hub.start()) self.assertEqual(MockServer.call_args[0][2], 420) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_custom_event_port_from_url(self, MockServer) -> None: @@ -255,6 +269,7 @@ def test_custom_event_port_from_url(self, MockServer) -> None: wait_for(hub.start()) self.assertEqual(MockServer.call_args[0][2], 416) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_custom_event_port_and_url(self, MockServer) -> None: @@ -264,6 +279,7 @@ def test_custom_event_port_and_url(self, MockServer) -> None: wait_for(hub.start()) self.assertEqual(MockServer.call_args[0][2], 420) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_stop_server(self, MockServer) -> None: @@ -274,6 +290,7 @@ def test_stop_server(self, MockServer) -> None: hub.stop() self.assertTrue(MockServer.return_value.stop.called) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_devices_loaded(self, MockServer) -> None: @@ -282,6 +299,7 @@ def test_devices_loaded(self, MockServer) -> None: wait_for(hub.start()) self.assertEqual(len(hub.devices), 9) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_event(self, MockServer) -> None: @@ -297,6 +315,7 @@ def test_process_event(self, MockServer) -> None: attr = device.attributes["switch"] self.assertEqual(attr.value, "on") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_mode_event(self, MockServer) -> None: @@ -317,6 +336,7 @@ def listener(_: Any): hub._process_event(events["mode"]) self.assertTrue(handler_called) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_hsm_event(self, MockServer) -> None: @@ -337,6 +357,7 @@ def listener(_: Any): hub._process_event(events["hsm"]) self.assertTrue(handler_called) + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_other_event(self, MockServer) -> None: @@ -352,6 +373,7 @@ def test_process_other_event(self, MockServer) -> None: attr = device.attributes["switch"] self.assertEqual(attr.value, "off") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_set_hsm(self, MockServer) -> None: @@ -365,6 +387,7 @@ def test_process_set_hsm(self, MockServer) -> None: hub._process_event(events["hsm"]) self.assertEqual(hub.hsm_status, "armedAway") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_process_set_mode(self, MockServer) -> None: @@ -378,6 +401,7 @@ def test_process_set_mode(self, MockServer) -> None: hub._process_event(events["mode"]) self.assertEqual(hub.mode, "Evening") + @patch("getmac.get_mac_address", new=fake_get_mac_address) @patch("aiohttp.request", new=create_fake_request()) @patch("hubitatmaker.server.Server") def test_set_event_url(self, MockServer) -> None: diff --git a/poetry.lock b/poetry.lock index bb8264c..c54f008 100644 --- a/poetry.lock +++ b/poetry.lock @@ -138,6 +138,14 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.6.0a1,<2.7.0" pyflakes = ">=2.2.0,<2.3.0" +[[package]] +name = "getmac" +version = "0.8.2" +description = "Get MAC addresses of remote hosts and local interfaces" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "identify" version = "1.5.6" @@ -426,7 +434,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.7.1" -content-hash = "185b9707c67aa3bbedfc84476c1858c4fc675d80dcc32c3b6d30753e8f4bba5a" +content-hash = "83a049ba0c7a15c44de13705ff29dd910db57864e8e60d2f742373367db08f0c" [metadata.files] aiohttp = [ @@ -511,6 +519,10 @@ flake8 = [ {file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"}, {file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"}, ] +getmac = [ + {file = "getmac-0.8.2-py2.py3-none-any.whl", hash = "sha256:2e4aef2dd6c3befccd7cf9e18badddd24ab1992b928e2e811d415ed47137c547"}, + {file = "getmac-0.8.2.tar.gz", hash = "sha256:d501d20b71856248cfa07a8758192e86a01077910afb8b659a89946c4d52d368"}, +] identify = [ {file = "identify-1.5.6-py2.py3-none-any.whl", hash = "sha256:3139bf72d81dfd785b0a464e2776bd59bdc725b4cc10e6cf46b56a0db931c82e"}, {file = "identify-1.5.6.tar.gz", hash = "sha256:969d844b7a85d32a5f9ac4e163df6e846d73c87c8b75847494ee8f4bd2186421"}, diff --git a/pyproject.toml b/pyproject.toml index 0d9e2e8..66a3cf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ include = ["LICENSE"] [tool.poetry.dependencies] aiohttp = "^3.6.2" python = "^3.7.1" +getmac = "^0.8.2" [tool.poetry.dev-dependencies] flake8 = "^3.8.3"