diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..ee23d2f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,266 @@ +[MAIN] + +# Files or directories to be skipped. They should be base names, not paths. +ignore=tests + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma. +good-names=ex,fp,i,id,j,k,on,Run,T + +[DESIGN] + +# Maximum number of attributes for a class (see R0902). +max-attributes=8 + +[MESSAGES CONTROL] + +disable=format, + abstract-method, + cyclic-import, + duplicate-code, + inconsistent-return-statements, + locally-disabled, + not-context-manager, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-public-methods, + too-many-boolean-expressions, + wrong-import-order, + consider-using-f-string, + consider-using-namedtuple-or-dataclass, + consider-using-assignment-expr, + possibly-used-before-assignment, + + # Handled by ruff + # Ref: + await-outside-async, # PLE1142 + bad-str-strip-call, # PLE1310 + bad-string-format-type, # PLE1307 + bidirectional-unicode, # PLE2502 + continue-in-finally, # PLE0116 + duplicate-bases, # PLE0241 + misplaced-bare-raise, # PLE0704 + format-needs-mapping, # F502 + function-redefined, # F811 + # Needed because ruff does not understand type of __all__ generated by a function + # invalid-all-format, # PLE0605 + invalid-all-object, # PLE0604 + invalid-character-backspace, # PLE2510 + invalid-character-esc, # PLE2513 + invalid-character-nul, # PLE2514 + invalid-character-sub, # PLE2512 + invalid-character-zero-width-space, # PLE2515 + logging-too-few-args, # PLE1206 + logging-too-many-args, # PLE1205 + missing-format-string-key, # F524 + mixed-format-string, # F506 + no-method-argument, # N805 + no-self-argument, # N805 + nonexistent-operator, # B002 + nonlocal-without-binding, # PLE0117 + not-in-loop, # F701, F702 + notimplemented-raised, # F901 + return-in-init, # PLE0101 + return-outside-function, # F706 + syntax-error, # E999 + too-few-format-args, # F524 + too-many-format-args, # F522 + too-many-star-expressions, # F622 + truncated-format-string, # F501 + undefined-all-variable, # F822 + undefined-variable, # F821 + used-prior-global-declaration, # PLE0118 + yield-inside-async-function, # PLE1700 + yield-outside-function, # F704 + anomalous-backslash-in-string, # W605 + assert-on-string-literal, # PLW0129 + assert-on-tuple, # F631 + bad-format-string, # W1302, F + bad-format-string-key, # W1300, F + bare-except, # E722 + binary-op-exception, # PLW0711 + cell-var-from-loop, # B023 + # dangerous-default-value, # B006, ruff catches new occurrences, needs more work + duplicate-except, # B014 + duplicate-key, # F601 + duplicate-string-formatting-argument, # F + duplicate-value, # F + eval-used, # S307 + exec-used, # S102 + expression-not-assigned, # B018 + f-string-without-interpolation, # F541 + forgotten-debug-statement, # T100 + format-string-without-interpolation, # F + # global-statement, # PLW0603, ruff catches new occurrences, needs more work + global-variable-not-assigned, # PLW0602 + implicit-str-concat, # ISC001 + import-self, # PLW0406 + inconsistent-quotes, # Q000 + invalid-envvar-default, # PLW1508 + keyword-arg-before-vararg, # B026 + logging-format-interpolation, # G + logging-fstring-interpolation, # G + logging-not-lazy, # G + misplaced-future, # F404 + named-expr-without-context, # PLW0131 + nested-min-max, # PLW3301 + pointless-statement, # B018 + raise-missing-from, # B904 + redefined-builtin, # A001 + try-except-raise, # TRY302 + unused-argument, # ARG001, we don't use it + unused-format-string-argument, #F507 + unused-format-string-key, # F504 + unused-import, # F401 + unused-variable, # F841 + useless-else-on-loop, # PLW0120 + wildcard-import, # F403 + bad-classmethod-argument, # N804 + consider-iterating-dictionary, # SIM118 + empty-docstring, # D419 + invalid-name, # N815 + line-too-long, # E501, disabled globally + missing-class-docstring, # D101 + missing-final-newline, # W292 + missing-function-docstring, # D103 + missing-module-docstring, # D100 + multiple-imports, #E401 + singleton-comparison, # E711, E712 + subprocess-run-check, # PLW1510 + superfluous-parens, # UP034 + ungrouped-imports, # I001 + unidiomatic-typecheck, # E721 + unnecessary-direct-lambda-call, # PLC3002 + unnecessary-lambda-assignment, # PLC3001 + unnecessary-pass, # PIE790 + unneeded-not, # SIM208 + useless-import-alias, # PLC0414 + wrong-import-order, # I001 + wrong-import-position, # E402 + comparison-of-constants, # PLR0133 + comparison-with-itself, # PLR0124 + consider-alternative-union-syntax, # UP007 + consider-merging-isinstance, # PLR1701 + consider-using-alias, # UP006 + consider-using-dict-comprehension, # C402 + consider-using-generator, # C417 + consider-using-get, # SIM401 + consider-using-set-comprehension, # C401 + consider-using-sys-exit, # PLR1722 + consider-using-ternary, # SIM108 + literal-comparison, # F632 + property-with-parameters, # PLR0206 + super-with-arguments, # UP008 + too-many-branches, # PLR0912 + too-many-return-statements, # PLR0911 + too-many-statements, # PLR0915 + trailing-comma-tuple, # COM818 + unnecessary-comprehension, # C416 + use-a-generator, # C417 + use-dict-literal, # C406 + use-list-literal, # C405 + useless-object-inheritance, # UP004 + useless-return, # PLR1711 + no-else-break, # RET508 + no-else-continue, # RET507 + no-else-raise, # RET506 + no-else-return, # RET505 + broad-except, # BLE001 + protected-access, # SLF001 + # no-self-use, # PLR6301 # Optional plugin, not enabled + + # Handled by mypy + # Ref: + abstract-class-instantiated, + arguments-differ, + assigning-non-slot, + assignment-from-no-return, + assignment-from-none, + bad-exception-cause, + bad-format-character, + bad-reversed-sequence, + bad-super-call, + bad-thread-instantiation, + catching-non-exception, + comparison-with-callable, + deprecated-class, + dict-iter-missing-items, + format-combined-specification, + global-variable-undefined, + import-error, + inconsistent-mro, + inherit-non-class, + init-is-generator, + invalid-class-object, + invalid-enum-extension, + invalid-envvar-value, + invalid-format-returned, + invalid-hash-returned, + invalid-metaclass, + invalid-overridden-method, + invalid-repr-returned, + invalid-sequence-index, + invalid-slice-index, + invalid-slots-object, + invalid-slots, + invalid-star-assignment-target, + invalid-str-returned, + invalid-unary-operand-type, + invalid-unicode-codec, + isinstance-second-argument-not-valid-type, + method-hidden, + misplaced-format-function, + missing-format-argument-key, + missing-format-attribute, + missing-kwoa, + no-member, + no-value-for-parameter, + non-iterator-returned, + non-str-assignment-to-dunder-name, + nonlocal-and-global, + not-a-mapping, + not-an-iterable, + not-async-context-manager, + not-callable, + not-context-manager, + overridden-final-method, + raising-bad-type, + raising-non-exception, + redundant-keyword-arg, + relative-beyond-top-level, + self-cls-assignment, + signature-differs, + star-needs-assignment-target, + subclassed-final-class, + super-without-brackets, + too-many-function-args, + typevar-double-variance, + typevar-name-mismatch, + unbalanced-dict-unpacking, + unbalanced-tuple-unpacking, + unexpected-keyword-arg, + unhashable-member, + unpacking-non-sequence, + unsubscriptable-object, + unsupported-assignment-operation, + unsupported-binary-operation, + unsupported-delete-operation, + unsupported-membership-test, + used-before-assignment, + using-final-decorator-in-unsupported-version, + wrong-exception-operation, +] + +[FORMAT] + +max-line-length = 88 + +[SIMILARITIES] + +ignore-imports=yes diff --git a/api-info/api_info.py b/api-info/api_info.py index d09a3a2..eb4dddd 100644 --- a/api-info/api_info.py +++ b/api-info/api_info.py @@ -6,110 +6,119 @@ API_URL = "https://mypagesapi.sectoralarm.net/api" + async def main(): message_headers = { - "API-Version":"6", - "Platform":"iOS", - "User-Agent":" SectorAlarm/387 CFNetwork/1206 Darwin/20.1.0", - "Version":"2.0.27", - "Connection":"keep-alive", - "Content-Type":"application/json" + "API-Version": "6", + "Platform": "iOS", + "User-Agent": " SectorAlarm/387 CFNetwork/1206 Darwin/20.1.0", + "Version": "2.0.27", + "Connection": "keep-alive", + "Content-Type": "application/json", } - json_data = { - "UserId": "INSERT_USERNAME_HERE", - "Password": "INSERT_PASSWORD_HERE" - } + json_data = {"UserId": "INSERT_USERNAME_HERE", "Password": "INSERT_PASSWORD_HERE"} async with aiohttp.ClientSession() as session: - print(datetime.now()) - async with session.post("https://mypagesapi.sectoralarm.net/api/Login/Login", - headers=message_headers, json=json_data) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to send Login: %d", response.status) - print("Funkar inte") - else: - data_out = await response.json() - AUTH_TOKEN = data_out['AuthorizationToken'] - #_LOGGER.debug("Sector: AUTH: %s", AUTH_TOKEN) - - message_headers = { - "Authorization": AUTH_TOKEN, - "API-Version":"6", - "Platform":"iOS", - "User-Agent":"SectorAlarm/356 CFNetwork/1152.2 Darwin/19.4.0", - "Version":"2.0.20", - "Connection":"keep-alive", - "Content-Type":"application/json" - } - print(datetime.now()) - async with session.get(API_URL + "/Panel/getFullSystem", - headers=message_headers) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to get Full system: %d", response.status) - #raise PlatformNotReady - print("Funkar inte") - else: - firstrun = await response.json() - - PANELID = firstrun['Panel']['PanelId'] - FULLSYSTEMINFO = firstrun - print("FULLSYSTEMINFO") - print (FULLSYSTEMINFO) - - print(datetime.now()) - async with session.get(API_URL + "/Panel/GetPanelStatus?panelId={}".format(PANELID), - headers=message_headers) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to get Full system: %d", response.status) - #raise PlatformNotReady - print("Funkar inte") - else: - GetPanelStatus = await response.json() - - print("GetPanelStatus") - print(GetPanelStatus) - - print(datetime.now()) - async with session.get(API_URL + "/Panel/GetTemperatures?panelId={}".format(PANELID), - headers=message_headers) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to get Full system: %d", response.status) - #raise PlatformNotReady - print("Funkar inte") - else: - GetTemperatures = await response.json() - - print("GetTemperatures") - print(GetTemperatures) - - print(datetime.now()) - async with session.get(API_URL + "/Panel/GetLockStatus?panelId={}".format(PANELID), - headers=message_headers) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to get Full system: %d", response.status) - #raise PlatformNotReady - print("Funkar inte") - else: - GetLockStatus = await response.json() - - print("GetLockStatus") - print(GetLockStatus) - - print(datetime.now()) - async with session.get(API_URL + "/Panel/GetLogs?panelId={}".format(PANELID), - headers=message_headers) as response: - if response.status != 200: - #_LOGGER.debug("Sector: Failed to get Full system: %d", response.status) - #raise PlatformNotReady - print("Funkar inte") - else: - GetLogs = await response.json() - - print("GetLogs") - print(GetLogs) - - print(datetime.now()) - + print(datetime.now()) + async with session.post( + "https://mypagesapi.sectoralarm.net/api/Login/Login", + headers=message_headers, + json=json_data, + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to send Login: %d", response.status) + print("Funkar inte") + else: + data_out = await response.json() + AUTH_TOKEN = data_out["AuthorizationToken"] + # _LOGGER.debug("Sector: AUTH: %s", AUTH_TOKEN) + + message_headers = { + "Authorization": AUTH_TOKEN, + "API-Version": "6", + "Platform": "iOS", + "User-Agent": "SectorAlarm/356 CFNetwork/1152.2 Darwin/19.4.0", + "Version": "2.0.20", + "Connection": "keep-alive", + "Content-Type": "application/json", + } + print(datetime.now()) + async with session.get( + API_URL + "/Panel/getFullSystem", headers=message_headers + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to get Full system: %d", response.status) + # raise PlatformNotReady + print("Funkar inte") + else: + firstrun = await response.json() + + PANELID = firstrun["Panel"]["PanelId"] + FULLSYSTEMINFO = firstrun + print("FULLSYSTEMINFO") + print(FULLSYSTEMINFO) + + print(datetime.now()) + async with session.get( + API_URL + "/Panel/GetPanelStatus?panelId={}".format(PANELID), + headers=message_headers, + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to get Full system: %d", response.status) + # raise PlatformNotReady + print("Funkar inte") + else: + GetPanelStatus = await response.json() + + print("GetPanelStatus") + print(GetPanelStatus) + + print(datetime.now()) + async with session.get( + API_URL + "/Panel/GetTemperatures?panelId={}".format(PANELID), + headers=message_headers, + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to get Full system: %d", response.status) + # raise PlatformNotReady + print("Funkar inte") + else: + GetTemperatures = await response.json() + + print("GetTemperatures") + print(GetTemperatures) + + print(datetime.now()) + async with session.get( + API_URL + "/Panel/GetLockStatus?panelId={}".format(PANELID), + headers=message_headers, + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to get Full system: %d", response.status) + # raise PlatformNotReady + print("Funkar inte") + else: + GetLockStatus = await response.json() + + print("GetLockStatus") + print(GetLockStatus) + + print(datetime.now()) + async with session.get( + API_URL + "/Panel/GetLogs?panelId={}".format(PANELID), + headers=message_headers, + ) as response: + if response.status != 200: + # _LOGGER.debug("Sector: Failed to get Full system: %d", response.status) + # raise PlatformNotReady + print("Funkar inte") + else: + GetLogs = await response.json() + + print("GetLogs") + print(GetLogs) + + print(datetime.now()) loop = asyncio.get_event_loop() diff --git a/custom_components/sector/__init__.py b/custom_components/sector/__init__.py index 2e162b8..377047a 100644 --- a/custom_components/sector/__init__.py +++ b/custom_components/sector/__init__.py @@ -1,39 +1,35 @@ """Sector Alarm integration for Home Assistant.""" + from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, PLATFORMS -from .coordinator import SectorDataUpdateCoordinator +from .const import PLATFORMS +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: SectorAlarmConfigEntry) -> bool: """Set up Sector Alarm from a config entry.""" coordinator = SectorDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + entry.runtime_data = coordinator entry.async_on_unload(entry.add_update_listener(async_update_listener)) - # Schedule the platform setups to avoid blocking the event loop - hass.async_create_task( - hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: +async def async_update_listener( + hass: HomeAssistant, entry: SectorAlarmConfigEntry +) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_unload_entry( + hass: HomeAssistant, entry: SectorAlarmConfigEntry +) -> bool: """Unload a Sector Alarm config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id) -# await coordinator.api.logout() - await coordinator.api.close() - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/custom_components/sector/alarm_control_panel.py b/custom_components/sector/alarm_control_panel.py index 0f73fc8..ab0773d 100644 --- a/custom_components/sector/alarm_control_panel.py +++ b/custom_components/sector/alarm_control_panel.py @@ -1,4 +1,5 @@ """Alarm Control Panel for Sector Alarm integration.""" + from __future__ import annotations import logging @@ -7,7 +8,6 @@ AlarmControlPanelEntity, AlarmControlPanelEntityFeature, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -16,10 +16,11 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import SectorDataUpdateCoordinator +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -30,11 +31,14 @@ 0: STATE_ALARM_PENDING, } + async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up the Sector Alarm control panel.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data async_add_entities([SectorAlarmControlPanel(coordinator)]) @@ -50,10 +54,14 @@ def __init__(self, coordinator: SectorDataUpdateCoordinator) -> None: """Initialize the control panel.""" super().__init__(coordinator) panel_status = coordinator.data.get("panel_status", {}) - self._serial_no = panel_status.get("SerialNo") or coordinator.entry.data.get("panel_id") + self._serial_no = panel_status.get("SerialNo") or coordinator.entry.data.get( + "panel_id" + ) self._attr_unique_id = f"{self._serial_no}_alarm_panel" self._attr_name = "Sector Alarm Panel" - _LOGGER.debug(f"Initialized alarm control panel with unique_id: {self._attr_unique_id}") + _LOGGER.debug( + "Initialized alarm control panel with unique_id: %s", self._attr_unique_id + ) @property def state(self): @@ -65,7 +73,9 @@ def state(self): status_code = status.get("Status", 0) # Map the status code to the appropriate Home Assistant state mapped_state = ALARM_STATE_TO_HA_STATE.get(status_code, STATE_ALARM_PENDING) - _LOGGER.debug(f"Alarm status_code: {status_code}, Mapped state: {mapped_state}") + _LOGGER.debug( + "Alarm status_code: %s, Mapped state: %s", status_code, mapped_state + ) return mapped_state async def async_alarm_arm_away(self, code=None): diff --git a/custom_components/sector/binary_sensor.py b/custom_components/sector/binary_sensor.py index de51fd6..bc329fc 100644 --- a/custom_components/sector/binary_sensor.py +++ b/custom_components/sector/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensor platform for Sector Alarm integration.""" + from __future__ import annotations import logging @@ -7,22 +8,24 @@ BinarySensorDeviceClass, BinarySensorEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, CATEGORY_MODEL_MAPPING -from .coordinator import SectorDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up Sector Alarm binary sensors.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data devices = coordinator.data.get("devices", {}) entities = [] @@ -32,7 +35,12 @@ async def async_setup_entry( device_type = device.get("type", "") device_model = device.get("model", "") - _LOGGER.debug(f"Adding binary sensor {serial_no} as model '{device_model}' with type '{device_type}'") + _LOGGER.debug( + "Adding binary sensor %s as model '%s' with type '%s'", + serial_no, + device_model, + device_type, + ) if "closed" in sensors: entities.append( @@ -46,7 +54,12 @@ async def async_setup_entry( ) ) if "low_battery" in sensors: - _LOGGER.debug(f"Adding battery to {serial_no} as model '{device_model}' with type '{device_type}'") + _LOGGER.debug( + "Adding battery to %s as model '%s' with type '%s'", + serial_no, + device_model, + device_type, + ) entities.append( SectorAlarmBinarySensor( coordinator, @@ -58,7 +71,12 @@ async def async_setup_entry( ) ) else: - _LOGGER.warning(f"No low_battery sensor found for device {serial_no} ({device_model}). Confirmed sensors: {sensors}") + _LOGGER.warning( + "No low_battery sensor found for device %s (%s). Confirmed sensors: %s", + serial_no, + device_model, + sensors, + ) if "leak_detected" in sensors: entities.append( SectorAlarmBinarySensor( @@ -125,7 +143,7 @@ def __init__( ) self._attr_device_class = device_class _LOGGER.debug( - f"Initialized binary sensor with unique_id: {self._attr_unique_id}" + "Initialized binary sensor with unique_id: %s", self._attr_unique_id ) @property @@ -135,7 +153,9 @@ def is_on(self) -> bool: if device: sensor_value = device["sensors"].get(self._sensor_type) if self._sensor_type == "closed": - return not sensor_value # Invert because "Closed": true means door is closed + return ( + not sensor_value + ) # Invert because "Closed": true means door is closed if self._sensor_type == "low_battery": return sensor_value if self._sensor_type == "alarm": @@ -188,7 +208,7 @@ def __init__( "model": "Alarm Panel", } _LOGGER.debug( - f"Initialized panel online sensor with unique_id: {self._attr_unique_id}" + "Initialized panel online sensor with unique_id: %s", self._attr_unique_id ) @property diff --git a/custom_components/sector/camera.py b/custom_components/sector/camera.py index ff2cc7a..89a2d04 100644 --- a/custom_components/sector/camera.py +++ b/custom_components/sector/camera.py @@ -1,24 +1,27 @@ # camera.py """Camera platform for Sector Alarm integration.""" + from __future__ import annotations import logging from homeassistant.components.camera import Camera -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import SectorDataUpdateCoordinator +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up Sector Alarm cameras.""" coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -41,13 +44,16 @@ class SectorAlarmCamera(CoordinatorEntity, Camera): def __init__(self, coordinator: SectorDataUpdateCoordinator, camera_data: dict): """Initialize the camera.""" super().__init__(coordinator) + Camera.__init__(self) self._camera_data = camera_data self._serial_no = str(camera_data.get("SerialNo") or camera_data.get("Serial")) self._attr_unique_id = f"{self._serial_no}_camera" self._attr_name = camera_data.get("Label", "Sector Camera") - _LOGGER.debug(f"Initialized camera with unique_id: {self._attr_unique_id}") + _LOGGER.debug("Initialized camera with unique_id: %s", self._attr_unique_id) - async def async_camera_image(self): + async def async_camera_image( + self, width: int | None = None, height: int | None = None + ): """Return a still image response from the camera.""" # Implement the method to retrieve an image from the camera image = await self.coordinator.api.get_camera_image(self._serial_no) @@ -60,7 +66,7 @@ def device_info(self) -> DeviceInfo: identifiers={(DOMAIN, self._serial_no)}, name=self._attr_name, manufacturer="Sector Alarm", -# model="Camera", + # model="Camera", ) @property diff --git a/custom_components/sector/client.py b/custom_components/sector/client.py index 7bab17f..0c9d1df 100644 --- a/custom_components/sector/client.py +++ b/custom_components/sector/client.py @@ -1,14 +1,17 @@ """Client module for interacting with Sector Alarm API.""" + from __future__ import annotations import asyncio +import base64 import logging import aiohttp import async_timeout +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import LOGGER -from .endpoints import get_data_endpoints, get_action_endpoints +from .endpoints import get_action_endpoints, get_data_endpoints _LOGGER = logging.getLogger(__name__) @@ -22,8 +25,9 @@ class SectorAlarmAPI: API_URL = "https://mypagesapi.sectoralarm.net" - def __init__(self, email, password, panel_id, panel_code): + def __init__(self, hass: HomeAssistant, email, password, panel_id, panel_code): """Initialize the API client.""" + self.hass = hass self.email = email self.password = password self.panel_id = panel_id @@ -37,7 +41,7 @@ def __init__(self, email, password, panel_id, panel_code): async def login(self): """Authenticate with the API and obtain an access token.""" if self.session is None: - self.session = aiohttp.ClientSession() + self.session = async_get_clientsession(self.hass) login_url = f"{self.API_URL}/api/Login/Login" payload = { @@ -48,7 +52,9 @@ async def login(self): async with async_timeout.timeout(10): async with self.session.post(login_url, json=payload) as response: if response.status != 200: - _LOGGER.error(f"Login failed with status code {response.status}") + _LOGGER.error( + "Login failed with status code %s", response.status + ) raise AuthenticationError("Invalid credentials") data = await response.json() self.access_token = data.get("AuthorizationToken") @@ -59,12 +65,12 @@ async def login(self): "Authorization": f"Bearer {self.access_token}", "Accept": "application/json", } - except asyncio.TimeoutError: + except asyncio.TimeoutError as err: _LOGGER.error("Timeout occurred during login") - raise AuthenticationError("Timeout during login") - except aiohttp.ClientError as e: - _LOGGER.error(f"Client error during login: {e}") - raise AuthenticationError("Client error during login") + raise AuthenticationError("Timeout during login") from err + except aiohttp.ClientError as err: + _LOGGER.error("Client error during login: %s", str(err)) + raise AuthenticationError("Client error during login") from err async def retrieve_all_data(self): """Retrieve all relevant data from the API.""" @@ -79,13 +85,13 @@ async def retrieve_all_data(self): payload = {"PanelId": self.panel_id} response = await self._post(url, payload) else: - _LOGGER.error(f"Unsupported HTTP method {method} for endpoint {key}") + _LOGGER.error("Unsupported HTTP method %s for endpoint %s", method, key) continue if response: data[key] = response else: - _LOGGER.info(f"No data retrieved for {key}") + _LOGGER.info("No data retrieved for %s", key) locks_status = await self.get_lock_status() data["Lock Status"] = locks_status @@ -108,46 +114,62 @@ async def _get(self, url): async with async_timeout.timeout(10): async with self.session.get(url, headers=self.headers) as response: if response.status == 200: - content_type = response.headers.get('Content-Type', '') - if 'application/json' in content_type: + content_type = response.headers.get("Content-Type", "") + if "application/json" in content_type: return await response.json() else: text = await response.text() - _LOGGER.error(f"Received non-JSON response from {url}: {text}") + _LOGGER.error( + "Received non-JSON response from %s: %s", url, text + ) return None else: text = await response.text() - _LOGGER.error(f"GET request to {url} failed with status code {response.status}, response: {text}") + _LOGGER.error( + "GET request to %s failed with status code %s, response: %s", + url, + response.status, + text, + ) return None except asyncio.TimeoutError: - _LOGGER.error(f"Timeout occurred during GET request to {url}") + _LOGGER.error("Timeout occurred during GET request to %s", url) return None except aiohttp.ClientError as e: - _LOGGER.error(f"Client error during GET request to {url}: {e}") + _LOGGER.error("Client error during GET request to %s: %s", url, str(e)) return None async def _post(self, url, payload): """Helper method to perform POST requests with timeout.""" try: async with async_timeout.timeout(10): - async with self.session.post(url, json=payload, headers=self.headers) as response: + async with self.session.post( + url, json=payload, headers=self.headers + ) as response: if response.status == 200: - content_type = response.headers.get('Content-Type', '') - if 'application/json' in content_type: + content_type = response.headers.get("Content-Type", "") + if "application/json" in content_type: return await response.json() else: text = await response.text() - _LOGGER.error(f"Received non-JSON response from {url}: {text}") + _LOGGER.error( + "Received non-JSON response from %s: %s", url, text + ) return None else: text = await response.text() - _LOGGER.error(f"POST request to {url} failed with status code {response.status}, response: {text}") + _LOGGER.error( + "POST request to %s failed with status code %s, response: %s", + url, + response.status, + text, + ) return None except asyncio.TimeoutError: - _LOGGER.error(f"Timeout occurred during POST request to {url}") + _LOGGER.error("Timeout occurred during POST request to %s", url) return None - except aiohttp.ClientError as e: - _LOGGER.error(f"Client error during POST request to {url}: {e}") + except aiohttp.ClientError as err: + _LOGGER.error("Client error during POST request to %s: %s", url, str(err)) return None async def arm_system(self, mode): @@ -192,10 +214,10 @@ async def lock_door(self, serial_no): } result = await self._post(url, payload) if result is not None: - _LOGGER.debug(f"Door {serial_no} locked successfully") + _LOGGER.debug("Door %s locked successfully", serial_no) return True else: - _LOGGER.error(f"Failed to lock door {serial_no}") + _LOGGER.error("Failed to lock door %s", serial_no) return False async def unlock_door(self, serial_no): @@ -209,10 +231,10 @@ async def unlock_door(self, serial_no): } result = await self._post(url, payload) if result is not None: - _LOGGER.debug(f"Door {serial_no} unlocked successfully") + _LOGGER.debug("Door %s unlocked successfully", serial_no) return True else: - _LOGGER.error(f"Failed to unlock door {serial_no}") + _LOGGER.error("Failed to unlock door %s", serial_no) return False async def turn_on_smartplug(self, plug_id): @@ -224,10 +246,10 @@ async def turn_on_smartplug(self, plug_id): } result = await self._post(url, payload) if result is not None: - _LOGGER.debug(f"Smart plug {plug_id} turned on successfully") + _LOGGER.debug("Smart plug %s turned on successfully", plug_id) return True else: - _LOGGER.error(f"Failed to turn on smart plug {plug_id}") + _LOGGER.error("Failed to turn on smart plug %s", plug_id) return False async def turn_off_smartplug(self, plug_id): @@ -239,10 +261,10 @@ async def turn_off_smartplug(self, plug_id): } result = await self._post(url, payload) if result is not None: - _LOGGER.debug(f"Smart plug {plug_id} turned off successfully") + _LOGGER.debug("Smart plug %s turned off successfully", plug_id) return True else: - _LOGGER.error(f"Failed to turn off smart plug {plug_id}") + _LOGGER.error("Failed to turn off smart plug %s", plug_id) return False async def get_camera_image(self, serial_no): @@ -254,21 +276,12 @@ async def get_camera_image(self, serial_no): } response = await self._post(url, payload) if response and response.get("ImageData"): - import base64 image_data = base64.b64decode(response["ImageData"]) return image_data - else: - _LOGGER.error(f"Failed to retrieve image for camera {serial_no}") - return None + _LOGGER.error("Failed to retrieve image for camera %s", serial_no) + return None async def logout(self): """Logout from the API.""" logout_url = f"{self.API_URL}/api/Login/Logout" await self._post(logout_url, {}) - await self.close() - - async def close(self): - """Close the aiohttp session.""" - if self.session: - await self.session.close() - self.session = None diff --git a/custom_components/sector/config_flow.py b/custom_components/sector/config_flow.py index de909c7..dbd96cc 100644 --- a/custom_components/sector/config_flow.py +++ b/custom_components/sector/config_flow.py @@ -1,10 +1,10 @@ """Config flow for Sector Alarm integration.""" + from __future__ import annotations import logging import voluptuous as vol - from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -29,13 +29,12 @@ async def async_step_user(self, user_input=None): panel_code = user_input[CONF_PANEL_CODE] # Import SectorAlarmAPI here to avoid blocking calls during module import - from .client import SectorAlarmAPI, AuthenticationError + from .client import AuthenticationError, SectorAlarmAPI - api = SectorAlarmAPI(email, password, panel_id, panel_code) + api = SectorAlarmAPI(self.hass, email, password, panel_id, panel_code) try: await api.login() await api.retrieve_all_data() - await api.close() return self.async_create_entry( title="Sector Alarm", data={ @@ -50,8 +49,6 @@ async def async_step_user(self, user_input=None): except Exception as e: errors["base"] = "unknown_error" _LOGGER.exception("Unexpected exception during authentication: %s", e) - finally: - await api.close() data_schema = vol.Schema( { diff --git a/custom_components/sector/const.py b/custom_components/sector/const.py index d135bfd..a0de532 100644 --- a/custom_components/sector/const.py +++ b/custom_components/sector/const.py @@ -1,7 +1,16 @@ """Constants for the Sector Alarm integration.""" +from homeassistant.const import Platform + DOMAIN = "sector" -PLATFORMS = ["alarm_control_panel", "binary_sensor", "lock", "sensor", "switch", "camera"] +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.LOCK, + Platform.SENSOR, + Platform.SWITCH, +] CATEGORY_MODEL_MAPPING = { "1": "Door/Window Sensor", @@ -20,9 +29,5 @@ "keypad": "Keypad", } -CONF_EMAIL = "email" -CONF_PASSWORD = "password" CONF_PANEL_ID = "panel_id" CONF_PANEL_CODE = "panel_code" - -LOGGER = "sector_alarm" diff --git a/custom_components/sector/coordinator.py b/custom_components/sector/coordinator.py index 49eeb18..074cf34 100644 --- a/custom_components/sector/coordinator.py +++ b/custom_components/sector/coordinator.py @@ -1,34 +1,36 @@ """Sector Alarm coordinator.""" + from __future__ import annotations -from datetime import timedelta import logging +from datetime import timedelta from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .client import SectorAlarmAPI, AuthenticationError +from .client import AuthenticationError, SectorAlarmAPI from .const import ( + CATEGORY_MODEL_MAPPING, CONF_PANEL_CODE, CONF_PANEL_ID, DOMAIN, - CONF_EMAIL, - CONF_PASSWORD, - CATEGORY_MODEL_MAPPING, ) +type SectorAlarmConfigEntry = ConfigEntry[SectorDataUpdateCoordinator] + _LOGGER = logging.getLogger(__name__) class SectorDataUpdateCoordinator(DataUpdateCoordinator): """Coordinator to manage data fetching from Sector Alarm.""" - def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistant, entry: SectorAlarmConfigEntry) -> None: """Initialize the coordinator.""" self.hass = hass - self.entry = entry self.api = SectorAlarmAPI( + hass, email=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], panel_id=entry.data[CONF_PANEL_ID], @@ -64,82 +66,148 @@ async def _async_update_data(self): "sensors": {}, "model": "Smart Lock", } - devices[serial_no]["sensors"]["lock_status"] = lock.get("Status") - devices[serial_no]["sensors"]["low_battery"] = lock.get("BatteryLow") + devices[serial_no]["sensors"]["lock_status"] = lock.get( + "Status" + ) + devices[serial_no]["sensors"]["low_battery"] = lock.get( + "BatteryLow" + ) else: - _LOGGER.warning(f"Lock missing Serial: {lock}") + _LOGGER.warning("Lock missing Serial: %s", lock) else: _LOGGER.debug("No locks data found.") # Process devices from different categories for category_name, category_data in data.items(): - _LOGGER.debug(f"Processing category: {category_name}") + _LOGGER.debug("Processing category: %s", category_name) model_name = CATEGORY_MODEL_MAPPING.get(category_name, category_name) - if category_name in ["Doors and Windows", "Smoke Detectors", "Leakage Detectors", "Cameras", "Keypad"]: + if category_name in [ + "Doors and Windows", + "Smoke Detectors", + "Leakage Detectors", + "Cameras", + "Keypad", + ]: for section in category_data.get("Sections", []): for place in section.get("Places", []): for component in place.get("Components", []): - serial_no = str(component.get("SerialNo") or component.get("Serial")) + serial_no = str( + component.get("SerialNo") or component.get("Serial") + ) device_type = component.get("Type", "") device_type_lower = str(device_type).lower() if device_type_lower in CATEGORY_MODEL_MAPPING: model = CATEGORY_MODEL_MAPPING[device_type_lower] else: - _LOGGER.debug(f"Unknown device_type '{device_type}' for serial '{serial_no}', falling back to category model '{model_name}'") + _LOGGER.debug( + "Unknown device_type '%s' for serial '%s', falling back to category model '%s'", + device_type, + serial_no, + model_name, + ) model = model_name # Use category model as fallback if serial_no: if serial_no not in devices: devices[serial_no] = { - "name": component.get("Label") or component.get("Name"), + "name": component.get("Label") + or component.get("Name"), "serial_no": serial_no, "sensors": {}, "model": model, "type": device_type, } - _LOGGER.debug(f"Processed device {serial_no} with type '{device_type}' and model '{model}'") + _LOGGER.debug( + "Processed device %s with type '%s' and model '%s'", + serial_no, + device_type, + model, + ) # Add sensors based on component data if "Closed" in component: - devices[serial_no]["sensors"]["closed"] = component["Closed"] - low_battery_value = component.get("LowBattery", component.get("BatteryLow")) + devices[serial_no]["sensors"]["closed"] = ( + component["Closed"] + ) + low_battery_value = component.get( + "LowBattery", component.get("BatteryLow") + ) if low_battery_value is not None: - devices[serial_no]["sensors"]["low_battery"] = low_battery_value - _LOGGER.debug(f"Assigned low_battery sensor for device {serial_no} with value {low_battery_value}") + devices[serial_no]["sensors"]["low_battery"] = ( + low_battery_value + ) + _LOGGER.debug( + "Assigned low_battery sensor for device %s with value %s", + serial_no, + low_battery_value, + ) else: - _LOGGER.warning(f"No LowBattery or BatteryLow found for device {serial_no} of type '{device_type}'") - if "Humidity" in component and component["Humidity"]: - devices[serial_no]["sensors"]["humidity"] = float(component["Humidity"]) - if "Temperature" in component and component["Temperature"]: - devices[serial_no]["sensors"]["temperature"] = float(component["Temperature"]) + _LOGGER.warning( + "No LowBattery or BatteryLow found for device %s of type '%s'", + serial_no, + device_type, + ) + if ( + "Humidity" in component + and component["Humidity"] + ): + devices[serial_no]["sensors"]["humidity"] = ( + float(component["Humidity"]) + ) + if ( + "Temperature" in component + and component["Temperature"] + ): + devices[serial_no]["sensors"]["temperature"] = ( + float(component["Temperature"]) + ) if "LeakDetected" in component: - devices[serial_no]["sensors"]["leak_detected"] = component["LeakDetected"] + devices[serial_no]["sensors"][ + "leak_detected" + ] = component["LeakDetected"] if "Alarm" in component: - devices[serial_no]["sensors"]["alarm"] = component["Alarm"] + devices[serial_no]["sensors"]["alarm"] = ( + component["Alarm"] + ) else: - _LOGGER.warning(f"Component missing SerialNo: {component}") + _LOGGER.warning( + "Component missing SerialNo: %s", component + ) elif category_name == "Temperatures": - _LOGGER.debug(f"Temperatures data received: {category_data}") + _LOGGER.debug("Temperatures data received: %s", category_data) if isinstance(category_data, dict) and "Sections" in category_data: for section in category_data["Sections"]: for place in section.get("Places", []): for component in place.get("Components", []): - serial_no = str(component.get("SerialNo") or component.get("Serial")) + serial_no = str( + component.get("SerialNo") + or component.get("Serial") + ) device_type = component.get("Type", "") device_type_lower = str(device_type).lower() if device_type_lower in CATEGORY_MODEL_MAPPING: - model = CATEGORY_MODEL_MAPPING[device_type_lower] + model = CATEGORY_MODEL_MAPPING[ + device_type_lower + ] else: - _LOGGER.debug(f"Unknown device_type '{device_type}' for serial '{serial_no}', falling back to category model '{model_name}'") - model = model_name # Use category model as fallback + _LOGGER.debug( + "Unknown device_type '%s' for serial '%s', falling back to category model '%s'", + device_type, + serial_no, + model_name, + ) + model = ( + model_name # Use category model as fallback + ) if serial_no: if serial_no not in devices: devices[serial_no] = { - "name": component.get("Label") or component.get("Name"), + "name": component.get("Label") + or component.get("Name"), "serial_no": serial_no, "sensors": {}, "model": model, @@ -147,65 +215,117 @@ async def _async_update_data(self): } temperature = component.get("Temperature") if temperature is not None: - devices[serial_no]["sensors"]["temperature"] = float(temperature) - _LOGGER.debug(f"Stored temperature {temperature} for device {serial_no}") + devices[serial_no]["sensors"][ + "temperature" + ] = float(temperature) + _LOGGER.debug( + "Stored temperature %s for device %s", + temperature, + serial_no, + ) else: - _LOGGER.debug(f"No temperature value for device {serial_no}") - low_battery_value = component.get("LowBattery", component.get("BatteryLow")) + _LOGGER.debug( + "No temperature value for device %s", + serial_no, + ) + low_battery_value = component.get( + "LowBattery", component.get("BatteryLow") + ) if low_battery_value is not None: - devices[serial_no]["sensors"]["low_battery"] = low_battery_value - _LOGGER.debug(f"Assigned low_battery sensor for device {serial_no} with value {low_battery_value}") + devices[serial_no]["sensors"][ + "low_battery" + ] = low_battery_value + _LOGGER.debug( + "Assigned low_battery sensor for device %s with value %s", + serial_no, + low_battery_value, + ) else: - _LOGGER.warning(f"No LowBattery or BatteryLow found for device {serial_no} of type '{device_type}'") + _LOGGER.warning( + "No LowBattery or BatteryLow found for device %s of type '%s'", + serial_no, + device_type, + ) else: - _LOGGER.warning(f"Component missing SerialNo: {component}") + _LOGGER.warning( + "Component missing SerialNo: %s", component + ) else: - _LOGGER.error(f"Unexpected data format for Temperatures: {category_data}") + _LOGGER.error( + "Unexpected data format for Temperatures: %s", category_data + ) elif category_name == "Humidity": - _LOGGER.debug(f"Humidity data received: {category_data}") + _LOGGER.debug("Humidity data received: %s", category_data) if isinstance(category_data, dict) and "Sections" in category_data: for section in category_data["Sections"]: for place in section.get("Places", []): for component in place.get("Components", []): - serial_no = str(component.get("SerialNo") or component.get("Serial")) + serial_no = str( + component.get("SerialNo") + or component.get("Serial") + ) device_type = component.get("Type", "") device_type_lower = str(device_type).lower() if device_type_lower in CATEGORY_MODEL_MAPPING: - model = CATEGORY_MODEL_MAPPING[device_type_lower] + model = CATEGORY_MODEL_MAPPING[ + device_type_lower + ] else: - _LOGGER.debug(f"Unknown device_type '{device_type}' for serial '{serial_no}', falling back to category model '{model_name}'") - model = model_name # Use category model as fallback + _LOGGER.debug( + "Unknown device_type '%s' for serial '%s', falling back to category model '%s'", + device_type, + serial_no, + model_name, + ) + model = ( + model_name # Use category model as fallback + ) if serial_no: if serial_no not in devices: devices[serial_no] = { - "name": component.get("Label") or component.get("Name"), + "name": component.get("Label") + or component.get("Name"), "serial_no": serial_no, "sensors": {}, "model": model, "type": device_type, } - _LOGGER.debug(f"Registering device {serial_no} with model: {model_name}") + _LOGGER.debug( + "Registering device %s with model: %s", + serial_no, + model_name, + ) humidity = component.get("Humidity") if humidity is not None: - devices[serial_no]["sensors"]["humidity"] = float(humidity) + devices[serial_no]["sensors"][ + "humidity" + ] = float(humidity) else: - _LOGGER.debug(f"No humidity value for device {serial_no}") + _LOGGER.debug( + "No humidity value for device %s", + serial_no, + ) else: - _LOGGER.warning(f"Component missing SerialNo: {component}") + _LOGGER.warning( + "Component missing SerialNo: %s", component + ) else: - _LOGGER.error(f"Unexpected data format for Humidity: {category_data}") - + _LOGGER.error( + "Unexpected data format for Humidity: %s", category_data + ) elif category_name == "Smartplug Status": - _LOGGER.debug(f"Smartplug data received: {category_data}") + _LOGGER.debug("Smartplug data received: %s", category_data) if isinstance(category_data, list): devices["smartplugs"] = category_data else: - _LOGGER.warning(f"Unexpected smartplug data format: {category_data}") + _LOGGER.warning( + "Unexpected smartplug data format: %s", category_data + ) elif category_name == "Lock Status": # Locks data is already retrieved in locks_data @@ -216,7 +336,7 @@ async def _async_update_data(self): pass else: - _LOGGER.debug(f"Unhandled category {category_name}") + _LOGGER.debug("Unhandled category %s", category_data) return { "devices": devices, diff --git a/custom_components/sector/diagnostics.py b/custom_components/sector/diagnostics.py index d676eb5..be390c9 100644 --- a/custom_components/sector/diagnostics.py +++ b/custom_components/sector/diagnostics.py @@ -1,22 +1,19 @@ """Diagnostics support for Sector Alarm integration.""" + from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import SectorDataUpdateCoordinator +from .coordinator import SectorAlarmConfigEntry async def async_get_config_entry_diagnostics( - hass: HomeAssistant, entry: ConfigEntry + hass: HomeAssistant, entry: SectorAlarmConfigEntry ): """Return diagnostics for a config entry.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data - diagnostics_data = { + return { "data": coordinator.data, "entry": entry.as_dict(), } - - return diagnostics_data diff --git a/custom_components/sector/endpoints.py b/custom_components/sector/endpoints.py index af1bf81..b813f80 100644 --- a/custom_components/sector/endpoints.py +++ b/custom_components/sector/endpoints.py @@ -15,8 +15,14 @@ def get_data_endpoints(panel_id): "Persons": ("GET", f"{API_URL}/api/persons/panels/{panel_id}"), "Temperatures": ("POST", f"{API_URL}/api/v2/housecheck/temperatures"), # Panel endpoints - "Panel Status": ("GET", f"{API_URL}/api/panel/GetPanelStatus?panelId={panel_id}"), - "Smartplug Status": ("GET", f"{API_URL}/api/panel/GetSmartplugStatus?panelId={panel_id}"), + "Panel Status": ( + "GET", + f"{API_URL}/api/panel/GetPanelStatus?panelId={panel_id}", + ), + "Smartplug Status": ( + "GET", + f"{API_URL}/api/panel/GetSmartplugStatus?panelId={panel_id}", + ), "Lock Status": ("GET", f"{API_URL}/api/panel/GetLockStatus?panelId={panel_id}"), "Logs": ("GET", f"{API_URL}/api/panel/GetLogs?panelId={panel_id}"), } diff --git a/custom_components/sector/lock.py b/custom_components/sector/lock.py index e3c5837..bf9261b 100644 --- a/custom_components/sector/lock.py +++ b/custom_components/sector/lock.py @@ -1,21 +1,26 @@ -# lock.py +"""Locks for Sector Alarm.""" + +import logging from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import SectorDataUpdateCoordinator - -import logging +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, +): """Set up Sector Alarm locks.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data devices = coordinator.data.get("devices", {}) entities = [] @@ -28,6 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e else: _LOGGER.debug("No lock entities to add.") + class SectorAlarmLock(CoordinatorEntity, LockEntity): """Representation of a Sector Alarm lock.""" @@ -38,7 +44,7 @@ def __init__(self, coordinator: SectorDataUpdateCoordinator, device_info: dict): self._device_info = device_info self._attr_unique_id = f"{self._serial_no}_lock" self._attr_name = device_info["name"] - _LOGGER.debug(f"Initialized lock with unique_id: {self._attr_unique_id}") + _LOGGER.debug("Initialized lock with unique_id: %s", self._attr_unique_id) @property def is_locked(self): diff --git a/custom_components/sector/manifest.json b/custom_components/sector/manifest.json index 8b5fb90..66f6aa2 100644 --- a/custom_components/sector/manifest.json +++ b/custom_components/sector/manifest.json @@ -1,15 +1,11 @@ { "domain": "sector", "name": "Sector Alarm", + "codeowners": ["@gjohansson-ST","@garnser"], "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/sector_alarm", - "requirements": [], - "dependencies": [], - "codeowners": ["@garnser"], + "documentation": "https://github.com/gjohansson-ST/sector/blob/master/readme.md", + "integration_type": "hub", "iot_class": "cloud_polling", - "version": "1.0.0", - "resources": { - "icon": "/custom_components/sector_alarm/icons/icon.png", - "logo": "/custom_components/sector_alarm/images/logo.png" - } + "issue_tracker": "https://github.com/gjohansson-ST/sector/issues", + "version": "1.0.0" } diff --git a/custom_components/sector/sensor.py b/custom_components/sector/sensor.py index fb9eaed..e3bac0e 100644 --- a/custom_components/sector/sensor.py +++ b/custom_components/sector/sensor.py @@ -1,4 +1,5 @@ """Sensor platform for Sector Alarm integration.""" + from __future__ import annotations import logging @@ -8,23 +9,25 @@ SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, CATEGORY_MODEL_MAPPING -from .coordinator import SectorDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up Sector Alarm sensors.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data devices = coordinator.data.get("devices", {}) entities = [] @@ -34,10 +37,19 @@ async def async_setup_entry( device_type = device.get("type", "") device_model = device.get("model", "") - _LOGGER.debug(f"Adding device {serial_no} as model '{device_model}' with type '{device_type}'") + _LOGGER.debug( + "Adding device %s as model '%s' with type '%s'", + serial_no, + device_model, + device_type, + ) if "temperature" in sensors: - _LOGGER.debug("Adding temperature sensor for device %s with sensors: %s", serial_no, sensors) + _LOGGER.debug( + "Adding temperature sensor for device %s with sensors: %s", + serial_no, + sensors, + ) entities.append( SectorAlarmSensor( coordinator, @@ -49,11 +61,15 @@ async def async_setup_entry( device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, ), - model=device_model + model=device_model, ) ) if "humidity" in sensors: - _LOGGER.debug("Adding humidity sensor for device %s with sensors: %s", serial_no, sensors) + _LOGGER.debug( + "Adding humidity sensor for device %s with sensors: %s", + serial_no, + sensors, + ) entities.append( SectorAlarmSensor( coordinator, @@ -65,7 +81,7 @@ async def async_setup_entry( device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, ), - model=device_model + model=device_model, ) ) @@ -96,7 +112,7 @@ def __init__( self._model = model self._attr_unique_id = f"{serial_no}_{sensor_type}" self._attr_name = f"{device_info['name']} {sensor_type.capitalize()}" - _LOGGER.debug(f"Initialized sensor with unique_id: {self._attr_unique_id}") + _LOGGER.debug("Initialized sensor with unique_id: %s", self._attr_unique_id) @property def native_value(self): diff --git a/custom_components/sector/strings.json b/custom_components/sector/strings.json deleted file mode 100644 index 4ed1488..0000000 --- a/custom_components/sector/strings.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "error": { - "auth_error": "[%key:common::config_flow::error::invalid_auth%]", - "connection_error": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "code_format": "Code length for alarm and locks", - "temp": "Enable Temp sensors" - } - }, - "reauth_confirm": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "code_format": "Code length for alarm and locks" - } - } - } - } -} diff --git a/custom_components/sector/switch.py b/custom_components/sector/switch.py index 872e56d..70a3ad6 100644 --- a/custom_components/sector/switch.py +++ b/custom_components/sector/switch.py @@ -1,4 +1,5 @@ """Switch platform for Sector Alarm integration.""" + from __future__ import annotations import logging @@ -7,22 +8,24 @@ SwitchDeviceClass, SwitchEntity, ) -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import SectorDataUpdateCoordinator +from .coordinator import SectorAlarmConfigEntry, SectorDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, + entry: SectorAlarmConfigEntry, + async_add_entities: AddEntitiesCallback, ): """Set up Sector Alarm switches.""" - coordinator: SectorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator = entry.runtime_data devices = coordinator.data.get("devices", {}) entities = [] @@ -52,7 +55,7 @@ def __init__( self._serial_no = str(plug_data.get("SerialNo") or plug_data.get("Serial")) self._attr_unique_id = f"{self._serial_no}_switch" self._attr_name = plug_data.get("Label", "Sector Smart Plug") - _LOGGER.debug(f"Initialized switch with unique_id: {self._attr_unique_id}") + _LOGGER.debug("Initialized switch with unique_id: %s", self._attr_unique_id) @property def is_on(self): diff --git a/custom_components/sector/translations/en.json b/custom_components/sector/translations/en.json index 10e0c3e..4693fd9 100644 --- a/custom_components/sector/translations/en.json +++ b/custom_components/sector/translations/en.json @@ -5,31 +5,16 @@ "reauth_successful": "Reauthentication successful" }, "error": { - "auth_error": "Username and/or password is incorrect", - "connection_error": "Failed to connect" + "authentication_failed": "Username and/or password is incorrect", + "unknown_error": "Failed to connect" }, "step": { "user": { "data": { - "username": "E-mail address", + "email": "E-mail address", "password": "Password", - "code_format": "Code length for alarm and locks", - "temp": "Enable Temp sensors" - } - }, - "reauth_confirm": { - "data": { - "username": "E-mail address", - "password": "Password" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "code_format": "Code length for alarm and locks" + "panel_id": "Panel ID", + "panel_code": "Panel code" } } } diff --git a/custom_components/sector/translations/fr.json b/custom_components/sector/translations/fr.json deleted file mode 100644 index d0f0fec..0000000 --- a/custom_components/sector/translations/fr.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Votre compte est déjà configuré", - "reauth_successful": "Nouvelle tentative d'authentification réussie" - }, - "error": { - "auth_error": "Nom d'utilisateur et/ou mot de passe incorrect", - "connection_error": "Erreur lors de la connexion" - }, - "step": { - "user": { - "data": { - "username": "Adresse mail", - "password": "Mot de passe", - "code_format": "Longueur du code pour armer et désarmer l'alarme", - "temp": "Activer la récupération des capteurs de température" - } - }, - "reauth_confirm": { - "data": { - "username": "Adresse mail", - "password": "Mot de passe" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "code_format": "Longueur du code pour armer et désarmer l'alarme" - } - } - } - } -} diff --git a/custom_components/sector/translations/no.json b/custom_components/sector/translations/no.json deleted file mode 100644 index 7f014b6..0000000 --- a/custom_components/sector/translations/no.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering vellykket" - }, - "error": { - "auth_error": "Brukernavn og / eller passord er feil", - "connection_error": "Tilkobling mislyktes" - }, - "step": { - "user": { - "data": { - "username": "Epostadresse", - "password": "Passord", - "code_format": "Kodelengde for alarm og låser", - "temp": "Aktiver Temp-sensorer" - } - }, - "reauth_confirm": { - "data": { - "username": "Epostadresse", - "password": "Passord" - } - } - } - }, - "options": { - "step": { - "init": { - "data": { - "code_format": "Kodelengde for alarm og låser" - } - } - } - } -}