pax_global_header00006660000000000000000000000064147744237400014526gustar00rootroot0000000000000052 comment=2ced88754c319f72a269015086f89d74e3ff1a7a aioairzone-1.0.0/000077500000000000000000000000001477442374000136645ustar00rootroot00000000000000aioairzone-1.0.0/.github/000077500000000000000000000000001477442374000152245ustar00rootroot00000000000000aioairzone-1.0.0/.github/workflows/000077500000000000000000000000001477442374000172615ustar00rootroot00000000000000aioairzone-1.0.0/.github/workflows/ci.yml000066400000000000000000000067131477442374000204060ustar00rootroot00000000000000name: CI on: - push - pull_request env: DEFAULT_PYTHON: "3.12" jobs: black: name: Check Black runs-on: ubuntu-latest steps: - name: Check out code from GitHub uses: actions/checkout@v4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Upgrade pip run: | python -m pip install --upgrade pip pip --version - name: Install Black run: | pip install black - name: Run Black run: | black --check --diff aioairzone - name: Run Black on examples run: | black --check --diff examples - name: Run Black on sim run: | black --check --diff sim pylint: name: Check Pylint (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12", "3.13"] steps: - name: Check out code from GitHub uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Upgrade pip run: | python -m pip install --upgrade pip pip --version - name: Install Requirements run: | pip install -r requirements.txt - name: Install Pylint run: | pip install pylint - name: Run Pylint run: | pylint aioairzone - name: Install aioairzone run: | pip install --upgrade . - name: Run Pylint on examples run: | pylint examples - name: Run Pylint on sim run: | pylint sim mypy: name: Check Mypy (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: matrix: python-version: ["3.12", "3.13"] steps: - name: Check out code from GitHub uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Upgrade pip run: | python -m pip install --upgrade pip pip --version - name: Install Requirements run: | pip install -r requirements.txt - name: Install Mypy run: | pip install mypy - name: Run Mypy run: | mypy --strict aioairzone - name: Install aioairzone run: | pip install --upgrade . - name: Run Mypy on examples run: | mypy --strict examples - name: Run Mypy on sim run: | mypy --strict sim ruff: name: Check ruff runs-on: ubuntu-latest steps: - name: Check out code from GitHub uses: actions/checkout@v4 - name: Set up Python ${{ env.DEFAULT_PYTHON }} uses: actions/setup-python@v5 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Upgrade pip run: | python -m pip install --upgrade pip pip --version - name: Install ruff run: | pip install ruff - name: Run ruff run: | ruff check aioairzone - name: Run ruff on examples run: | ruff check examples - name: Run ruff on sim run: | ruff check sim aioairzone-1.0.0/.github/workflows/matchers/000077500000000000000000000000001477442374000210675ustar00rootroot00000000000000aioairzone-1.0.0/.github/workflows/matchers/pylint.json000066400000000000000000000011601477442374000232770ustar00rootroot00000000000000{ "problemMatcher": [ { "owner": "pylint-error", "severity": "error", "pattern": [ { "regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", "file": 1, "line": 2, "column": 3, "message": 4, "code": 5 } ] }, { "owner": "pylint-warning", "severity": "warning", "pattern": [ { "regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", "file": 1, "line": 2, "column": 3, "message": 4, "code": 5 } ] } ] } aioairzone-1.0.0/.gitignore000066400000000000000000000000601477442374000156500ustar00rootroot00000000000000*.egg-info *.patch *.swp __pycache__ build dist aioairzone-1.0.0/LICENSE000066400000000000000000000261351477442374000147000ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. aioairzone-1.0.0/MANIFEST.in000066400000000000000000000001271477442374000154220ustar00rootroot00000000000000include LICENSE include README.md include requirements.txt include aioairzone/py.typed aioairzone-1.0.0/README.md000066400000000000000000000035511477442374000151470ustar00rootroot00000000000000# aioairzone [![Latest Version][mdversion-button]][md-pypi] [![Python Versions][pyversion-button]][md-pypi] [![License: Apache 2.0][apache-button]](LICENSE) [apache-button]: https://img.shields.io/badge/License-Apache%202.0-blue.svg [md-pypi]: https://pypi.org/project/aioairzone [mdversion-button]: https://img.shields.io/pypi/v/aioairzone.svg [pyversion-button]: https://img.shields.io/pypi/pyversions/aioairzone.svg Python library to control Airzone devices. ## Requirements - Python >= 3.12 ## Install ```bash pip install aioairzone ``` ## Install from Source Run the following command inside this folder ```bash pip install --upgrade . ``` ## Examples Examples can be found in the `examples` folder ## API Call examples Run the following command to list all your Airzone Zones: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/hvac" -d '{"systemID": 0, "zoneID": 0}' | jq ``` Run the following command to list all your Airzone Systems: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/hvac" -d '{"systemID": 127}' | jq ``` Run the following command to fetch your Airzone Altherma parameters: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/hvac" -d '{"systemID": 0}' | jq ``` Run the following command to fetch your Airzone WebServer parameters: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/webserver" | jq ``` Run the following command to fetch a demo Airzone Zone: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/demo" | jq ``` Run the following command to fetch your Airzone LocalAPI version: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/version" | jq ``` Run the following command to fetch your Airzone LocalAPI integration driver: ``` curl -s --location --request POST "http://192.168.1.25:3000/api/v1/integration" -d '{}' | jq ``` aioairzone-1.0.0/aioairzone/000077500000000000000000000000001477442374000160245ustar00rootroot00000000000000aioairzone-1.0.0/aioairzone/__init__.py000066400000000000000000000000271477442374000201340ustar00rootroot00000000000000"""Airzone library.""" aioairzone-1.0.0/aioairzone/common.py000066400000000000000000000134171477442374000176740ustar00rootroot00000000000000"""Airzone library common code.""" from __future__ import annotations from enum import IntEnum, StrEnum import json import re from typing import Any class AirzoneStages(IntEnum): """Airzone stages.""" UNKNOWN = -1 Off = 0 Air = 1 Radiant = 2 Combined = 3 @classmethod def _missing_(cls, value: Any) -> AirzoneStages: return cls.UNKNOWN def exists(self) -> bool: """Return if Airzone Stage exits.""" return self.value != self.Off def to_list(self) -> list[AirzoneStages]: """Convert AirzoneStages value to list.""" if self.value == self.Combined: return [ AirzoneStages.Off, AirzoneStages.Air, AirzoneStages.Radiant, AirzoneStages.Combined, ] if self.value in (self.Air, self.Radiant): return [AirzoneStages.Off, self] return [] class EcoAdapt(StrEnum): """Airzone Eco-Adapt.""" OFF = "off" MANUAL = "manual" A = "a" A_PLUS = "a_p" A_PLUS_PLUS = "a_pp" @classmethod def _missing_(cls, value: Any) -> EcoAdapt: return cls.OFF class GrilleAngle(IntEnum): """Airzone grille angles.""" DEG_90 = 0 DEG_50 = 1 DEG_45 = 2 DEG_40 = 3 @classmethod def _missing_(cls, value: Any) -> GrilleAngle: return cls.DEG_90 class OperationAction(IntEnum): """Airzone operation actions.""" COOLING = 1 DRYING = 2 FAN = 3 HEATING = 4 IDLE = 5 OFF = 6 class OperationMode(IntEnum): """Airzone operation modes.""" UNKNOWN = -1 STOP = 1 COOLING = 2 HEATING = 3 FAN = 4 DRY = 5 AUX_HEATING = 6 AUTO = 7 @classmethod def _missing_(cls, value: Any) -> OperationMode: return cls.UNKNOWN class HotWaterOperation(IntEnum): """Airzone Hot Water operations.""" UNKNOWN = -1 Off = 0 On = 1 Powerful = 2 @classmethod def _missing_(cls, value: Any) -> HotWaterOperation: return cls.UNKNOWN class SleepTimeout(IntEnum): """Airzone sleep timeouts.""" SLEEP_OFF = 0 SLEEP_30 = 30 SLEEP_60 = 60 SLEEP_90 = 90 @classmethod def _missing_(cls, value: Any) -> SleepTimeout: return cls.SLEEP_OFF class SystemType(IntEnum): """Airzone System Types.""" UNKNOWN = -1 C6 = 1 AQUAGLASS = 2 DZK = 3 RADIANT = 4 C3 = 5 ZBS = 6 ZS6 = 7 @classmethod def _missing_(cls, value: Any) -> SystemType: return cls.UNKNOWN def __str__(self) -> str: """Convert ThermostatType value to string.""" models: dict[int, str] = { self.UNKNOWN: "Unknown", self.C6: "C6", self.AQUAGLASS: "AQUAGLASS", self.DZK: "DZK", self.RADIANT: "Radiant", self.C3: "C3", self.ZBS: "ZBS", self.ZS6: "ZS6", } return models[self.value] class TemperatureUnit(IntEnum): """Airzone temperature units.""" CELSIUS = 0 FAHRENHEIT = 1 class ThermostatType(IntEnum): """Airzone Thermostat Types.""" UNKNOWN = -1 Blueface = 1 BluefaceZero = 2 Lite = 3 Think = 4 @classmethod def _missing_(cls, value: Any) -> ThermostatType: return cls.UNKNOWN def __str__(self) -> str: """Convert ThermostatType value to string.""" models: dict[int, str] = { self.UNKNOWN: "Unknown", self.Blueface: "Blueface", self.BluefaceZero: "Blueface Zero", self.Lite: "Lite", self.Think: "Think", } return models[self.value] def exists_radio(self) -> bool: """Return if a radio version of the Thermostat exists.""" models: dict[int, bool] = { self.UNKNOWN: False, self.Blueface: False, self.BluefaceZero: False, self.Lite: True, self.Think: True, } return models[self.value] class WebServerInterface(IntEnum): """Airzone WebServer Interface type.""" UNKNOWN = -1 ETHERNET = 1 WIFI = 2 @classmethod def _missing_(cls, value: Any) -> WebServerInterface: return cls.UNKNOWN class WebServerType(IntEnum): """Airzone WebServer Types.""" UNKNOWN = -1 AIRZONE = 1 AIDOO = 2 @classmethod def _missing_(cls, value: Any) -> WebServerType: return cls.UNKNOWN def __str__(self) -> str: """Convert WebServerType value to string.""" models: dict[int, str] = { self.UNKNOWN: "Unknown", self.AIRZONE: "Airzone WebServer", self.AIDOO: "Aidoo WebServer", } return models[self.value] def get_system_zone_id(system_id: int, zone_id: int) -> str: """Combine system and zone IDs.""" return f"{system_id}:{zone_id}" def json_dumps(data: Any) -> Any: """Convert data to JSON.""" if data is not None: return json.dumps(data) return None def parse_bool(data: Any) -> bool | None: """Convert data to bool.""" if data is not None: return bool(data) return None def parse_float(data: Any) -> float | None: """Convert data to float.""" if data is not None: return float(data) return None def parse_int(data: Any) -> int | None: """Convert data to int.""" if data is not None: return int(data) return None def parse_str(data: Any) -> str | None: """Convert data to string.""" if data is not None: return str(data) return None def validate_mac_address(mac_addr: str | None) -> bool: """Validate MAC address.""" if mac_addr is None: return False return ( re.match("[0-9a-f]{2}([-:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", mac_addr.lower()) is not None ) aioairzone-1.0.0/aioairzone/const.py000066400000000000000000000204701477442374000175270ustar00rootroot00000000000000"""Airzone library constants.""" from typing import Final from packaging.version import Version API_ACS_MAX_TEMP: Final[str] = "acs_maxtemp" API_ACS_MIN_TEMP: Final[str] = "acs_mintemp" API_ACS_ON: Final[str] = "acs_power" API_ACS_POWER_MODE: Final[str] = "acs_powerful" API_ACS_SET_POINT: Final[str] = "acs_setpoint" API_ACS_TEMP: Final[str] = "acs_temp" API_AIR_DEMAND: Final[str] = "air_demand" API_ANTI_FREEZE: Final[str] = "antifreeze" API_BATTERY: Final[str] = "battery" API_COLD_ANGLE: Final[str] = "coldangle" API_COLD_DEMAND: Final[str] = "cold_demand" API_COLD_STAGE: Final[str] = "coldStage" API_COLD_STAGES: Final[str] = "coldStages" API_COOL_MAX_TEMP: Final[str] = "coolmaxtemp" API_COOL_MIN_TEMP: Final[str] = "coolmintemp" API_COOL_SET_POINT: Final[str] = "coolsetpoint" API_COVERAGE: Final[str] = "coverage" API_DATA: Final[str] = "data" API_DEMO: Final[str] = "demo" API_DRIVER: Final[str] = "driver" API_DOUBLE_SET_POINT: Final[str] = "double_sp" API_ECO_ADAPT: Final[str] = "eco_adapt" API_ERROR: Final[str] = "error" API_ERRORS: Final[str] = "errors" API_FLOOR_DEMAND: Final[str] = "floor_demand" API_HEAT_ANGLE: Final[str] = "heatangle" API_HEAT_DEMAND: Final[str] = "heat_demand" API_HEAT_MAX_TEMP: Final[str] = "heatmaxtemp" API_HEAT_MIN_TEMP: Final[str] = "heatmintemp" API_HEAT_SET_POINT: Final[str] = "heatsetpoint" API_HEAT_STAGE: Final[str] = "heatStage" API_HEAT_STAGES: Final[str] = "heatStages" API_HUMIDITY: Final[str] = "humidity" API_HVAC: Final[str] = "hvac" API_INTEGRATION: Final[str] = "integration" API_INTERFACE: Final[str] = "interface" API_MAC: Final[str] = "mac" API_MANUFACTURER: Final[str] = "manufacturer" API_MASTER_ZONE_ID: Final[str] = "master_zoneID" API_MAX_TEMP: Final[str] = "maxTemp" API_MC_CONNECTED: Final[str] = "mc_connected" API_MIN_TEMP: Final[str] = "minTemp" API_MODE: Final[str] = "mode" API_MODES: Final[str] = "modes" API_NAME: Final[str] = "name" API_ON: Final[str] = "on" API_POWER: Final[str] = "power" API_ROOM_TEMP: Final[str] = "roomTemp" API_SET_POINT: Final[str] = "setpoint" API_SLEEP: Final[str] = "sleep" API_SPEED: Final[str] = "speed" API_SPEEDS: Final[str] = "speeds" API_SYSTEM_FIRMWARE: Final[str] = "system_firmware" API_SYSTEM_ID: Final[str] = "systemID" API_SYSTEM_TYPE: Final[str] = "system_type" API_SYSTEMS: Final[str] = "systems" API_TEMP_STEP: Final[str] = "temp_step" API_THERMOS_FIRMWARE: Final[str] = "thermos_firmware" API_THERMOS_RADIO: Final[str] = "thermos_radio" API_THERMOS_TYPE: Final[str] = "thermos_type" API_UNITS: Final[str] = "units" API_V1: Final[str] = "api/v1" API_VERSION: Final[str] = "version" API_WEBSERVER: Final[str] = "webserver" API_WIFI: Final[str] = "wifi" API_WIFI_CHANNEL: Final[str] = "wifi_channel" API_WIFI_QUALITY: Final[str] = "wifi_quality" API_WIFI_RSSI: Final[str] = "wifi_rssi" API_WS_AIDOO: Final[str] = "ws_aidoo" API_WS_AZ: Final[str] = "ws_az" API_WS_FIRMWARE: Final[str] = "ws_firmware" API_WS_TYPE: Final[str] = "ws_type" API_ZONE_ID: Final[str] = "zoneID" API_ERROR_HOT_WATER_NOT_CONNECTED: Final[str] = "acs not connected" API_ERROR_IAQ_SENSOR_ID_NOT_AVAILABLE: Final[str] = "iaqsensorid not available" API_ERROR_LOW_BATTERY: Final[str] = "Low battery" API_ERROR_METHOD_NOT_SUPPORTED: Final[str] = "Method not provided or not supported" API_ERROR_REQUEST_MALFORMED: Final[str] = "request malformed" API_ERROR_SYSTEM_ID_NOT_AVAILABLE: Final[str] = "systemid not avaiable" API_ERROR_SYSTEM_ID_NOT_PROVIDED: Final[str] = "systemid not provided" API_ERROR_SYSTEM_ID_OUT_RANGE: Final[str] = "systemid out of range" API_ERROR_ZONE_ID_NOT_AVAILABLE: Final[str] = "zoneid not avaiable" API_ERROR_ZONE_ID_NOT_PROVIDED: Final[str] = "zoneid not provided" API_ERROR_ZONE_ID_OUT_RANGE: Final[str] = "zoneid out of range" API_DHW_PARAMS: Final[list[str]] = [ API_ACS_ON, API_ACS_POWER_MODE, API_ACS_SET_POINT, ] API_DOUBLE_SET_POINT_PARAMS: Final[set[str]] = { API_COOL_MAX_TEMP, API_COOL_MIN_TEMP, API_COOL_SET_POINT, API_HEAT_MAX_TEMP, API_HEAT_MIN_TEMP, API_HEAT_SET_POINT, } API_NO_FEEDBACK_PARAMS: Final[list[str]] = [ API_MODE, ] API_SYSTEM_PARAMS: Final[list[str]] = [ API_MODE, API_SPEED, ] API_ZONE_PARAMS: Final[list[str]] = [ API_COOL_SET_POINT, API_COLD_ANGLE, API_COLD_STAGE, API_HEAT_ANGLE, API_HEAT_SET_POINT, API_HEAT_STAGE, API_NAME, API_ON, API_SET_POINT, API_SLEEP, ] AZD_ABS_TEMP_MAX: Final[str] = "absolute-temp-max" AZD_ABS_TEMP_MIN: Final[str] = "absolute-temp-min" AZD_ACTION: Final[str] = "action" AZD_AIR_DEMAND: Final[str] = "air-demand" AZD_ANTI_FREEZE: Final[str] = "anti-freeze" AZD_AVAILABLE: Final[str] = "available" AZD_BATTERY_LOW: Final[str] = "battery-low" AZD_CLAMP_METER: Final[str] = "clamp-meter" AZD_COLD_ANGLE: Final[str] = "cold-angle" AZD_COLD_DEMAND: Final[str] = "cold-demand" AZD_COLD_STAGE: Final[str] = "cold-stage" AZD_COLD_STAGES: Final[str] = "cold-stages" AZD_COOL_TEMP_MAX: Final[str] = "cool-temp-max" AZD_COOL_TEMP_MIN: Final[str] = "cool-temp-min" AZD_COOL_TEMP_SET: Final[str] = "cool-temp-set" AZD_DEMAND: Final[str] = "demand" AZD_DOUBLE_SET_POINT: Final[str] = "double-set-point" AZD_ECO_ADAPT: Final[str] = "eco-adapt" AZD_ENERGY: Final[str] = "energy" AZD_ERRORS: Final[str] = "errors" AZD_FIRMWARE: Final[str] = "firmware" AZD_FULL_NAME: Final[str] = "full-name" AZD_FLOOR_DEMAND: Final[str] = "floor-demand" AZD_HEAT_ANGLE: Final[str] = "heat-angle" AZD_HEAT_DEMAND: Final[str] = "heat-demand" AZD_HEAT_TEMP_MAX: Final[str] = "heat-temp-max" AZD_HEAT_TEMP_MIN: Final[str] = "heat-temp-min" AZD_HEAT_TEMP_SET: Final[str] = "heat-temp-set" AZD_HEAT_STAGE: Final[str] = "heat-stage" AZD_HEAT_STAGES: Final[str] = "heat-stages" AZD_HOT_WATER: Final[str] = "hot-water" AZD_HUMIDITY: Final[str] = "humidity" AZD_ID: Final[str] = "id" AZD_INTERFACE: Final[str] = "interface" AZD_MAC: Final[str] = "mac" AZD_MANUFACTURER: Final[str] = "manufacturer" AZD_MASTER: Final[str] = "master" AZD_MASTER_SYSTEM_ZONE: Final[str] = "master-system-zone" AZD_MASTER_ZONE: Final[str] = "master-zone" AZD_MODE: Final[str] = "mode" AZD_MODEL: Final[str] = "model" AZD_MODES: Final[str] = "modes" AZD_NAME: Final[str] = "name" AZD_OPERATION: Final[str] = "operation" AZD_OPERATIONS: Final[str] = "operations" AZD_ON: Final[str] = "on" AZD_POWER_MODE: Final[str] = "power-mode" AZD_PROBLEMS: Final[str] = "problems" AZD_SLEEP: Final[str] = "sleep" AZD_SPEED: Final[str] = "speed" AZD_SPEEDS: Final[str] = "speeds" AZD_SYSTEM: Final[str] = "system" AZD_SYSTEMS: Final[str] = "systems" AZD_SYSTEMS_NUM: Final[str] = "num-systems" AZD_TEMP: Final[str] = "temp" AZD_TEMP_MAX: Final[str] = "temp-max" AZD_TEMP_MIN: Final[str] = "temp-min" AZD_TEMP_SET: Final[str] = "temp-set" AZD_TEMP_STEP: Final[str] = "temp-step" AZD_TEMP_UNIT: Final[str] = "temp-unit" AZD_THERMOSTAT_BATTERY: Final[str] = "thermostat-battery" AZD_THERMOSTAT_FW: Final[str] = "thermostat-fw" AZD_THERMOSTAT_MODEL: Final[str] = "thermostat-model" AZD_THERMOSTAT_RADIO: Final[str] = "thermostat-radio" AZD_THERMOSTAT_SIGNAL: Final[str] = "thermostat-signal" AZD_VERSION: Final[str] = "version" AZD_WEBSERVER: Final[str] = "webserver" AZD_WIFI_CHANNEL: Final[str] = "wifi-channel" AZD_WIFI_QUALITY: Final[str] = "wifi-quality" AZD_WIFI_RSSI: Final[str] = "wifi-rssi" AZD_ZONES: Final[str] = "zones" AZD_ZONES_NUM: Final[str] = "num-zones" API_BUG_MIN_TEMP_FAH: Final[int] = 32 API_BUG_MAX_TEMP_FAH: Final[int] = 140 DEFAULT_PORT: Final[int] = 3000 DEFAULT_SYSTEM_ID: Final[int] = 0 DEFAULT_TEMP_MAX_CELSIUS: Final[float] = 30.0 DEFAULT_TEMP_MAX_FAHRENHEIT: Final[float] = 86.0 DEFAULT_TEMP_MIN_CELSIUS: Final[float] = 15.0 DEFAULT_TEMP_MIN_FAHRENHEIT: Final[float] = 60.0 DEFAULT_TEMP_STEP_CELSIUS: Final[float] = 0.5 DEFAULT_TEMP_STEP_FAHRENHEIT: Final[float] = 1.0 ERROR_SYSTEM: Final[str] = "system" ERROR_ZONE: Final[str] = "zone" HTTP_CALL_TIMEOUT: Final[int] = 10 HTTP_MAX_REQUESTS: Final[int] = 1 HTTP_QUIRK_VERSION: Final[Version] = Version("9.99") # Fix version is still unknown RAW_DEMO: Final[str] = "demo" RAW_DHW: Final[str] = "dhw" RAW_HEADERS: Final[str] = "headers" RAW_HVAC: Final[str] = "hvac" RAW_HTTP: Final[str] = "http" RAW_INTEGRATION: Final[str] = "integration" RAW_QUIRKS: Final[str] = "quirks" RAW_REASON: Final[str] = "reason" RAW_STATUS: Final[str] = "status" RAW_SYSTEMS: Final[str] = "systems" RAW_VERSION: Final[str] = "version" RAW_WEBSERVER: Final[str] = "webserver" THERMOSTAT_RADIO: Final[str] = "Radio" THERMOSTAT_WIRED: Final[str] = "Wired" aioairzone-1.0.0/aioairzone/exceptions.py000066400000000000000000000032651477442374000205650ustar00rootroot00000000000000"""Airzone library exceptions.""" from __future__ import annotations class AirzoneError(Exception): """Base class for aioairzone errors.""" class APIError(AirzoneError): """Exception raised when API fails.""" class HotWaterNotAvailable(AirzoneError): """Exception raised when Hot Water is not available.""" class IaqSensorNotAvailable(AirzoneError): """Exception raised when IAQ sensor is not available.""" class InvalidHost(AirzoneError): """Exception raised when invalid host is requested.""" class InvalidMethod(AirzoneError): """Exception raised when invalid method is requested.""" class InvalidParam(AirzoneError): """Exception raised when invalid param is requested.""" class InvalidState(AirzoneError): """Exception raised when InvalidStateError is raised from asyncio.""" class InvalidSystem(AirzoneError): """Exception raised when invalid system is requested.""" class InvalidZone(AirzoneError): """Exception raised when invalid zone is requested.""" class ParamUpdateFailure(AirzoneError): """Exception raised when parameter isn't updated.""" class RequestMalformed(AirzoneError): """Exception raised when API receives a malformed request.""" class SystemOutOfRange(InvalidSystem): """Exception raised when system id is out of range.""" class SystemNotAvailable(InvalidSystem): """Exception raised when system id is not available.""" class ZoneOutOfRange(InvalidZone): """Exception raised when zone id is out of range.""" class ZoneNotAvailable(InvalidZone): """Exception raised when zone id is not available.""" class ZoneNotProvided(AirzoneError): """Exception raised when zone id is not provided.""" aioairzone-1.0.0/aioairzone/hotwater.py000066400000000000000000000101711477442374000202330ustar00rootroot00000000000000"""Airzone Local API Domestic Hot Water.""" from __future__ import annotations from typing import Any from .common import HotWaterOperation, TemperatureUnit, parse_bool, parse_int from .const import ( API_ACS_MAX_TEMP, API_ACS_MIN_TEMP, API_ACS_ON, API_ACS_POWER_MODE, API_ACS_SET_POINT, API_ACS_TEMP, API_UNITS, AZD_NAME, AZD_ON, AZD_OPERATION, AZD_OPERATIONS, AZD_POWER_MODE, AZD_TEMP, AZD_TEMP_MAX, AZD_TEMP_MIN, AZD_TEMP_SET, AZD_TEMP_UNIT, ) class HotWater: """Airzone Domestic Hot Water.""" def __init__(self, data: dict[str, Any]): """Hot Water init.""" self.name: str = "Airzone DHW" self.on: bool self.temp: int self.temp_max: int self.temp_min: int self.temp_set: int self.temp_unit: TemperatureUnit = TemperatureUnit.CELSIUS self.power_mode: bool | None = None self.update_data(data) def update_data(self, data: dict[str, Any]) -> None: """Update Hot Water data.""" self.on = bool(data[API_ACS_ON]) self.temp = int(data[API_ACS_TEMP]) self.temp_max = int(data[API_ACS_MAX_TEMP]) self.temp_min = int(data[API_ACS_MIN_TEMP]) self.temp_set = int(data[API_ACS_SET_POINT]) acs_power_mode = parse_bool(data.get(API_ACS_POWER_MODE)) if acs_power_mode is not None: self.power_mode = acs_power_mode units = parse_int(data.get(API_UNITS)) if units is not None: self.temp_unit = TemperatureUnit(units) def data(self) -> dict[str, Any]: """Return Airzone Hot Water data.""" data: dict[str, Any] = { AZD_NAME: self.get_name(), AZD_ON: self.get_on(), AZD_OPERATION: self.get_operation(), AZD_OPERATIONS: self.get_operations(), AZD_TEMP: self.get_temp(), AZD_TEMP_MAX: self.get_temp_max(), AZD_TEMP_MIN: self.get_temp_min(), AZD_TEMP_SET: self.get_temp_set(), AZD_TEMP_UNIT: self.get_temp_unit(), } power_mode = self.get_power_mode() if power_mode is not None: data[AZD_POWER_MODE] = power_mode return data def get_name(self) -> str | None: """Return Hot Water name.""" return self.name def get_on(self) -> bool: """Return Hot Water on/off.""" return self.on def get_operation(self) -> HotWaterOperation: """Return Hot Water current operation.""" if self.get_on(): if self.get_power_mode(): return HotWaterOperation.Powerful return HotWaterOperation.On return HotWaterOperation.Off def get_operations(self) -> list[HotWaterOperation]: """Return Hot Water operation list.""" operations = [ HotWaterOperation.Off, HotWaterOperation.On, ] if self.get_power_mode() is not None: operations += [HotWaterOperation.Powerful] return operations def get_power_mode(self) -> bool | None: """Return Hot Water power mode on/off.""" if self.power_mode is not None: return self.power_mode return None def get_temp(self) -> int: """Return Hot Water temperature.""" return self.temp def get_temp_max(self) -> int: """Return Hot Water max temperature.""" return self.temp_max def get_temp_min(self) -> int: """Return Hot Water min temperature.""" return self.temp_min def get_temp_set(self) -> int: """Return Hot Water set temperature.""" return self.temp_set def get_temp_unit(self) -> TemperatureUnit: """Return Hot Water temperature unit.""" return self.temp_unit def set_param(self, key: str, value: Any) -> None: """Update Hot Water parameter by key and value.""" if key == API_ACS_ON: self.on = bool(value) elif key == API_ACS_POWER_MODE: if self.power_mode is not None: self.power_mode = bool(value) elif key == API_ACS_SET_POINT: self.temp_set = int(value) aioairzone-1.0.0/aioairzone/http.py000066400000000000000000000214301477442374000173550ustar00rootroot00000000000000"""Airzone Local API HTTP implementation.""" import asyncio from asyncio import Future, Protocol, Transport import json from json import JSONDecodeError import logging from typing import Any, Final from urllib.parse import urlparse from .exceptions import InvalidHost, InvalidState _LOGGER = logging.getLogger(__name__) HTTP_EOL: Final[str] = "\r\n" HTTP_HDR_SEP: Final[str] = f"{HTTP_EOL}{HTTP_EOL}" HTTP_CHARSET: Final[str] = "charset" HTTP_CONTENT_LEN: Final[str] = "Content-Length" HTTP_CONTENT_TYPE: Final[str] = "Content-Type" HTTP_SERVER: Final[str] = "Server" HTTP_BUFFER: Final[int] = 4096 HTTP_DEF_TIMEOUT: Final[int] = 30 HTTP_PREFIX: Final[str] = "HTTP" HTTP_VERSION: Final[str] = "1.1" class AirzoneHttpRequest: """Airzone HTTP request.""" def __init__( self, method: str, url: str, headers: dict[str, Any], data: str | None = None, ) -> None: """HTTP request init.""" self.data = data self.headers = headers self.method = method self.url = urlparse(url) def encode(self) -> bytearray: """HTTP request encode.""" buffer = bytearray(self.header().encode()) if self.data is not None: buffer += self.data.encode() return buffer def header(self) -> str: """HTTP request header.""" http = f"{self.method} {self.url.path} {HTTP_PREFIX}/{HTTP_VERSION}" host = f"Host: {self.url.netloc}" headers = "" for key, value in self.headers.items(): headers = f"{headers}{key}: {value}{HTTP_EOL}" if self.data is not None: headers = f"{headers}Content-Length: {len(self.data)}{HTTP_EOL}" return f"{http}{HTTP_EOL}{host}{HTTP_EOL}{headers}{HTTP_EOL}" class AirzoneHttpResponse: """Airzone HTTP response.""" def __init__(self) -> None: """HTTP response init.""" self.body: str | None = None self.buffer = bytearray() self.charset: str = "utf-8" self.header: str | None = None self.header_map: dict[str, str] = {} self.media_type: str | None = None self.reason: str | None = None self.status: int | None = None self.version: str | None = None def append_data(self, data: bytes) -> None: """Buffer HTTP response data.""" self.buffer += data def get_content_length(self) -> int: """Get HTTP Content-Length.""" content_len = self.header_map.get(HTTP_CONTENT_LEN) if content_len is not None: return int(content_len) return 0 def json(self) -> Any: """HTTP response to JSON conversion.""" if self.body is not None: try: return json.loads(self.body) except JSONDecodeError as err: raise InvalidHost(err) from err return None def parse_content_type(self) -> None: """HTTP content type parse.""" content_type = self.header_map.get(HTTP_CONTENT_TYPE) if content_type is None: return ct_split = content_type.split(";") if len(ct_split) < 1: return self.media_type = ct_split.pop(0).strip().lower() charset = None for ct_item in ct_split: item_split = ct_item.split("=", maxsplit=1) if len(item_split) == 2: key = item_split[0].strip() value = item_split[1].strip() if key == HTTP_CHARSET: charset = value.lower() self.charset = charset if self.media_type == "text/html" and charset is None: self.charset = "iso-8859-1" def parse_http_status( self, status: str, ) -> None: """HTTP response status parse.""" if not status.startswith(HTTP_PREFIX): return status_split = status.strip().split(" ", maxsplit=2) self.reason = status_split[2] self.status = int(status_split[1]) self.version = status_split[0].lstrip(f"{HTTP_PREFIX}/") _LOGGER.debug( "HTTP: version=%s status=%s reason=%s", self.version, self.status, self.reason, ) def parse_header_line(self, line: str) -> None: """HTTP response header line parse.""" line_list = line.split(":", maxsplit=1) if len(line_list) == 2: key = line_list[0].strip() value = line_list[1].strip() self.header_map[key] = value def parse_header(self) -> None: """HTTP response header parse.""" if self.header is None: return lines = self.header.splitlines() if len(lines) < 1: return status = lines.pop(0) self.parse_http_status(status) for line in lines: self.parse_header_line(line) _LOGGER.debug("HTTP: headers=%s", self.header_map) self.parse_content_type() def parse_header_bytes(self, header_bytes: bytes) -> None: """HTTP response header bytes parse.""" self.header = header_bytes.decode() self.parse_header() def parse_body_bytes(self, body_bytes: bytes) -> None: """HTTP response body bytes parse.""" self.body = body_bytes.decode(encoding=self.charset, errors="replace") _LOGGER.debug("HTTP: body=%s", self.body) def parse_data(self) -> None: """Parse HTTP response data.""" mv = memoryview(self.buffer) header_sep_bytes = HTTP_HDR_SEP.encode() try: header_end = self.buffer.index(header_sep_bytes) + len(header_sep_bytes) except ValueError as err: raise InvalidHost( f"HTTP Header separator not found: {self.buffer}" ) from err header_bytes = bytes(mv[:header_end]) self.parse_header_bytes(header_bytes) content_len = self.get_content_length() if content_len > 0: body_end = header_end + content_len body_bytes = bytes(mv[header_end:body_end]) self.parse_body_bytes(body_bytes) class AirzoneHttpProtocol(Protocol): """Airzone HTTP Protocol.""" def __init__( self, request: AirzoneHttpRequest, response: AirzoneHttpResponse, future: Future[Any], ) -> None: """Airzone HTTP Protocol init.""" self.future = future self.request = request self.response = response def connection_made( self, transport: Transport, # type: ignore ) -> None: """HTTP connection establised.""" transport.write(self.request.encode()) transport.write_eof() def connection_lost(self, exc: Exception | None) -> None: """HTTP connection lost.""" if exc is not None: _LOGGER.error(exc) if self.future.done(): raise InvalidState("Connection lost") self.future.set_result(True) def data_received(self, data: bytes) -> None: """HTTP data received from server.""" self.response.append_data(data) def eof_received(self) -> None: """HTTP EOF received from server.""" self.response.parse_data() class AirzoneHttp: """Airzone HTTP.""" def __init__(self) -> None: """HTTP init.""" self.headers: dict[str, Any] = { "User-Agent": "aioairzone", "Accept": "*/*", } self.loop = asyncio.get_running_loop() async def request( self, method: str, url: str, data: str | None = None, headers: dict[str, Any] | None = None, timeout: int = HTTP_DEF_TIMEOUT, ) -> AirzoneHttpResponse: """HTTP request.""" async with asyncio.timeout(timeout): req_headers = self.headers if headers is not None: req_headers |= headers response = AirzoneHttpResponse() request = AirzoneHttpRequest( method, url, headers=req_headers, data=data, ) transport = None if request.url.hostname is None: raise InvalidHost("Invalid URL host.") if request.url.port is None: raise InvalidHost("Invalid URL port.") try: future = self.loop.create_future() transport, protocol = await self.loop.create_connection( lambda: AirzoneHttpProtocol(request, response, future), request.url.hostname, request.url.port, ) await future except OSError as err: raise InvalidHost(err) from err finally: if transport is not None: transport.close() return protocol.response aioairzone-1.0.0/aioairzone/localapi.py000066400000000000000000000634411477442374000201720ustar00rootroot00000000000000"""Airzone Local API.""" from __future__ import annotations import asyncio from asyncio import Lock, Semaphore from dataclasses import dataclass from enum import IntEnum from json import JSONDecodeError import logging from typing import Any, cast from aiohttp import ClientConnectorError, ClientSession, ClientTimeout from aiohttp.client_reqrep import ClientResponse from packaging.version import Version from .common import OperationMode, get_system_zone_id, json_dumps, validate_mac_address from .const import ( API_ACS_MAX_TEMP, API_ACS_MIN_TEMP, API_ACS_ON, API_ACS_SET_POINT, API_ACS_TEMP, API_DATA, API_DEMO, API_DHW_PARAMS, API_ERROR_HOT_WATER_NOT_CONNECTED, API_ERROR_IAQ_SENSOR_ID_NOT_AVAILABLE, API_ERROR_METHOD_NOT_SUPPORTED, API_ERROR_REQUEST_MALFORMED, API_ERROR_SYSTEM_ID_NOT_AVAILABLE, API_ERROR_SYSTEM_ID_OUT_RANGE, API_ERROR_ZONE_ID_NOT_AVAILABLE, API_ERROR_ZONE_ID_NOT_PROVIDED, API_ERROR_ZONE_ID_OUT_RANGE, API_ERRORS, API_HVAC, API_INTEGRATION, API_MAC, API_NO_FEEDBACK_PARAMS, API_SYSTEM_ID, API_SYSTEM_PARAMS, API_SYSTEMS, API_V1, API_VERSION, API_WEBSERVER, API_ZONE_ID, API_ZONE_PARAMS, AZD_HOT_WATER, AZD_SYSTEMS, AZD_SYSTEMS_NUM, AZD_VERSION, AZD_WEBSERVER, AZD_ZONES, AZD_ZONES_NUM, DEFAULT_PORT, DEFAULT_SYSTEM_ID, HTTP_CALL_TIMEOUT, HTTP_MAX_REQUESTS, HTTP_QUIRK_VERSION, RAW_DEMO, RAW_DHW, RAW_HEADERS, RAW_HTTP, RAW_HVAC, RAW_INTEGRATION, RAW_QUIRKS, RAW_REASON, RAW_STATUS, RAW_SYSTEMS, RAW_VERSION, RAW_WEBSERVER, ) from .exceptions import ( APIError, HotWaterNotAvailable, IaqSensorNotAvailable, InvalidHost, InvalidMethod, InvalidParam, InvalidSystem, InvalidZone, RequestMalformed, SystemNotAvailable, SystemOutOfRange, ZoneNotAvailable, ZoneNotProvided, ZoneOutOfRange, ) from .hotwater import HotWater from .http import AirzoneHttp from .system import System from .webserver import WebServer from .zone import Zone _LOGGER = logging.getLogger(__name__) class ApiFeature(IntEnum): """Supported features of the Airzone Local API.""" HVAC = 0 SYSTEMS = 1 WEBSERVER = 2 HOT_WATER = 4 @dataclass class ConnectionOptions: """Airzone Local API options for connection.""" host: str port: int = DEFAULT_PORT system_id: int = DEFAULT_SYSTEM_ID http_quirks: bool = False class AirzoneLocalApi: """Airzone Local API device representation.""" def __init__( self, aiohttp_session: ClientSession, options: ConnectionOptions, ): """Device init.""" self._api_raw_data: dict[str, Any] = { RAW_DEMO: {}, RAW_DHW: {}, RAW_HVAC: {}, RAW_HTTP: {}, RAW_INTEGRATION: {}, RAW_SYSTEMS: {}, RAW_VERSION: {}, RAW_WEBSERVER: {}, } self._api_raw_data_lock = Lock() self._api_semaphore: Semaphore = Semaphore(HTTP_MAX_REQUESTS) self._api_timeout: ClientTimeout = ClientTimeout(total=HTTP_CALL_TIMEOUT) self._first_update: bool = True self.aiohttp_session = aiohttp_session self.api_features: int = ApiFeature.HVAC self.api_features_checked = False self.api_features_lock = Lock() self.hotwater: HotWater | None = None self.http = AirzoneHttp() self.http_quirks_needed = True self.options = options self.systems: dict[int, System] = {} self.version: str | None = None self.webserver: WebServer | None = None self.zones: dict[str, Zone] = {} def handle_empty_response(self, function: str, request: str) -> None: """Handle Airzone API empty response.""" error_str = f"{function}: empty {request} API response" if self._first_update: raise APIError(error_str) _LOGGER.error(error_str) @staticmethod def handle_errors(errors: list[dict[str, str]]) -> None: """Handle API errors.""" for error in errors: for key, val in error.items(): if val == API_ERROR_HOT_WATER_NOT_CONNECTED: raise HotWaterNotAvailable(f"{key}: {val}") if val == API_ERROR_IAQ_SENSOR_ID_NOT_AVAILABLE: raise IaqSensorNotAvailable(f"{key}: {val}") if val == API_ERROR_METHOD_NOT_SUPPORTED: raise InvalidMethod(f"{key}: {val}") if val == API_ERROR_REQUEST_MALFORMED: raise RequestMalformed(f"{key}: {val}") if val == API_ERROR_SYSTEM_ID_NOT_AVAILABLE: raise SystemNotAvailable(f"{key}: {val}") if val == API_ERROR_SYSTEM_ID_OUT_RANGE: raise SystemOutOfRange(f"{key}: {val}") if val == API_ERROR_ZONE_ID_OUT_RANGE: raise ZoneOutOfRange(f"{key}: {val}") if val == API_ERROR_ZONE_ID_NOT_AVAILABLE: raise ZoneNotAvailable(f"{key}: {val}") if val == API_ERROR_ZONE_ID_NOT_PROVIDED: raise ZoneNotProvided(f"{key}: {val}") raise APIError(f"{key}: {val}") def http_quirks_enabled(self) -> bool: """API expects HTTP headers + body on the same TCP segment.""" return self.options.http_quirks or self.http_quirks_needed async def aiohttp_request( self, method: str, path: str, data: Any | None = None ) -> dict[str, Any] | None: """Perform aiohttp request.""" async with self._api_semaphore: try: resp: ClientResponse = await self.aiohttp_session.request( method, f"http://{self.options.host}:{self.options.port}/{path}", data=json_dumps(data), headers={"Content-Type": "text/json"}, timeout=self._api_timeout, ) except ClientConnectorError as err: raise InvalidHost(err) from err try: resp_json = await resp.json(content_type=None) except JSONDecodeError as err: raise InvalidHost(err) from err _LOGGER.debug("aiohttp response: %s", resp_json) if resp.status != 200: if resp_json is not None: resp_err = resp_json.get(API_ERRORS) else: resp_err = None if resp_err is not None: self.handle_errors(resp_err) raise APIError(f"HTTP status: {resp.status}") return cast(dict[str, Any], resp_json) async def http_quirks_request( self, method: str, path: str, data: Any | None = None ) -> dict[str, Any] | None: """Perform http quirks request.""" async with self._api_semaphore: resp = await self.http.request( method, f"http://{self.options.host}:{self.options.port}/{path}", data=json_dumps(data), headers={ "Content-Type": "text/json", }, timeout=HTTP_CALL_TIMEOUT, ) resp_json = resp.json() _LOGGER.debug("aiohttp response: %s", resp_json) if resp.status != 200: if resp_json is not None: resp_err = resp_json.get(API_ERRORS) else: resp_err = None if resp_err is not None: self.handle_errors(resp_err) raise APIError(f"HTTP status: {resp.status}") if path.endswith(API_VERSION): async with self._api_raw_data_lock: self._api_raw_data[RAW_HTTP][RAW_HEADERS] = resp.header_map self._api_raw_data[RAW_HTTP][RAW_REASON] = resp.reason self._api_raw_data[RAW_HTTP][RAW_STATUS] = resp.status self._api_raw_data[RAW_HTTP][RAW_VERSION] = resp.version return cast(dict[str, Any], resp_json) async def http_request( self, method: str, path: str, data: Any | None = None ) -> dict[str, Any] | None: """Device HTTP request.""" _LOGGER.debug("http_request: /%s (params=%s)", path, data) if self.http_quirks_enabled(): return await self.http_quirks_request(method, path, data) return await self.aiohttp_request(method, path, data) def update_dhw(self, data: dict[str, Any]) -> None: """Gather Domestic Hot Water data.""" dhw = data.get(API_DATA, {}) if self.hotwater is not None: self.hotwater.update_data(dhw) else: self.hotwater = HotWater(dhw) def update_systems(self, data: dict[str, Any] | None) -> None: """Gather Systems data.""" if data is None: self.handle_empty_response("update_systems", "Systems") return api_systems = data.get(API_SYSTEMS) if api_systems is None: raise APIError(f"update_systems: {API_SYSTEMS} not in API response") for api_system in api_systems: system = self.get_system(api_system[API_SYSTEM_ID]) if system: system.update_data(api_system) def update_webserver(self, data: dict[str, Any]) -> None: """Gather WebServer data.""" if self.webserver is not None: self.webserver.update_data(data) else: self.webserver = WebServer(data) def check_dhw(self, dhw: dict[str, Any]) -> bool: """Check Airzone Domestic Hot Water validity.""" return all( [ dhw.get(API_ACS_MAX_TEMP, 0) != 0, dhw.get(API_ACS_MIN_TEMP, 0) != 0, API_ACS_ON in dhw, dhw.get(API_ACS_SET_POINT, 0) != 0, dhw.get(API_ACS_TEMP, 0) != 0, dhw.get(API_SYSTEM_ID, 0) == 0, ] ) async def check_feature_dhw(self, update: bool) -> None: """Check DHW feature.""" try: dhw = await self.get_dhw() if dhw is None: raise APIError("check_feature_dhw: empty API response") if self.check_dhw(dhw.get(API_DATA, {})): await self.set_api_feature(ApiFeature.HOT_WATER) if update: self.update_dhw(dhw) except (HotWaterNotAvailable, ZoneNotProvided): pass async def check_feature_systems(self, update: bool) -> None: """Check Systems feature.""" try: systems = await self.get_hvac_systems() if systems is None: raise APIError("check_feature_systems: empty API response") if API_SYSTEMS in systems: await self.set_api_feature(ApiFeature.SYSTEMS) if update: self.update_systems(systems) except (SystemOutOfRange, ZoneNotProvided): pass async def check_feature_version(self) -> None: """Check Version feature.""" try: version_data = await self.get_version() if version_data is None: raise APIError("check_feature_version: empty API response") version_str = version_data.get(API_VERSION) if version_str is not None: self.version = version_str self.http_quirks_needed = Version(version_str) < HTTP_QUIRK_VERSION async with self._api_raw_data_lock: self._api_raw_data[RAW_HTTP][RAW_QUIRKS] = self.http_quirks_needed except InvalidMethod: pass async def check_feature_webserver(self) -> None: """Check WebServer feature.""" try: self.webserver = None webserver = await self.get_webserver() if webserver is None: raise APIError("check_feature_webserver: empty API response") if validate_mac_address(webserver.get(API_MAC)): await self.set_api_feature(ApiFeature.WEBSERVER) self.update_webserver(webserver) except InvalidMethod: pass async def check_features(self, update: bool) -> None: """Check Airzone API features.""" # Check version and toggle HTTP quirks first. await self.check_feature_version() tasks = [ asyncio.create_task(self.check_feature_webserver()), asyncio.create_task(self.check_feature_systems(update)), asyncio.create_task(self.check_feature_dhw(update)), ] await asyncio.gather(*tasks) self.api_features_checked = True async def update_feature_dhw(self) -> None: """Update DHW feature.""" dhw = await self.get_dhw() if dhw is not None: self.update_dhw(dhw) else: self.handle_empty_response("update_features", "DHW") async def update_feature_systems(self) -> None: """Update Systems feature.""" systems = await self.get_hvac_systems() if systems is not None: self.update_systems(systems) else: self.handle_empty_response("update_features", "Systems") async def update_feature_webserver(self) -> None: """Update WebServer feature.""" webserver = await self.get_webserver() if webserver is not None: self.update_webserver(webserver) else: self.handle_empty_response("update_features", "WebServer") async def update_features(self) -> None: """Update Airzone features data.""" tasks = [] if not self.api_features_checked: tasks += [asyncio.create_task(self.check_features(True))] else: if self.api_feature(ApiFeature.HOT_WATER): tasks += [asyncio.create_task(self.update_feature_dhw())] if self.api_feature(ApiFeature.SYSTEMS): tasks += [asyncio.create_task(self.update_feature_systems())] if self.api_feature(ApiFeature.WEBSERVER): tasks += [asyncio.create_task(self.update_feature_webserver())] await asyncio.gather(*tasks) async def validate(self) -> str | None: """Validate Airzone API.""" self._first_update = True await self.check_features(False) response = await self.get_hvac() if response is None: raise APIError("validate: empty HVAC API response") if self.options.system_id == DEFAULT_SYSTEM_ID: if API_SYSTEMS not in response: raise InvalidHost(f"validate: {API_SYSTEMS} not in API response") elif API_DATA not in response: raise InvalidHost(f"validate: {API_DATA} not in API response") if self.webserver: return self.webserver.get_mac() return None async def update(self) -> None: """Gather Airzone data.""" hvac = await self.get_hvac() if hvac is None: self.handle_empty_response("update", "HVAC") return for system in self.systems.values(): system.set_available(False) for zone in self.zones.values(): zone.set_available(False) if self.options.system_id == DEFAULT_SYSTEM_ID: hvac_systems = hvac.get(API_SYSTEMS) if hvac_systems is None: raise APIError(f"update: {API_SYSTEMS} not in API response") for system_data in hvac_systems: self.parse_system_zones(system_data) else: self.parse_system_zones(hvac) await self.update_features() self._first_update = False def parse_system_zones(self, system_data: dict[str, Any]) -> None: """Parse all zones from system data.""" system_zones: list[dict[str, Any]] = system_data.get(API_DATA, []) for zone_data in system_zones: system_id = int(zone_data.get(API_SYSTEM_ID, 0)) if system_id > 0: if system_id not in self.systems: self.systems[system_id] = System(system_id, zone_data) else: self.systems[system_id].update_zone_data(zone_data) zone_id = int(zone_data.get(API_ZONE_ID, 0)) if zone_id > 0: system_zone_id = get_system_zone_id(system_id, zone_id) if system_zone_id not in self.zones: _zone = Zone(system_id, zone_id, zone_data) self.zones[system_zone_id] = _zone self.systems[system_id].add_zone(_zone) else: self.zones[system_zone_id].update_data(zone_data) self.update_system_from_zone( self.systems[system_id], self.zones[system_zone_id] ) self.update_zones_from_master_zone() def update_system_from_zone(self, system: System, zone: Zone) -> None: """Update system data from zone.""" if zone.get_master(): if (eco_adapt := zone.get_eco_adapt()) is not None: system.set_eco_adapt(eco_adapt) system.set_master_system_zone(zone.get_system_zone_id()) system.set_master_zone(zone.get_id()) if (mode := zone.get_mode()) is not None: system.set_mode(mode) system.set_modes(zone.get_modes()) else: if system.get_eco_adapt() is None: system.set_eco_adapt(zone.get_eco_adapt()) if system.get_mode() is None: system.set_mode(zone.get_mode()) if len(system.get_modes()) == 0: system.set_modes(zone.get_modes()) def update_zones_from_master_zone(self) -> None: """Update slave zones data with their master zone.""" for zone in self.zones.values(): system_id = zone.get_system_id() if system_id is not None and not zone.get_master(): modes: list[OperationMode] = [] master_id = zone.get_master_zone() if master_id is None: system = self.get_system(system_id) modes = system.get_modes() else: master_zone = self.get_zone(system_id, master_id) modes = master_zone.get_modes() if len(modes) > 0: zone.set_modes(modes) async def get_demo(self) -> dict[str, Any] | None: """Return Airzone demo.""" res = await self.http_request( "POST", f"{API_V1}/{API_DEMO}", ) await self.set_api_raw_data(RAW_DEMO, res) return res async def get_dhw( self, params: dict[str, Any] | None = None ) -> dict[str, Any] | None: """Return Airzone DHW (Domestic Hot Water).""" if not params: params = { API_SYSTEM_ID: 0, } res = await self.http_request( "POST", f"{API_V1}/{API_HVAC}", params, ) await self.set_api_raw_data(RAW_DHW, res) return res async def get_hvac_systems( self, params: dict[str, Any] | None = None ) -> dict[str, Any] | None: """Return Airzone HVAC systems.""" if not params: params = { API_SYSTEM_ID: 127, } res = await self.http_request( "POST", f"{API_V1}/{API_HVAC}", params, ) await self.set_api_raw_data(RAW_SYSTEMS, res) return res async def get_hvac( self, params: dict[str, Any] | None = None ) -> dict[str, Any] | None: """Return Airzone HVAC zones.""" if not params: params = { API_SYSTEM_ID: self.options.system_id, API_ZONE_ID: 0, } res = await self.http_request( "POST", f"{API_V1}/{API_HVAC}", params, ) await self.set_api_raw_data(RAW_HVAC, res) return res async def get_integration(self) -> dict[str, Any] | None: """Return Airzone integration.""" res = await self.http_request( "POST", f"{API_V1}/{API_INTEGRATION}", ) await self.set_api_raw_data(RAW_INTEGRATION, res) return res async def get_version(self) -> dict[str, Any] | None: """Return Airzone Local API version.""" res = await self.http_request( "POST", f"{API_V1}/{API_VERSION}", ) await self.set_api_raw_data(RAW_VERSION, res) return res async def get_webserver(self) -> dict[str, Any] | None: """Return Airzone WebServer.""" res = await self.http_request( "POST", f"{API_V1}/{API_WEBSERVER}", ) await self.set_api_raw_data(RAW_WEBSERVER, res) return res async def put_hvac(self, params: dict[str, Any]) -> dict[str, Any] | None: """Perform a PUT request to update HVAC parameters.""" return await self.http_request( "PUT", f"{API_V1}/{API_HVAC}", params, ) async def set_api_feature(self, feature: int) -> None: """Set API feature.""" async with self.api_features_lock: self.api_features |= feature async def set_api_raw_data(self, key: str, data: dict[str, Any] | None) -> None: """Save API raw data if not empty.""" if data is not None: async with self._api_raw_data_lock: self._api_raw_data[key] = data async def set_dhw_parameters(self, params: dict[str, Any]) -> dict[str, Any]: """Set Airzone Hot Water parameters and handle response.""" res = await self.put_hvac(params) if res is None: raise APIError("set_dhw: empty HVAC API response") if API_DATA not in res: if API_ERRORS in res: self.handle_errors(res[API_ERRORS]) raise APIError(f"set_dhw: {API_DATA} not in API response") if self.hotwater is not None: data: dict[str, Any] = res.get(API_DATA, {}) for key, value in data.items(): if key in API_DHW_PARAMS: self.hotwater.set_param(key, value) return res async def set_hvac_parameters(self, params: dict[str, Any]) -> dict[str, Any]: """Set Airzone HVAC parameters and handle response.""" res = await self.put_hvac(params) if res is None: raise APIError("set_hvac: empty HVAC API response") if API_DATA not in res: if API_ERRORS in res: self.handle_errors(res[API_ERRORS]) raise APIError(f"set_hvac: {API_DATA} not in API response") data: dict[str, Any] = res[API_DATA][0] for param in API_NO_FEEDBACK_PARAMS: value = params.get(param) if value is not None and param not in data: _LOGGER.debug("set_hvac: forcing %s=%s", param, value) data[param] = value for key, value in params.items(): if ( key in [API_SYSTEM_ID, API_ZONE_ID] and value != 0 and data.get(key) != value ): if key == API_SYSTEM_ID: raise InvalidSystem( f"set_hvac: System mismatch: {data.get(key)} vs {value}" ) if key == API_ZONE_ID: raise InvalidZone( f"set_hvac: Zone mismatch: {data.get(key)} vs {value}" ) if key not in data: raise InvalidParam(f"set_hvac: param not in data: {key}={value}") system = self.get_system(data[API_SYSTEM_ID]) zone = self.get_zone(data[API_SYSTEM_ID], data[API_ZONE_ID]) for key, value in data.items(): if key in API_SYSTEM_PARAMS: system.set_param(key, value) elif key in API_ZONE_PARAMS: zone.set_param(key, value) return res def api_feature(self, feature: int) -> bool: """Get API feature.""" return bool(self.api_features & feature) def raw_data(self) -> dict[str, Any]: """Return raw Airzone API data.""" return self._api_raw_data def data(self) -> dict[str, Any]: """Return Airzone device data.""" data: dict[str, Any] = {} if self.hotwater is not None: data[AZD_HOT_WATER] = self.hotwater.data() data[AZD_SYSTEMS_NUM] = self.num_systems() if len(self.systems) > 0: systems: dict[int, Any] = {} for system_id, system in self.systems.items(): systems[system_id] = system.data() data[AZD_SYSTEMS] = systems if self.webserver is not None: data[AZD_WEBSERVER] = self.webserver.data() data[AZD_ZONES_NUM] = self.num_zones() if len(self.zones) > 0: zones: dict[str, Any] = {} for system_zone_id, zone in self.zones.items(): zones[system_zone_id] = zone.data() data[AZD_ZONES] = zones if self.version is not None: data[AZD_VERSION] = self.version return data def get_system(self, system_id: int) -> System: """Return Airzone system.""" if system_id not in self.systems: raise InvalidSystem(f"System {system_id} not present") return self.systems[system_id] def get_zone(self, system_id: int, zone_id: int) -> Zone: """Return Airzone zone.""" system_zone_id = get_system_zone_id(system_id, zone_id) if system_zone_id not in self.zones: raise InvalidZone(f"Zone {system_zone_id} not present") return self.zones[system_zone_id] def num_systems(self) -> int: """Return number of systems.""" return len(self.systems) def num_zones(self) -> int: """Return number of zones.""" return len(self.zones) aioairzone-1.0.0/aioairzone/py.typed000066400000000000000000000000001477442374000175110ustar00rootroot00000000000000aioairzone-1.0.0/aioairzone/system.py000066400000000000000000000202241477442374000177220ustar00rootroot00000000000000"""Airzone Local API Device.""" from __future__ import annotations from typing import Any from .common import ( EcoAdapt, OperationMode, SystemType, parse_bool, parse_int, parse_str, ) from .const import ( API_ECO_ADAPT, API_ERRORS, API_MANUFACTURER, API_MC_CONNECTED, API_MODE, API_POWER, API_SYSTEM_FIRMWARE, API_SYSTEM_TYPE, AZD_AVAILABLE, AZD_CLAMP_METER, AZD_ECO_ADAPT, AZD_ENERGY, AZD_ERRORS, AZD_FIRMWARE, AZD_FULL_NAME, AZD_ID, AZD_MANUFACTURER, AZD_MASTER_SYSTEM_ZONE, AZD_MASTER_ZONE, AZD_MODE, AZD_MODEL, AZD_MODES, AZD_PROBLEMS, ERROR_SYSTEM, ) from .zone import Zone class System: """Airzone System.""" def __init__(self, system_id: int, zone_data: dict[str, Any]): """System init.""" self.available: bool = True self.clamp_meter: bool | None = None self.eco_adapt: EcoAdapt | None = None self.energy: int | None = None self.errors: list[str] = [] self.id: int = system_id self.firmware: str | None = None self.manufacturer: str | None = None self.master_system_zone: str | None = None self.master_zone: int | None = None self.mode: OperationMode | None = None self.modes: list[OperationMode] = [] self.type: SystemType | None = None self.zones: dict[int, Zone] = {} self.update_zone_data(zone_data) def update_zone_data(self, zone_data: dict[str, Any]) -> None: """Update System data.""" self.available = True errors: list[dict[str, str]] = zone_data.get(API_ERRORS, []) for error in errors: for key, val in error.items(): self.add_error(val, key) def data(self) -> dict[str, Any]: """Return Airzone system data.""" data: dict[str, Any] = {} data[AZD_AVAILABLE] = self.get_available() clamp_meter = self.get_clamp_meter() if clamp_meter is not None: data[AZD_CLAMP_METER] = clamp_meter if clamp_meter: energy = self.get_energy() if energy is not None: data[AZD_ENERGY] = energy eco_adapt = self.get_eco_adapt() if eco_adapt is not None: data[AZD_ECO_ADAPT] = eco_adapt errors = self.get_errors() if len(errors) > 0: data[AZD_ERRORS] = errors firmware = self.get_firmware() if firmware is not None: data[AZD_FIRMWARE] = firmware full_name = self.get_full_name() if full_name is not None: data[AZD_FULL_NAME] = full_name data[AZD_ID] = self.get_id() manufacturer = self.get_manufacturer() if manufacturer is not None: data[AZD_MANUFACTURER] = manufacturer master_system_zone = self.get_master_system_zone() if master_system_zone is not None: data[AZD_MASTER_SYSTEM_ZONE] = master_system_zone master_zone = self.get_master_zone() if master_zone is not None: data[AZD_MASTER_ZONE] = master_zone mode = self.get_mode() if mode is not None: data[AZD_MODE] = mode model = self.get_model() if self.type is not None: data[AZD_MODEL] = model modes = self.get_modes() if modes is not None: data[AZD_MODES] = modes data[AZD_PROBLEMS] = self.get_problems() return data def add_error(self, error: str, error_id: str | None = None) -> None: """Add system error.""" if error_id is not None: if error_id.casefold() == ERROR_SYSTEM and error not in self.errors: self.errors += [error] else: if error not in self.errors: self.errors += [error] def add_zone(self, zone: Zone) -> None: """Add zone to system.""" zone_id = zone.get_id() if zone_id not in self.zones: self.zones[zone_id] = zone def get_available(self) -> bool: """Return availability.""" return self.available def get_clamp_meter(self) -> bool | None: """Return system clamp meter connection.""" return self.clamp_meter def get_eco_adapt(self) -> EcoAdapt | None: """Return system Eco Adapt.""" return self.eco_adapt def get_energy(self) -> int | None: """Return system energy consumption.""" return self.energy def get_errors(self) -> list[str]: """Return system errors.""" return self.errors def get_id(self) -> int: """Return system ID.""" return self.id def get_firmware(self) -> str | None: """Return system firmware.""" if self.firmware and "." not in self.firmware and len(self.firmware) > 2: return f"{self.firmware[0:1]}.{self.firmware[1:]}" return self.firmware def get_full_name(self) -> str: """Return full name.""" return f"Airzone [{self.get_id()}] System" def get_manufacturer(self) -> str | None: """Return system manufacturer.""" return self.manufacturer def get_master_system_zone(self) -> str | None: """Return master system zone ID.""" return self.master_system_zone def get_master_zone(self) -> int | None: """Return master zone ID.""" return self.master_zone def get_model(self) -> str | None: """Return system model.""" if self.type: return str(self.type) return None def get_mode(self) -> OperationMode | None: """Return system mode.""" return self.mode def get_modes(self) -> list[OperationMode]: """Return system modes.""" modes = self.modes if len(modes) == 0 and self.mode is not None: modes = [self.mode] if OperationMode.STOP not in modes: modes += [OperationMode.STOP] return modes def get_problems(self) -> bool: """Return system problems.""" return bool(self.errors) def set_available(self, available: bool) -> None: """Set availability.""" self.available = available def set_eco_adapt(self, eco_adapt: EcoAdapt | None) -> None: """Set system Eco Adapt.""" self.eco_adapt = eco_adapt def set_master_system_zone(self, master_system_zone: str) -> None: """Set master system zone ID.""" self.master_system_zone = master_system_zone def set_master_zone(self, master_zone: int) -> None: """Set master zone ID.""" self.master_zone = master_zone def set_mode(self, mode: OperationMode | None) -> None: """Set system mode.""" self.mode = mode def set_modes(self, modes: list[OperationMode]) -> None: """Set system modes.""" self.modes = modes def set_param(self, key: str, value: Any) -> None: """Update parameters by key and value.""" if key == API_ECO_ADAPT: self.eco_adapt = EcoAdapt(value) elif key == API_MODE: self.mode = OperationMode(value) for zone in self.zones.values(): zone.set_param(key, value) def update_data(self, data: dict[str, Any]) -> None: """Update system parameters by dict.""" self.available = True mc_connected = parse_bool(data.get(API_MC_CONNECTED)) if mc_connected is not None: self.clamp_meter = mc_connected errors: list[dict[str, str]] | None = data.get(API_ERRORS) if errors is not None: for error in errors: for val in error.values(): self.add_error(val) manufacturer = parse_str(data.get(API_MANUFACTURER)) if manufacturer is not None: self.manufacturer = manufacturer power = parse_int(data.get(API_POWER)) if power is not None: self.energy = power system_firmware = parse_str(data.get(API_SYSTEM_FIRMWARE)) if system_firmware is not None: self.firmware = system_firmware system_type = parse_int(data.get(API_SYSTEM_TYPE)) if system_type is not None: self.type = SystemType(system_type) aioairzone-1.0.0/aioairzone/thermostat.py000066400000000000000000000052041477442374000205710ustar00rootroot00000000000000"""Airzone Local API Thermostat.""" from __future__ import annotations from typing import Any, Final from .common import ThermostatType, parse_bool, parse_int, parse_str from .const import ( API_BATTERY, API_COVERAGE, API_THERMOS_FIRMWARE, API_THERMOS_RADIO, API_THERMOS_TYPE, THERMOSTAT_RADIO, THERMOSTAT_WIRED, ) LOW_BATTERY_VALUE: Final[int] = 35 class Thermostat: """Airzone Thermostat.""" def __init__(self, data: dict[str, Any]): """Thermostat init.""" self.battery: int | None = None self.firmware: str | None = None self.radio: bool | None = None self.signal: int | None = None self.type: ThermostatType | None = None thermos_battery = parse_int(data.get(API_BATTERY)) if thermos_battery is not None: self.battery = thermos_battery thermos_firmware = parse_str(data.get(API_THERMOS_FIRMWARE)) if thermos_firmware is not None: self.firmware = thermos_firmware thermos_radio = parse_bool(data.get(API_THERMOS_RADIO)) if thermos_radio is not None: self.radio = thermos_radio thermos_signal = parse_int(data.get(API_COVERAGE)) if thermos_signal is not None: self.signal = thermos_signal thermos_type = parse_int(data.get(API_THERMOS_TYPE)) if thermos_type is not None: self.type = ThermostatType(thermos_type) def get_battery(self) -> int | None: """Return Airzone Thermostat battery.""" return self.battery def get_battery_low(self) -> bool | None: """Return Airzone Thermostat low battery.""" if not self.get_radio(): return None if self.battery is None: return None return self.battery < LOW_BATTERY_VALUE def get_firmware(self) -> str | None: """Return Airzone Thermostat firmware.""" if self.firmware and "." not in self.firmware and len(self.firmware) > 2: return f"{self.firmware[0:1]}.{self.firmware[1:]}" return self.firmware def get_model(self) -> str | None: """Return Airzone Thermostat model.""" if self.type: name = str(self.type) if self.type.exists_radio(): sfx = f" ({THERMOSTAT_RADIO if self.radio else THERMOSTAT_WIRED})" else: sfx = "" return f"{name}{sfx}" return None def get_radio(self) -> bool | None: """Return Airzone Thermostat radio.""" return self.radio def get_signal(self) -> int | None: """Return Airzone Thermostat signal.""" return self.signal aioairzone-1.0.0/aioairzone/webserver.py000066400000000000000000000105601477442374000204040ustar00rootroot00000000000000"""Airzone Local API WebServer.""" from __future__ import annotations from typing import Any from .common import WebServerInterface, WebServerType, parse_int, parse_str from .const import ( API_INTERFACE, API_MAC, API_WIFI, API_WIFI_CHANNEL, API_WIFI_QUALITY, API_WIFI_RSSI, API_WS_AIDOO, API_WS_AZ, API_WS_FIRMWARE, API_WS_TYPE, AZD_FIRMWARE, AZD_FULL_NAME, AZD_INTERFACE, AZD_MAC, AZD_MODEL, AZD_WIFI_CHANNEL, AZD_WIFI_QUALITY, AZD_WIFI_RSSI, ) class WebServer: """Airzone WebServer.""" def __init__(self, data: dict[str, Any]): """WebServer init.""" self.firmware: str | None = None self.interface: WebServerInterface | None = None self.mac: str | None = None self.type: WebServerType | None = None self.wifi_channel: int | None = None self.wifi_quality: int | None = None self.wifi_rssi: int | None = None self.update_data(data) def update_data(self, data: dict[str, Any]) -> None: """Update WebServer data.""" interface = parse_str(data.get(API_INTERFACE)) if interface == API_WIFI: self.interface = WebServerInterface.WIFI elif interface is not None: self.interface = WebServerInterface.ETHERNET mac = parse_str(data.get(API_MAC)) if mac is not None: self.mac = mac wifi_channel = parse_int(data.get(API_WIFI_CHANNEL)) if wifi_channel is not None: self.wifi_channel = wifi_channel wifi_quality = parse_int(data.get(API_WIFI_QUALITY)) if wifi_quality is not None: self.wifi_quality = wifi_quality wifi_rssi = parse_int(data.get(API_WIFI_RSSI)) if wifi_rssi is not None: self.wifi_rssi = wifi_rssi ws_firmware = parse_str(data.get(API_WS_FIRMWARE)) if ws_firmware is not None: self.firmware = ws_firmware ws_type = parse_str(data.get(API_WS_TYPE)) if ws_type == API_WS_AZ: self.type = WebServerType.AIRZONE elif ws_type == API_WS_AIDOO: self.type = WebServerType.AIDOO elif ws_type is not None: self.type = WebServerType.UNKNOWN def data(self) -> dict[str, Any]: """Return Airzone system data.""" data: dict[str, Any] = {} firmware = self.get_firmware() if firmware is not None: data[AZD_FIRMWARE] = firmware full_name = self.get_full_name() if full_name is not None: data[AZD_FULL_NAME] = full_name interface = self.get_interface() if interface is not None: data[AZD_INTERFACE] = interface mac = self.get_mac() if mac is not None: data[AZD_MAC] = mac model = self.get_model() if model is not None: data[AZD_MODEL] = model wifi_channel = self.get_wifi_channel() if wifi_channel is not None: data[AZD_WIFI_CHANNEL] = wifi_channel wifi_quality = self.get_wifi_quality() if wifi_quality is not None: data[AZD_WIFI_QUALITY] = wifi_quality wifi_rssi = self.get_wifi_rssi() if wifi_rssi is not None: data[AZD_WIFI_RSSI] = wifi_rssi return data def get_firmware(self) -> str | None: """Return WebServer firmware.""" if self.firmware and "." not in self.firmware and len(self.firmware) > 2: return f"{self.firmware[0:1]}.{self.firmware[1:]}" return self.firmware def get_full_name(self) -> str | None: """Return full name.""" return self.get_model() def get_interface(self) -> WebServerInterface | None: """Return WebServer network interface.""" return self.interface def get_mac(self) -> str | None: """Return WebServer MAC address.""" return self.mac def get_model(self) -> str | None: """Return WebServer model.""" if self.type: return str(self.type) return None def get_wifi_channel(self) -> int | None: """Return WebServer wifi channel.""" return self.wifi_channel def get_wifi_quality(self) -> int | None: """Return WebServer wifi quality.""" return self.wifi_quality def get_wifi_rssi(self) -> int | None: """Return WebServer wifi RSSI.""" return self.wifi_rssi aioairzone-1.0.0/aioairzone/zone.py000066400000000000000000000706011477442374000173550ustar00rootroot00000000000000"""Airzone Local API Device.""" from __future__ import annotations from typing import Any from .common import ( AirzoneStages, EcoAdapt, GrilleAngle, OperationAction, OperationMode, SleepTimeout, TemperatureUnit, get_system_zone_id, parse_bool, parse_float, parse_int, parse_str, ) from .const import ( API_AIR_DEMAND, API_ANTI_FREEZE, API_BUG_MAX_TEMP_FAH, API_BUG_MIN_TEMP_FAH, API_COLD_ANGLE, API_COLD_DEMAND, API_COLD_STAGE, API_COLD_STAGES, API_COOL_MAX_TEMP, API_COOL_MIN_TEMP, API_COOL_SET_POINT, API_DOUBLE_SET_POINT, API_DOUBLE_SET_POINT_PARAMS, API_ECO_ADAPT, API_ERROR_LOW_BATTERY, API_ERRORS, API_FLOOR_DEMAND, API_HEAT_ANGLE, API_HEAT_DEMAND, API_HEAT_MAX_TEMP, API_HEAT_MIN_TEMP, API_HEAT_SET_POINT, API_HEAT_STAGE, API_HEAT_STAGES, API_HUMIDITY, API_MASTER_ZONE_ID, API_MAX_TEMP, API_MIN_TEMP, API_MODE, API_MODES, API_NAME, API_ON, API_ROOM_TEMP, API_SET_POINT, API_SLEEP, API_SPEED, API_SPEEDS, API_TEMP_STEP, API_UNITS, AZD_ABS_TEMP_MAX, AZD_ABS_TEMP_MIN, AZD_ACTION, AZD_AIR_DEMAND, AZD_ANTI_FREEZE, AZD_AVAILABLE, AZD_BATTERY_LOW, AZD_COLD_ANGLE, AZD_COLD_DEMAND, AZD_COLD_STAGE, AZD_COLD_STAGES, AZD_COOL_TEMP_MAX, AZD_COOL_TEMP_MIN, AZD_COOL_TEMP_SET, AZD_DEMAND, AZD_DOUBLE_SET_POINT, AZD_ECO_ADAPT, AZD_ERRORS, AZD_FLOOR_DEMAND, AZD_FULL_NAME, AZD_HEAT_ANGLE, AZD_HEAT_DEMAND, AZD_HEAT_STAGE, AZD_HEAT_STAGES, AZD_HEAT_TEMP_MAX, AZD_HEAT_TEMP_MIN, AZD_HEAT_TEMP_SET, AZD_HUMIDITY, AZD_ID, AZD_MASTER, AZD_MASTER_ZONE, AZD_MODE, AZD_MODES, AZD_NAME, AZD_ON, AZD_PROBLEMS, AZD_SLEEP, AZD_SPEED, AZD_SPEEDS, AZD_SYSTEM, AZD_TEMP, AZD_TEMP_MAX, AZD_TEMP_MIN, AZD_TEMP_SET, AZD_TEMP_STEP, AZD_TEMP_UNIT, AZD_THERMOSTAT_BATTERY, AZD_THERMOSTAT_FW, AZD_THERMOSTAT_MODEL, AZD_THERMOSTAT_RADIO, AZD_THERMOSTAT_SIGNAL, DEFAULT_TEMP_MAX_CELSIUS, DEFAULT_TEMP_MAX_FAHRENHEIT, DEFAULT_TEMP_MIN_CELSIUS, DEFAULT_TEMP_MIN_FAHRENHEIT, DEFAULT_TEMP_STEP_CELSIUS, DEFAULT_TEMP_STEP_FAHRENHEIT, ERROR_ZONE, ) from .thermostat import Thermostat class Zone: """Airzone Zone.""" def __init__(self, system_id: int, zone_id: int, zone_data: dict[str, Any]): """Zone init.""" self.air_demand: bool | None = None self.anti_freeze: bool | None = None self.available: bool = True self.cold_angle: GrilleAngle | None = None self.cold_demand: bool | None = None self.cold_stage: AirzoneStages | None = None self.cold_stages: list[AirzoneStages] = [] self.cool_temp_max: float | None = None self.cool_temp_min: float | None = None self.cool_temp_set: float | None = None self.double_set_point: bool | None = None self.eco_adapt: EcoAdapt | None = None self.errors: list[str] = [] self.floor_demand: bool | None = None self.heat_angle: GrilleAngle | None = None self.heat_demand: bool | None = None self.heat_temp_max: float | None = None self.heat_temp_min: float | None = None self.heat_temp_set: float | None = None self.heat_stage: AirzoneStages | None = None self.heat_stages: list[AirzoneStages] = [] self.humidity: int | None = None self.id = zone_id self.master_zone: int | None = None self.mode: OperationMode self.modes: list[OperationMode] = [] self.on: bool self.sleep: SleepTimeout | None = None self.speed: int | None = None self.speeds: list[int] = [] self.system_id: int = system_id self.system_zone_id: str = get_system_zone_id(system_id, zone_id) self.temp_set: float | None = None self.name: str = f"Airzone {self.get_system_zone_id()}" self.update_data(zone_data) def update_data(self, zone_data: dict[str, Any]) -> None: """Update Zone data.""" self.available = True self.double_set_point_params: bool = ( zone_data.keys() >= API_DOUBLE_SET_POINT_PARAMS ) self.master = bool(API_MODES in zone_data) self.on = bool(zone_data[API_ON]) self.temp = float(zone_data[API_ROOM_TEMP]) self.temp_max = float(zone_data[API_MAX_TEMP]) self.temp_min = float(zone_data[API_MIN_TEMP]) self.temp_step: float | None = None self.temp_unit = TemperatureUnit(zone_data[API_UNITS]) self.thermostat = Thermostat(zone_data) master_zone_id = parse_int(zone_data.get(API_MASTER_ZONE_ID)) if master_zone_id is not None and master_zone_id != self.id: self.master_zone = master_zone_id air_demand = parse_bool(zone_data.get(API_AIR_DEMAND)) if air_demand is not None: self.air_demand = air_demand floor_demand = parse_bool(zone_data.get(API_FLOOR_DEMAND)) if floor_demand is not None: self.floor_demand = floor_demand anti_freeze = parse_bool(zone_data.get(API_ANTI_FREEZE)) if anti_freeze is not None: self.anti_freeze = anti_freeze cold_demand = parse_bool(zone_data.get(API_COLD_DEMAND)) if cold_demand is not None: self.cold_demand = cold_demand heat_demand = parse_bool(zone_data.get(API_HEAT_DEMAND)) if heat_demand is not None: self.heat_demand = heat_demand double_set_point = parse_bool(zone_data.get(API_DOUBLE_SET_POINT)) if double_set_point is not None: self.double_set_point = double_set_point eco_adapt = parse_str(zone_data.get(API_ECO_ADAPT)) if eco_adapt is not None: self.eco_adapt = EcoAdapt(eco_adapt) humidity = parse_int(zone_data.get(API_HUMIDITY)) if humidity is not None: self.humidity = humidity cold_angle = parse_int(zone_data.get(API_COLD_ANGLE)) if cold_angle is not None: self.cold_angle = GrilleAngle(cold_angle) heat_angle = parse_int(zone_data.get(API_HEAT_ANGLE)) if heat_angle is not None: self.heat_angle = GrilleAngle(heat_angle) cold_stage = parse_int(zone_data.get(API_COLD_STAGE)) if cold_stage is not None: self.cold_stage = AirzoneStages(cold_stage) cold_stages = parse_int(zone_data.get(API_COLD_STAGES)) if cold_stages is not None: self.cold_stages = AirzoneStages(cold_stages).to_list() elif self.cold_stage is not None and self.cold_stage.exists(): self.cold_stages = [self.cold_stage] heat_stage = parse_int(zone_data.get(API_HEAT_STAGE)) if heat_stage is not None: self.heat_stage = AirzoneStages(heat_stage) heat_stages = parse_int(zone_data.get(API_HEAT_STAGES)) if heat_stages is not None: self.heat_stages = AirzoneStages(heat_stages).to_list() elif self.heat_stage is not None and self.heat_stage.exists(): self.heat_stages = [self.heat_stage] cool_temp_max = parse_float(zone_data.get(API_COOL_MAX_TEMP)) if cool_temp_max is not None: self.cool_temp_max = cool_temp_max cool_temp_min = parse_float(zone_data.get(API_COOL_MIN_TEMP)) if cool_temp_min is not None: self.cool_temp_min = cool_temp_min cool_set_point = parse_float(zone_data.get(API_COOL_SET_POINT)) if cool_set_point is not None: cool_temp_set = self.validate_temp_set(cool_set_point, self.cool_temp_max) if cool_temp_set is not None: self.cool_temp_set = cool_temp_set heat_temp_max = parse_float(zone_data.get(API_HEAT_MAX_TEMP)) if heat_temp_max is not None: self.heat_temp_max = heat_temp_max heat_temp_min = parse_float(zone_data.get(API_HEAT_MIN_TEMP)) if heat_temp_min is not None: self.heat_temp_min = heat_temp_min heat_set_point = parse_float(zone_data.get(API_HEAT_SET_POINT)) if heat_set_point is not None: heat_temp_set = self.validate_temp_set(heat_set_point, self.heat_temp_max) if heat_temp_set is not None: self.heat_temp_set = heat_temp_set errors: list[dict[str, str]] | None = zone_data.get(API_ERRORS) if errors is not None: for error in errors: for key, val in error.items(): self.add_error(key, val) mode = parse_int(zone_data.get(API_MODE)) if mode is not None: self.mode = OperationMode(mode) else: self.master = True self.mode = OperationMode.AUTO self.modes = [self.mode] if self.master: modes = zone_data.get(API_MODES) if modes is not None: mode_list = [] for cur_mode in modes: mode_list += [OperationMode(cur_mode)] self.modes = mode_list name = parse_str(zone_data.get(API_NAME)) if name is not None: self.name = name sleep = parse_int(zone_data.get(API_SLEEP)) if sleep is not None: self.sleep = SleepTimeout(sleep) speed = parse_int(zone_data.get(API_SPEED)) if speed is not None: self.speed = speed speeds = parse_int(zone_data.get(API_SPEEDS)) if speeds is not None: self.speeds = list(range(0, speeds + 1)) set_point = parse_float(zone_data.get(API_SET_POINT)) if set_point is not None: temp_set = self.validate_temp_set(set_point, self.temp_max) if temp_set is not None: self.temp_set = temp_set temp_step = parse_float(zone_data.get(API_TEMP_STEP)) if temp_step is not None: self.temp_step = temp_step else: if self.temp_unit == TemperatureUnit.FAHRENHEIT: self.temp_step = DEFAULT_TEMP_STEP_FAHRENHEIT else: self.temp_step = DEFAULT_TEMP_STEP_CELSIUS def data(self) -> dict[str, Any]: """Return Airzone zone data.""" data = { AZD_ABS_TEMP_MAX: self.get_abs_temp_max(), AZD_ABS_TEMP_MIN: self.get_abs_temp_min(), AZD_ACTION: self.get_action(), AZD_AVAILABLE: self.get_available(), AZD_DEMAND: self.get_demand(), AZD_DOUBLE_SET_POINT: self.get_double_set_point(), AZD_ID: self.get_id(), AZD_MASTER: self.get_master(), AZD_MODE: self.get_mode(), AZD_NAME: self.get_name(), AZD_ON: self.get_on(), AZD_PROBLEMS: self.get_problems(), AZD_SYSTEM: self.get_system_id(), AZD_TEMP: self.get_temp(), AZD_TEMP_MAX: self.get_temp_max(), AZD_TEMP_MIN: self.get_temp_min(), AZD_TEMP_UNIT: self.get_temp_unit(), } air_demand = self.get_air_demand() if air_demand is not None: data[AZD_AIR_DEMAND] = air_demand floor_demand = self.get_floor_demand() if floor_demand is not None: data[AZD_FLOOR_DEMAND] = floor_demand anti_freeze = self.get_anti_freeze() if anti_freeze is not None: data[AZD_ANTI_FREEZE] = anti_freeze eco_adapt = self.get_eco_adapt() if eco_adapt is not None: data[AZD_ECO_ADAPT] = eco_adapt full_name = self.get_full_name() if full_name is not None: data[AZD_FULL_NAME] = full_name humidity = self.get_humidity() if humidity is not None: data[AZD_HUMIDITY] = humidity cool_temp_max = self.get_cool_temp_max() if cool_temp_max: data[AZD_COOL_TEMP_MAX] = cool_temp_max cool_temp_min = self.get_cool_temp_min() if cool_temp_min: data[AZD_COOL_TEMP_MIN] = cool_temp_min cool_temp_set = self.get_cool_temp_set() if cool_temp_set: data[AZD_COOL_TEMP_SET] = cool_temp_set heat_temp_max = self.get_heat_temp_max() if heat_temp_max: data[AZD_HEAT_TEMP_MAX] = heat_temp_max heat_temp_min = self.get_heat_temp_min() if heat_temp_min: data[AZD_HEAT_TEMP_MIN] = heat_temp_min heat_temp_set = self.get_heat_temp_set() if heat_temp_set: data[AZD_HEAT_TEMP_SET] = heat_temp_set cold_angle = self.get_cold_angle() if cold_angle is not None: data[AZD_COLD_ANGLE] = cold_angle heat_angle = self.get_heat_angle() if heat_angle is not None: data[AZD_HEAT_ANGLE] = heat_angle cold_demand = self.get_cold_demand() if cold_demand is not None: data[AZD_COLD_DEMAND] = cold_demand heat_demand = self.get_heat_demand() if heat_demand is not None: data[AZD_HEAT_DEMAND] = heat_demand cold_stage = self.get_cold_stage() if cold_stage is not None: data[AZD_COLD_STAGE] = cold_stage cold_stages = self.get_cold_stages() if cold_stages is not None: data[AZD_COLD_STAGES] = cold_stages heat_stage = self.get_heat_stage() if heat_stage is not None: data[AZD_HEAT_STAGE] = heat_stage heat_stages = self.get_heat_stages() if heat_stages is not None: data[AZD_HEAT_STAGES] = heat_stages sleep = self.get_sleep() if sleep is not None: data[AZD_SLEEP] = sleep speed = self.get_speed() if speed is not None: data[AZD_SPEED] = speed speeds = self.get_speeds() if speeds is not None: data[AZD_SPEEDS] = speeds errors = self.get_errors() if len(errors) > 0: data[AZD_ERRORS] = errors master_zone = self.get_master_zone() if master_zone is not None: data[AZD_MASTER_ZONE] = master_zone modes = self.get_modes() if modes is not None: data[AZD_MODES] = modes temp_set = self.get_temp_set() if temp_set is not None: data[AZD_TEMP_SET] = temp_set temp_step = self.get_temp_step() if temp_step is not None: data[AZD_TEMP_STEP] = temp_step thermostat_battery = self.thermostat.get_battery() if thermostat_battery is not None: data[AZD_THERMOSTAT_BATTERY] = thermostat_battery thermostat_firmware = self.thermostat.get_firmware() if thermostat_firmware is not None: data[AZD_THERMOSTAT_FW] = thermostat_firmware thermostat_model = self.thermostat.get_model() if thermostat_model is not None: data[AZD_THERMOSTAT_MODEL] = thermostat_model thermostat_radio = self.thermostat.get_radio() if thermostat_radio is not None: data[AZD_THERMOSTAT_RADIO] = thermostat_radio thermostat_signal = self.thermostat.get_signal() if thermostat_signal is not None: data[AZD_THERMOSTAT_SIGNAL] = thermostat_signal battery_low = self.get_battery_low() if battery_low is not None: data[AZD_BATTERY_LOW] = battery_low return data def add_error(self, key: str, val: str) -> None: """Add zone error.""" _key = key.casefold() if _key == ERROR_ZONE and val not in self.errors: self.errors += [val] def fix_max_temp(self, max_temp: float) -> float: """Fix possibly bugged max temperatures.""" temp_unit = self.get_temp_unit() if temp_unit == TemperatureUnit.FAHRENHEIT: if max_temp >= API_BUG_MAX_TEMP_FAH: max_temp = max_temp / 10 elif max_temp <= API_BUG_MIN_TEMP_FAH: max_temp = max_temp * 10 if max_temp == 0.0: if temp_unit == TemperatureUnit.FAHRENHEIT: max_temp = DEFAULT_TEMP_MAX_FAHRENHEIT else: max_temp = DEFAULT_TEMP_MAX_CELSIUS return round(max_temp, 1) def fix_min_temp(self, min_temp: float) -> float: """Fix possibly bugged min temperatures.""" temp_unit = self.get_temp_unit() if temp_unit == TemperatureUnit.FAHRENHEIT: if min_temp <= API_BUG_MIN_TEMP_FAH: min_temp = min_temp * 10 if min_temp == 0.0: if temp_unit == TemperatureUnit.FAHRENHEIT: min_temp = DEFAULT_TEMP_MIN_FAHRENHEIT else: min_temp = DEFAULT_TEMP_MIN_CELSIUS return round(min_temp, 1) def get_abs_temp_max(self) -> float: """Return absolute max temp.""" temps = [ self.get_cool_temp_max(), self.get_heat_temp_max(), self.get_temp_max(), ] return max(list(temp for temp in temps if temp is not None)) def get_abs_temp_min(self) -> float: """Return absolute min temp.""" temps = [ self.get_cool_temp_min(), self.get_heat_temp_min(), self.get_temp_min(), ] return min(list(temp for temp in temps if temp is not None)) def get_action(self) -> OperationAction: """Return zone action.""" if self.get_on(): if self.get_demand(): mode = self.get_mode() if mode == OperationMode.COOLING: action = OperationAction.COOLING elif mode in [OperationMode.AUX_HEATING, OperationMode.HEATING]: action = OperationAction.HEATING elif mode == OperationMode.FAN: action = OperationAction.FAN elif mode == OperationMode.DRY: action = OperationAction.DRYING elif mode == OperationMode.AUTO: action = self.get_auto_mode() else: action = OperationAction.OFF else: action = OperationAction.IDLE else: action = OperationAction.OFF return action def get_air_demand(self) -> bool | None: """Return zone air demand.""" if self.air_demand is not None and self.is_stage_supported(AirzoneStages.Air): return self.air_demand return None def get_anti_freeze(self) -> bool | None: """Return zone anti freeze.""" return self.anti_freeze def get_auto_mode(self) -> OperationAction: """Return action from auto mode.""" temp_sp = self.get_temp_set() temp_min = self.get_temp_min() temp_max = self.get_temp_max() cool_sp = self.get_cool_temp_set() cool_max = self.get_cool_temp_max() cool_min = self.get_cool_temp_min() heat_sp = self.get_heat_temp_set() heat_max = self.get_heat_temp_max() heat_min = self.get_heat_temp_min() if ( cool_max is not None and cool_min is not None and heat_max is not None and heat_min is not None ): cool_match = cool_max == temp_max and cool_min == temp_min heat_match = heat_max == temp_max and heat_min == temp_min if cool_match and not heat_match: return OperationAction.COOLING if heat_match and not cool_match: return OperationAction.HEATING if cool_sp is not None and heat_sp is not None: cool_match = cool_sp == temp_sp heat_match = heat_sp == temp_sp if cool_match and not heat_match: return OperationAction.COOLING if heat_match and not cool_match: return OperationAction.HEATING return OperationAction.IDLE def get_available(self) -> bool: """Return availability.""" return self.available def get_battery_low(self) -> bool | None: """Return battery status.""" battery_low = self.thermostat.get_battery_low() if battery_low is not None: return battery_low if self.thermostat.get_radio(): return API_ERROR_LOW_BATTERY in self.errors return None def get_cold_angle(self) -> GrilleAngle | None: """Return zone cold angle.""" return self.cold_angle def get_cold_demand(self) -> bool | None: """Return zone cold demand.""" return self.cold_demand def get_cold_stage(self) -> AirzoneStages | None: """Return zone cold stage.""" return self.cold_stage def get_cold_stages(self) -> list[AirzoneStages] | None: """Return zone cold stages.""" if len(self.cold_stages) > 0: return self.cold_stages return None def get_cool_temp_max(self) -> float | None: """Return zone maximum cool temperature.""" if self.cool_temp_max is not None: return self.fix_max_temp(self.cool_temp_max) return None def get_cool_temp_min(self) -> float | None: """Return zone minimum cool temperature.""" if self.cool_temp_min is not None: return self.fix_min_temp(self.cool_temp_min) return None def get_cool_temp_set(self) -> float | None: """Return zone set cool temperature.""" if self.cool_temp_set: return round(self.cool_temp_set, 1) return None def get_demand(self) -> bool: """Return zone demand.""" return bool(self.air_demand) or bool(self.floor_demand) def get_double_set_point(self) -> bool: """Return zone double set point.""" if self.double_set_point_params: if self.double_set_point is not None: return self.double_set_point return OperationMode.AUTO in self.get_modes() return False def get_eco_adapt(self) -> EcoAdapt | None: """Return zone echo adapt.""" return self.eco_adapt def get_errors(self) -> list[str]: """Return zone errors.""" return self.errors def get_floor_demand(self) -> bool | None: """Return zone floor demand.""" if self.floor_demand is not None and self.is_stage_supported( AirzoneStages.Radiant ): return self.floor_demand return None def get_full_name(self) -> str: """Return full name.""" return f"Airzone [{self.get_system_zone_id()}] {self.get_name()}" def get_id(self) -> int: """Return zone ID.""" return self.id def get_heat_angle(self) -> GrilleAngle | None: """Return zone heat angle.""" return self.heat_angle def get_heat_demand(self) -> bool | None: """Return zone heat demand.""" return self.heat_demand def get_heat_stage(self) -> AirzoneStages | None: """Return zone heat stage.""" return self.heat_stage def get_heat_stages(self) -> list[AirzoneStages] | None: """Return zone heat stages.""" if len(self.heat_stages) > 0: return self.heat_stages return None def get_heat_temp_max(self) -> float | None: """Return zone maximum heat temperature.""" if self.heat_temp_max is not None: return self.fix_max_temp(self.heat_temp_max) return None def get_heat_temp_min(self) -> float | None: """Return zone minimum heat temperature.""" if self.heat_temp_min is not None: return self.fix_min_temp(self.heat_temp_min) return None def get_heat_temp_set(self) -> float | None: """Return zone set heat temperature.""" if self.heat_temp_set: return round(self.heat_temp_set, 1) return None def get_humidity(self) -> int | None: """Return zone humidity.""" if self.humidity is not None and self.humidity != 0: return self.humidity return None def get_master(self) -> bool: """Return zone master/slave.""" return self.master def get_master_zone(self) -> int | None: """Return corresponding master zone.""" return self.master_zone def get_mode(self) -> OperationMode: """Return zone mode.""" return self.mode def get_modes(self) -> list[OperationMode]: """Return zone modes.""" modes = self.modes if len(modes) == 0 and self.mode is not None: modes = [self.mode] if OperationMode.STOP not in modes: modes += [OperationMode.STOP] return modes def get_name(self) -> str: """Return zone name.""" return self.name def get_on(self) -> bool: """Return zone on/off.""" return self.on def get_problems(self) -> bool: """Return zone problems.""" return bool(self.errors) def get_sleep(self) -> SleepTimeout | None: """Return zone sleep time in minutes.""" return self.sleep def get_speed(self) -> int | None: """Return zone speed.""" return self.speed def get_speeds(self) -> list[int] | None: """Return zone speeds.""" if len(self.speeds) > 0: return self.speeds return None def get_system_id(self) -> int | None: """Return system ID.""" return self.system_id def get_system_zone_id(self) -> str: """Return combined system and zone ID.""" return self.system_zone_id def get_temp(self) -> float: """Return zone temperature.""" return round(self.temp, 2) def get_temp_max(self) -> float: """Return zone maximum temperature.""" return self.fix_max_temp(self.temp_max) def get_temp_min(self) -> float: """Return zone minimum temperature.""" return self.fix_min_temp(self.temp_min) def get_temp_set(self) -> float | None: """Return zone set temperature.""" if self.temp_set is not None: return round(self.temp_set, 1) return None def get_temp_step(self) -> float | None: """Return zone step temperature.""" if self.temp_step is not None: return round(self.temp_step, 1) return None def get_temp_unit(self) -> TemperatureUnit: """Return zone temperature unit.""" return self.temp_unit def is_stage_supported(self, stage: AirzoneStages) -> bool: """Check if Airzone Stage is supported.""" cold_stages = self.get_cold_stages() if cold_stages is not None: if stage in cold_stages: return True if len(cold_stages) == 0: return True heat_stages = self.get_heat_stages() if heat_stages is not None: if stage in heat_stages: return True if len(heat_stages) == 0: return True return cold_stages is None and heat_stages is None def set_available(self, available: bool) -> None: """Set availability.""" self.available = available def set_modes(self, modes: list[OperationMode]) -> None: """Set zone modes.""" self.modes = modes def set_param(self, key: str, value: Any) -> None: """Update zone parameter by key and value.""" if key == API_ANTI_FREEZE: self.anti_freeze = bool(value) elif key == API_COOL_SET_POINT: self.cool_temp_set = float(value) elif key == API_COLD_ANGLE: self.cold_angle = GrilleAngle(value) elif key == API_COLD_STAGE: self.cold_stage = AirzoneStages(value) elif key == API_ECO_ADAPT: self.eco_adapt = EcoAdapt(value) elif key == API_HEAT_ANGLE: self.heat_angle = GrilleAngle(value) elif key == API_HEAT_SET_POINT: self.heat_temp_set = float(value) elif key == API_HEAT_STAGE: self.heat_stage = AirzoneStages(value) elif key == API_MODE: self.mode = OperationMode(value) elif key == API_NAME: self.name = str(value) elif key == API_ON: self.on = bool(value) elif key == API_SET_POINT: self.temp_set = float(value) elif key == API_SLEEP: self.sleep = SleepTimeout(value) elif key == API_SPEED: self.speed = int(value) def validate_temp_set(self, temp_set: float, max_val: float | None) -> float | None: """Validate Zone temp set against its maximum value.""" if max_val is not None and max_val != 0.0: if temp_set <= max_val: return temp_set return None return temp_set aioairzone-1.0.0/docs/000077500000000000000000000000001477442374000146145ustar00rootroot00000000000000aioairzone-1.0.0/docs/airzone-local-api.pdf000066400000000000000000011063621477442374000206260ustar00rootroot00000000000000%PDF-1.7 %쏢 %%Invocation: gs -dNOPAUSE -sDEVICE=pdfwrite -sOUTPUTFILE=? -dBATCH ? 8 0 obj <> stream xXK B؋Q$zp٢)6-Y]i;׆=&Q#j> o>_~{H>vA$&J%%|5H2StGΞݧC죚˒xH85 ,:NGXr3Ӈ~j>R [:UNFvqB5@Fߢ)xJ4b!aKm`T[ůEkÜ]\ RQWBT"EN9p"ugw5:- GBF?wwA@#&h. kϕŀ=hq[h*'  rnƔ5dT> 7,nv*FӘ|-ON=M{d$ۮHW{2e_5ƁpBD`T`cfZjLgb" >&9 qQ ̈e!'TĴ} HHC<ۈJʊrLvhGGjn/YIF҄uE592RbAvTaQ}T*ts Bٮ"&"M:2r"&D (M!ZּĬXD=qtG 2 Sg$1%"IT1!p#V jD 9P~8hP2ewC: 2L%FӦOv"h?Z`,U,Mh65&`M4.)XCHi\B6A'vD"d^gSg-؝niv36)5jBk=֠ eЦUACR@kK;RQ=F :"blm)j#lmc\r> A'cdjUd8zF T@flYB XPjҝ>v x)-)bIv#1CvǤ5 B#DEH1ŘaEdvZRuzGAn{l+ˠsѧ)0rDWND^o_T  UCe`y_{/9^<~׷__=؛Si_[4`gPp`_|^?җ8 xX;>eU8J:tM}*ˆzG'%mCdo>eF{ڹ氯gX?.j@SWA쐌VğS,!G=M> stream xX=oG:7]w6DLF``dwr>;]?<2#(vnvy|-"o99*n(Y|Y.Iު_A j=HJlU(Fuq~V6GN~XboVxDpYyswx$~OQyPT_ 2Z-?<-VOuIY}tCc1ޒtMSd }~]jhRu>,rJ#]\)>WVL'S;Y<6,×eScAH@ 2~=OCP!Uڬ^6GvY@ſiCT7ׄ8%L\y1,3VX> >VEK0p"v>OM?z" *^a_B?D}AYRcG ݰK\_:|F؄e siFig1KXR_oE C:N:(| y ߆@aσ?Ck_ 6}&n!6KĘHd. T$Q2N= >m,QWr}\.|y)p1ף4o^¸AR=TG&З7VBw|.Z|Y =S"Hs=wޠq 0h{TPrX0¸Uۦ H`;?FN r>5= ϢIZaeX_p\hrPBXq'uR?:endstream endobj 55 0 obj 923 endobj 95 0 obj <> stream xX[w~ן{r˾8e[ AƶK_gFΞpTUu&j_O^\FnDf| :&r>g8uuX+oOߓ $/.d-ĶW:قDĨh tf>wv>vwdvӰ>b8;kۇj*m-d|WIWGoov?TY3Al Yo Q?VW,tTSU  avdJT1ABH#2țZIZQBhфP-AW??씵,4-lJV*wJ[(UJkH+vXϳ< adTBI뺒8`ğ;1] '5\[J@[&!!A:3Lwfڜ~|x"3g2&WūӓT%MQ)q' URx޲n-²݌ElQEwHg:OÇmyX$nՎl0xuEea<#C0Ree٭ni#9}ΘB25l&r"[h@iD @oB~D21Z C-ȘFh DL7ron`O .[Py\uP%p}Îj)wh{rˮ+m٭BY˗l1x;KXw=;X`E ӣT4|!%IX ȂfbE@xPk> dEAѻhꝼڢ[LYܰ]M.tR^Yz|pGU FޫЅ;ʌCRwJbd캿ڕ]Ն-W77(M}RCf>QDP=&g}^54 s~ݤJir*Օk&1XDm9ph4O.\QW_*mϟ19 Kl-T4B2r͇WvlXB?Z"|" /lItǜ Hvl€|S٧@ <WI^ gC4Z(Tl٤]HC>Pc)401 hDS|C2 F8d k鬅74ٵsf8,IMm5 As珖d;WNHرO>׉v؜x, F\e`G\cH6qh`\-W kt(pׁPri\G\#W5 ![ טV*FW5rz䯎rk0\cZ \׀"Qrk0\cZ \׀`@)W(GC5JUp}~Y 4bM"Lt@%e?2luC6$H8Gu>=-!uS}Zwkoi. vWխoYر&ʗS+[ޱ{O/yª0^l0C4> stream xXRG}Od2sIʼn0ˮ<i1Jt;H>8gvW+a\ZP>}zL iN'ѳ =f2hW#<dTLRyh1s,p6~|"XVOl.JF|>bTieSrO<|1z^R~vWRWOoyXmRs[]^WfBR+yb( |ZjEx5excWld9: vwQau+)Co"c0Ox&l,S )C7Ue!-Re A d`>(!Dž E^IV*"ʹN'DŽ/ ZǛ* ZٱZZdh0"{ H5m"VJ $Zh"ׇD׹ÁP%SR{;KS!p?-R a1$oؓe\vFYF)t-E$L@FrvW2c=&2*(i<UW7>gƎt-^uOxcDi-&Bɠ{⼢yuࠢbݩI/|p84 KLq}^ =ue[endstream endobj 128 0 obj 1732 endobj 137 0 obj <> stream xśKoE\9V>ؠ$Qb!1 `;)o\ʲ}ȯ9S~ZfkjO;6n7=T?fzuW,<ͱ{[ Nw/_/t =<.v ]H4Dб?ŧ5E+麃C@c?3;4$9>q^j坸{'sJZ콷^*{ooJ*/{ 4|}g-#fDw7;ZpzgЊ'WRˬM Sc)X tO>~ghNAt:c\_hZQI:WP)]wE>:qk2~Z\l e#~uX6%aCFN(aw= 'T“OMB힯WC9Ji^_|JPTn/Kp5I!Sڨ:,pIIjIAMԛMb 2 ڤ$ 2qj|_VF ZQM/]2qo6^cA[_.)[;QmA\:j#ЄVCjbw:B>\؇STNh8_J^|;r^:+{ʐ 礬xtq헯v'Z)%_o ~tt &׫Ӄ'a^Z}-KVlOYG{_zhw' |qݳPn\젘t!a@F })%ᳱ$XR:֒|N#_?n'gHT!@|0Ka4{Mc`\"mwIW#ų3 iAjĨ`iF1~FHWfT FIicQ#0]p,I:?Pp|9UJd((Eb"01_"7jcũˤ(W|EE"_"_DF1SQQmbTc8U|ˆk/NU_U}qSml5n8&_jcU}tJd((Eb"01_"7jcũ{U"3_D/F/QQmTEaTcƘ/NU_dgU"3_D/F/QQmTEaTcƘ/NU_4U"3_D/F/QQmTEaTcƘ/NU_J)U"3_D/F/QQmTEaTcƘ/N_nҤU#DU_&T5Q kF_UpM8&_3rIFf"_"_D/F/ ũ(rè6|11_t5,Jd((Eb"01_"7jcũK`dU#DU_&T5Q kF_UpM8&_3hk lUdM 2U21MibLT21M)&1칠"1EE"'jcL) Ũ6tq`\٪ ɚrUR51d2$ebĘj2$dbS41M bLj&Df"E"GD$F% ũj(¨6d11]*,|Tp_U}qSDU7jcU|MTué66T|ͨ|+ϯ="o1oEϷEϷgYjc-Y{Ũ6ƞo1jڜ-sji֝ Zgm_jBp|6'Sp7B6D5nm6GK nstA 9Z8aU&Ύ 7XFрܑx|;>}YÁ0JeyN 2#P10/ua46K~LJ'vGg5,.糂]rZ>s0Wr: =>"Jӥ38B,Vjn6BtmoO0"6(|]D<:n߶\s>Kb<턌SjxB_3$45xNj!9z'oǚk[ +|!x 6E]qqwPrl Zjh&JY0RKOMmla%N0ц1a7pxd7H ^SǕTt訇>L[{MRPL1pRr|EGW@։8P]':LKula_MA /5 AWU"{ͯMNC > stream xKs7XS%GvNOUeR9HEhnr2)/L^E&b2]]J>J)vJ_(b_\A jRRkC0L%vUj-P&喵pVhabi/b+L\Gj]E sU14=z#Kە igeveb[ (#3PjBTETIU,@@Q"F@2A(T%Srt"Dǘi*. "Y:.LF431j!"MW5Tu:TtUA%@'4,bU-c%*jh4kBP8 SLDf"fB4HCN,BMLh&I#CiZ:jxYIΈ5LD4r55A 稅Z*\dfEelEE fMtTNH@U U-cU5P]UP,A$XEU("A͚A`j Z:(jpYI 5PD4W^BVt}h\ԱI8e#ssƆ 4/ _ⳆX8:䵒>Oo D àHkp$m? jg~5e6eOW\)mD„.P~͍HB[;ux.l)l(#ז6ۯAS9'bLvcΰip0bpS8ʯO?+)f8vA]V!dx|^ȏ-;=X%\}PJ##.?i_L?=H 4ynQ: cx FPڛe2}Lr IXN.olď6Vߗd~ ć006 HCZ`>PI;08y;7%"vhdc}קVUKÏ{۶Du3m /jG&~]TLjMz+Vn'0WBq~o\d/~_q(eP,(b Cˀ "_/LV8U/!c'l!7uGVmmgY/U}@btKh.0ۘ ;+>q37Ȗ0bu:YNqz,+'=N]z $fL0) ɪ^HPV0__3~u>OwͩT?vQ Lcȥ*&2-r*kEtȞ) 1XkC$3.ծ[\{T?O] d^e'x`/v<ҵWe>t![y{v[L!Oy1gzz Y 7(|HeOr,}Y<`G뗛ݯz?&"\{d7xggV5Ǜ X!Σ20񏘻%]ZڠkŸTN\I\(_ Lo$|m?jbO< tǗ5'O^9q)d[W:Xqn: c^^ll-yvx$W "wRK'bv1p ~4] [)0^ @?vc. S3DɯC'_R4ٯp _%~!.ˆ]>j%1EϚMoM߯~'_O:"z(=_O3oW4a_endstream endobj 147 0 obj 2222 endobj 152 0 obj <> stream x՚KQEd9|?4f=Rɪ|Jz,,:Ǘ7oC'}[<{{\r'_"=d\gB7 ʇЉ0Hg|w[0-].~ BزL{i e쌑vP-E.^M\IMygj0>\WfbjBF%v {5OMaΡRB XI* *`,j5"V#+êz,m1 e*,PXՔaQWU] uZE]Va\VaQWUXԕ`kɰCVf" j5ZF#,ʰ)Ky#+,PXՔaQWU] uZE]Va\VaQWUXԕ`kɰrQb**Z"V#,j52jJ'B0ULqXUWXju5E\F`WUM $X2ĭ)+,PXՔaQWU] uZE]Va\VaQWUXԕ`kɰ$jI鐅b**Z"V#,j52jJ'NY 2,ʰ+V**,j ,b`<b eVzʖBz )Li$ˆx!T=DTOS,q䊝[18ROϳz=[J^=ZϞRWO#Rz3H^=D5z:] ]H]q<^q WN@vJeq=%&iؓeOY #0&M POpCVM/QI-eb}|oY,Cwyq*[moף@ ߮W7}(6Oþ_``{Ǒ iCv=^ן1LeίqJwFAĸ׃f=gg+ zڟ-05YkWG0(?*%LAuR=mf?6̍l}=raSFM19⣵X~a0QBݩ}`>KkU;6]1ښxm^Zwnibe=~u߱}B:6כS F ixRh0{Dsn&SK)1O)s21o1Tb#2YE\<Xyy{4*tt0ܖ6fqsF 6XrQEʢ.Oi~s*Ğ#p^84X=dW1ZpcB4rNqjbzFc?JHql1O}7XyW)CrB`A?,a_<{ً˱5=X4z is.vd#Cˮ׻ո =::pDW1WΌ:ҰB hX3}V䋺FWVC@"qRd>lW,>gq`> "B8w$y#v+aP%)DZd#˸NUvz?#Y6WmJ3>M3'LjeXwO06 vc9''i,zpR2¹d4\~D&KcH6e),cw$DAEr*.8n lѬxD5q&pڎ's:q3rI iJVWooޤ7>LkdP>RsC4`ba{7]t8NNx/t۞H8Pܜk:qq ?\Qj WM5rzdMOG~4 Qc #bbe#JItL >h4qcww9Ό_Zz>UjDolC )+bvYqXZ8Dq"zJ2ʎkz#Kv&}o@Χ 0g/g; qol-endstream endobj 153 0 obj 2362 endobj 158 0 obj <> stream x[[ /ki&-ФJ61ڢ3<<l若JJTSD_UzoT( &WU%%FjJJT7BoKC_Uz*)Q6Bߨ VtJ_, }UURm&D}*pn}4UUIz諪Uo#jACbU3aMgpẄ́H4in4ٻ hhV 󤩀3f'M50SMaAPWUe$Fj"IT7B&WU%%FjJJT7^ySdj"67B?&Rm3=U=C7%!uxp0fPVߚ 1钼Ɯ)9 vg1je9p"e3FPnYa,]\Kw90/|3g[/ֻlV |۝$)o |mh`K\х8 {BKl>W q4AR:o^Qm%#p/T}}z׽a,9OaEDji|E0p.a@N_4_a iVS|9@ z/U7 Ɏ8Mq<`2I֕<1Ex\&CîD*?џl!F3_<#W8>(g1vo#aoP ϼB"a'?%{28ݭ O?,zPp=n" \{5!B(b?y" ^PZF@[v<%LʃWz780}D%B#|?(XM x?*/ R+x<*u ?'TUcC թ)!üQjtV0+rpW?n{ fNZ]PvzZ|ԺoBap5<\RFoxd3nChZS F|Osl'B2ltvl/IJAY:Qq\lݤGal #XfR)r}CptL<0+OGᢵSY1[$|K{Fj۝J8%tsbr!fʕ~f@z]u|"?]Z;ԓ|#8X8WqpNqbXDtlJũ:şO$A)(u}\loܲM^zeiWD{SW ?IRd>O)k֟.;a  +Էrw&W}w,/B 7x, q3lA.nPV z ˛^zɞQX7S%%!x5|+&{1RiX7qq=7駅)+cxeufed-[\1lN/شl:5/yr?bS=ٟs#4ƙph*b#h-lٌ kbۙ;Bk Z88Wx4jȱFH|Wjub϶ô>+vÎn8pdآ ɽE<"$ Y|2 GT  `QcHbȳ|C:]wuwA u/ &"Lٝaч=4zo:JFFNRN˰䰧 W pzU[2Mȉ i^;tBEb@|=r7yﴩE72K ed' 8@g +uz> stream xݙϒBhovTlLR49bD.%Sȕ͇ 3M|R:l~FIALCYfFEDt62V7 J1BypEВTY|9fbm#k OǻsvY_Zēw[97c)q?dRv?2 Q[vjS*|} 7CAC #>Cϟ0U@d!>RPoyw8|xA}}~}kKVKÇ[>?)c4?f~2!HiUoqӳRۧ)2nDF`F??>a?Ajn;;ݰP"ÅX~'5!-}py-Ix7=u#9GѼ<6/fCҏ fLU}}.RxC}@OW= F@Y;4)f4oIJDTu*t ffU ۡQ^L##g/bg裤O^4#c*I0U̓fD6Ѣ|<jI4RתR 36תmbU>X'b0/_ AkVWT; ~0W?FZUji+Uw~0W?FZUji+U.*0/_ AkVWTx5`QT~Rja+VWCkQ|JU'Z R-m|*K>B>i xьfn'T1OKԵϐGIA4hFJ37Uȓfa'͓gRc"9cT9o9А:6Do%w)cJSwa*E3QttE&i߿rg_҄ͩ  mwܷ)ՖN]N&#Y<~:z_Xo!u;(o7M $|3X&ؤDE[m'ۄq S~QRM6HHߌ==.MoR1oTxR@USGz! ӪwЕw*8̮_Z{9r:mлV{}">T_8G>=MPv fey6$NS0RZzMih y 4a~ڀwIRJ0hc>+NJ;'Cpw4+c+Ž7,ktuΏ7w^V-XNc;2ݦڏ*uZ4^ATzwTrhRU: N-I!=~|'.OQGV;|Lňے⻻|q4@[Y=kBe5nvTk1b)tڧ3y}LS9n`A>*X1.V ky>yV&d;},W%a8^stHOҧOpiqs-=:+IKd?E@Ds稃RίN'\HW#0i¢Rz ΐlPnp{FU8Lv8-g> stream xVn7}ߟ(J"$">Ȓ^9]+!wuE=h˙3s̐Whɲ9yx5uYjhx1&Cی6֋v EEC 1ZAYsIF2m^ P8yI,&̺pX{^xG@ VD+/g폿=U?p3ڃ=jڐ,gV2G3p:YNNk r:V+Pvb 9t.\Oż7l*%'u#rObBN1$ĶDJu:ގBEMϽʚn`>-:MbK/K@99OF7l.5!v^ުQ9G飄YC}=_?`!vsFҠ OT 5) 4QAZclcf9OHxNB5N. rjbLrRsAPRnA|&J׊C )0$ *VR-^BXcgɖJf9"҂im9a#N_߫T)b?i{`(t7#ew=dJ YꝢn.7b]XP8/jʑKCMg3! "*O7|2+q;O+g-l d'>]{A4HvOQB]֣7+EE>R l%j("3hȆ|u9ozz;HNT# GCW ]N<d1_ϞSodW HZ򼐉:0[F=R<ה> stream xM%{F{ s+ق8m19ȁJLzkE:ؗ gLhW#0`owW=]=U^2AO/={~\ f&_/d"=\Ag#luIC赖L^ybV^|Z|N7#OwL.zVzBQ2k$qw?ޏ>{SzrM&aL?CJ"pi%Ǘ3}TD(H4'79 ojʧt}[^H"߰uXn)2|s=ʅӒz; q+sKԖu]"*~4$D!n yyT4]vhrJTFN>  #[}X}?ATb?`wN~5}'% A 42=9",‚rطAףQG~]Wv#vY~z~MEA,-(g\O5~uLƱB:Cyxr1E#t#EG|t a&$|38 ,v=;2'lR@{ԑI^tBGc2qexb譶h*XhkWY FjUp b|tbkf&h1}Ӊ2+že,VEUX5ª FUXUaը \ZVY5j43Sê(k˖ULXUaը *UaUTU**siU3[eՈfʪL vJ ջ d*ªQVEUX5ª FUXUUҪfʪLUVD_e 0!5TNYS0UM5RFEq4⩊L鍫IMd6&)̦d6E4̦jV356dj)lRٴd6R,:EU4§ FUUAԨ DZV15j43S(H\*Y&jTUQV*ªQVUjD3SehfU;QbVҲ̪UeVUY̪2VY5e V43Vff&eV﷡eª FUXUaը *UaUUK*F43UVffjX%VkʖUUeV*̪UeVUY̪Q%.VXj53SeՊj+Bw"P1kt>z] ʌ C'0GuIXj@s0ơt[f@]ҙ 4E"f;mӚNrc&U-Q\h, }w$Zѳts<^U#5^-JsZi[?m1m9[ k@sE Md38>RCeD!\ ǟ?Z s]%0s&H0ŎwLaaGPxQ*OAࡑU$P{ERD  _ CS[>i.ࣥ'ѿ9bb)z4ouٷ~Q C30A#uݱ*qyh]Տ&[6AJE4iǞ..7WC+ 7W J!\$M)r(ׂ}(sjv-SjE!ٝofSAz>JDebK}Pq>'i8A ˥6q=*Fa)1tA[`, SJ9 Ze Fez-rxAc/u T*DyRU@18G O1I";YC!K$BT0*Ѿ;Kξcf]ήvp,%dyk yBq):Tę1%\ո;5(Y3橻,g*xqOJy44@q kԂ:qi?opXˏYNwK8z?Q!Kǯx#"3=,B@i%ݑ^E34B~LT\mζ7͈N}]bg7!|Ɇ7+eMphj}5t~m,%8ζ?ljϗzn@Js$[/iqّ8X94Gt%b~؝q=߼-LgK :cor^*ջN+޹{p=ƢxP Q')Y#x37nܷD)Bu1&.&:>-\~ޓwb=ËC2"G"޳B)fpqgXvPEGui߽1<{%iʜ>VVMOgɲ[G{Gc!hBƒUSL;f_,endstream endobj 178 0 obj 2619 endobj 183 0 obj <> stream xYɮ7Wp t#^8JB9hl|ok`ݢ<xaŠ`Ǯ"`xGvi$F9Y@ݷCW1胪#YRРlL)ʒYJdÔY}IIzΎ3Ye;=QPD՜B?jB1`IzwR [Ia'yAm<$OIٺ6Ѥ|ir)ab&Y%\94ߍ%,elqɪ1~UaFh V+ˮ(qTS>qʙSAD7,!zTЬP&H3<" хU$VݔUaD9aج&^)yD,VJX5FSVUcxJ;~!~D %#)ˆxªY<%M-;gY/"j # f47Y/"j # f4,Y/"j # f4WF 8Gd!Pª1"*'S($` g$jl`U,j_&Q.dϋ js 0+?yUO#+ʚ2eO&} gcv)).] LP̆4fڮ 4u譼&[[7 1K30 _w}t=5haiMoͯ_y+5OXԋHTGqC>>ʠR0J\ x|S! T:5)DκPF&|c9 glfa4):aiHan٪ڝn("_C&HC$R8o[X9|ZGsp%#l.wЪ\mǛټ?7vcDUiK`/;l8c}{@?s(D E/+O |`P e]f>;GfPoVj?<ݴJ < (BS#|I|D4^;A!<#/iViq󑘰p0wls\^_Tynu㭰OgxpF8.39$} _lʆT8'η钼s~DFwʺš$#a{9vi1eM^"r/4T:Y+t`{aguTOc|׫b]~Sq6_8Ӛa($ B{tN'T))e('V0~$T!>)y. $hQ6!5wTQʰmSꣿΘ3鱰I2}KС !B(V?{ 7teotַ_endstream endobj 184 0 obj 1878 endobj 188 0 obj <> stream xYnG wN V.[h؀m IɌ)g' I^4!YNgZ=]Uޫ3)[R`u7/3ŞuwUye}.]UvV$tp\kf{xE7A9KaYضap*9fdby׉_:_|߷)`xB`y )[uVO?.\ bWRt̫cOY=ӪD A>by/#R^(ߝdA$U}ܯ5il"$?w O%O#϶ʓ񭇕6p'AK< ګKO['%1%\,xuD"gSwq* +Jk/')l6 J1[L>}w68!kd"#:l6- Q  im+ 3 y?ͫ?d[WE)"3|<Pwd/*BN& ER,Ș ?`{I>:@\Q4NؕK"<+BgȺw6{7ʘU˸4e&$8dy|\I9.&&2*Z\w25a]@tcc eu&#O*H鐰l@M捔$*tŽ)iY!0a]!MU*);'8J3ʛ/ axaēcDd ؔ$ՠj|rZyrTIzImIM,q,)aĖ?naj*'φUZLAр51c UeSEY8*RnЉ?[`, \,YEDB4|EVjHMk,]!z]?,w)JJkR'|ܹF 3(g[Cέݠ. sV{dI}5M~ySaIGaro}/e9*ob|Z;@:i:ag(^3=ksaMP@_ڳ&*7?5Q\a6 ZmI0ksme3r>sAI'xTHs=1>t&;Y]ڕh?]^2^ "T]SZu2xzDXyVV&ɋ"6")h1^yՁ;`u' endstream endobj 189 0 obj 1729 endobj 4 0 obj <> /Contents 8 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 36 0 obj <> /Annots[79 0 R 80 0 R 81 0 R 82 0 R 83 0 R 84 0 R 85 0 R 86 0 R 87 0 R 88 0 R]/Contents 54 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 38 0 obj <> /Annots[111 0 R 112 0 R 113 0 R 114 0 R 115 0 R 116 0 R 117 0 R 118 0 R 119 0 R 120 0 R]/Contents 95 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 41 0 obj <> /Annots[131 0 R 132 0 R]/Contents 127 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 44 0 obj <> /Contents 137 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 145 0 obj <> /Contents 146 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 151 0 obj <> /Contents 152 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 157 0 obj <> /Contents 158 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 162 0 obj <> /Contents 163 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 46 0 obj <> /Annots[172 0 R]/Contents 168 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 48 0 obj <> /Contents 177 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 182 0 obj <> /Contents 183 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 50 0 obj <> /Annots[190 0 R 191 0 R]/Contents 188 0 R /CropBox [0 0 419.528 595.276] /BleedBox [0 0 419.528 595.276] /TrimBox [0 0 419.528 595.276] /ArtBox [0 0 419.528 595.276] >> endobj 3 0 obj << /Type /Pages /Kids [ 4 0 R 36 0 R 38 0 R 41 0 R 44 0 R 145 0 R 151 0 R 157 0 R 162 0 R 46 0 R 48 0 R 182 0 R 50 0 R ] /Count 13 >> endobj 5 0 obj << /Count 2 /First 6 0 R /Last 37 0 R >> endobj 1 0 obj <> endobj 10 0 obj <>endobj 12 0 obj [/Separation /PANTONE#20327#20U#201 /DeviceCMYK 11 0 R]endobj 13 0 obj <>endobj 14 0 obj [/Indexed /DeviceRGB 1 (\377\377\377\000\000\000)]endobj 17 0 obj <>endobj 21 0 obj [/Separation /PANTONE#20327#20U /DeviceCMYK 20 0 R]endobj 31 0 obj <>endobj 32 0 obj <> endobj 33 0 obj <> endobj 34 0 obj <> endobj 30 0 obj <>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYa" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?j( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ZJвҮ$=@^}=լsnbxU A@Q@Q@Vv 5vmT&}6(׫<,~b3袊((((((((((((((((p@ ූąO 3K2ַ@Jr$cֻM>sb<^¥)cW6ossf[7ӠƻM&}>X6m\crt'W!+X$Y#nC!5JGO>2m`sw.mu 2R˼d2 pǃKf~|> QHU8[s}:T:<IgqB%8.Q$AAI1I-wo 0HoSXGn/;ENGB=I 4Oj7eA=נ4UcoC44J 34MSVI摿߆jdQ@=A%bI_O>֣ҭ&P0ѓA{ %;dr5:`mF7?A[GIoP[ լ&ghNIFͶB%s$,R{NQSf{e_cZ5ƵvQƶ2u88|\GkYfSySٽGd gyy#Cr:Y>µ-(=KRzP%Ķ' p8 } X?2g @;d AY)\ busy$reN3Z6VVq1?{ڴmu;)mu;;EP0@I8 L%3l)ϧq@ʈ]*%GH0_ߵڠQsÏҴA){Q KHƽ(Ƶ_ vCxpC`gv8wk=UM#rp8ܧGAsRdHCbJPǺ7T:ppxwQn =R(((((((((r;aFMkWr@n^H{ f5u.Zo{xsO }%p]:֕m~1NƃU2"ѭ/%eeoUHLoa&nWΣ)-d+NP|R,pSATIcO$Ln eVF-XEC17׺5WXQE?ι 1?OjF{YF#Jϵ4:|LG2g S["?–=BkiÔu\k$e+  bo,,  HaSsp:t_@)ɧeb9Xb5?*T]}˙׃SsR6<2O$HKU8g-4d-A,wwQHb_M,Χ/ J#QE,*O@=jΥkY1GPvf'8CiF'rA rzdKyƚmm!h6*0UsPiAU_O*Y$̢KonshevkVg $( q۞+;I4˨X'YFxNTnp:Uk+=7SWVytr[Fi#(I-/5ϓ78ݑ:{ i0Mt>ZՏcpz MH[E4ϩ=ϹfG-Ƥہ*g+i+V?[0+Q/D@Q@T7W0Zsu" o:T{[ȄǺP{hs:[wFSxW{^^\p9#'' 4QEQEQEQEQEQEQEQVl漝aK;tg?JZnwL#wl|5B6j*y=0}ӵ[D g$"UBFϱU?²Ӭ[v#??a5D<j$dBe=Uky7D<5i͹=ދ ֗=C'Csb8Y[RkH⺂XnѪUWV8E}k=H)؉rm#\g֟go=݇RYUJ pp84ao<9d ##M&82\HnU1rsT/Ϫ{vmnO$T@*g昶4.7MJovn$:䚞ۉe ;V5ڻ˪͵#>4>fP֐}s+e>ee@Lzi>=(6~{34l,+*gl rG@}NEt6}gDTPj (Q@$Jh 3P:k|kiIogk"|[#ֿkSo6™;O<}x|:j}q"S\ӂZ_[n<|¹؀aPׁe@Q@Q@Q@Q@Q@Q@Q@IO+b5-*:y5%0=9ɠh50 tQYr-Kul,dP0=znPZD:68 >k!-,g!Ƞ攥ԃOԭp8/l# F2 UѭȜr%~j:>dS.%'}E.m9>էG8uSͩN0SS"noK#/|n?3ZA{!Q}OzcmHIx#?%7V?0_"@qp:ƝPN؞Úo^+ۈ)'v q#^j@ V+ 7 /TLdAO25+gDvC~};T Yh6:ǰrK3v{(j_^"Ef^Bջi{MprN Ow(ؤ-'܄ GNsw9-,^KltfO&Q!b!AվSkAyYnGIeGE:QAAEPPQEQEU diRwlG%C ?#](I$kf8kenoxs!oڀ<ĺ]rjdI TbII㟯Rן gMB++#f6-eLQEQEQEQEQEQEQESY*N$ֶXyl4p촭2@n's"Di~[sw;e]m_aҺ`,z\ chW>y\'N@hm"M4\V=+\G1~"(86W9JG3e=Mَ.xga YiXnO=UԴgϜc'Uu9mkS j61U3Lo]چ+`a%F.Aޫ G2hO~b?^SSYnK"w;}MwW뉵y~{  ")*$}qҙ&uze1䁴qH.Bl%bV2]>[)rg$/⢷Gq/umM:AԐ^%1[V奝e-XՏShأq"ƃ(((ּWi e1On@ąR@U$=Mq, \ J~puhO*@ݣgj:ltXcDr8s>?μqQEQEQEQEQEQEz׆X*G ]džK")|cѽ:'"Տ AoVAiyFǧJX&TW YZ23y;v)R {w\tPcLVM-Eۅ?zs[P#i\'فc?ϳdtǒ< Cl|}8U4Ցa\$~ʽ?ԧW F\vt¨qV PUD}&{ ׭s6IK LKP 4^cw涷1N HC3V'엚nz[ۈ$RI6*ptךt2]Je&yH$~?@=Ay'OWm@nqSiSLw"ɃBՏFm4dd*1Gw4{G2SaW(P((- c9@VvhЗUF9f? !Xp=z`WO-443@VD<6`[0s?r$I$zIEQEQE5̖1B@3I桢6WdᏩZ((((((+ucdοeom +xOE[B5Y mV'=H09nªj6BF\)ܧϸqVIZPW,CMY׶iyfD;>Q{C^Iqq}Ge*cH3G3?ǥD"$3[C(avր/{ElaT?*}bӐcW/PCB(#X^T4A )}m[k$50*օ# AEPXQEQER3*Vni;"@r ;r{8N1^sxKo= T e^0zb(|aiqF;3.pgAy^Tm p`1Zt 66&һx!`!z".k@M[ce?t7/gXެV6]^y-db@s=}hGdNH{V. #8{9?=?{FDwRh Vħi$$]a͟U*?l ``qjޭvYdvS>(4$KyZljzddg L.>Mc=ZG`(((K*CI+E$5M##˖JvR#\\o1f ^X\QkAd #2'x!\ YX[$csXzP`SGBEQEQEQE,q*F*O@E^ ǥXG,3W$}Oq@k&I&v3@;ϮWX$o5\M)@Tr1k(ևA&dXq.a܁Y4QEQEQEu귦7YG?u?E'[3zVh67ܳzAZ]8(({{2p?-uL'̗ gAsںZw{ UbZY?œAO(mŔ[ `^1ӆP][2} g&YF&9[xesn?-b'[UUSlgXrI{Pe`H*FA'ؿf9%@n}߹5DIl=Fpn8h"T+K+uȆIfCb#[:AnA; &y7CrүZiW%f):yv"$3{ X{3zgUM ҋ-?pzKK8,=KRzS8!UF*Zi}yOsAji00`B:==JpЮ![98$nx}zq\>4JQv.-}zMt׌nŰ0!Q7,/8%rYKԓTPEPEPEPEPEP]|cwi2Cg H'+45Ե;FGbCXAYQ@YnorB*# REzHFgC+)bq2 ( (&KwI+QIv,"4{8@ 1'ܚ=m|-$vQEQER;h#*"1{l LI,ecTw5]-}@t-ew<ޤ7wNqt7R/OӞ=m#W9g8U((eWR)4P =OH$z]ϵ_($I ԚKhE̅Dיk+u.`t6rץvZmP\!W}5ڍ֣1wv?\:EQEQEQVlg&Yr' nPq~N s##4$ޡShP6@QEQEQE薞 A2a$`szW׫ QlS">k9 !xPs(1%m7AE7q7y(,]*\ʌ' \ zuKm;‘-+Aw eTQG< EEzӭ4 \ a#F?J((OCP1WW5-܁Fnր ( N&7{C}O),A=KQ2HGc5EQEQEQEdx]A3II.{wqq -5ĩK՜WXG\(q,Airޟks}bCa!r2p=CŖsV$u!w]c%# 1u+_VB 19zZl"mZkfXb7  N}s: :[xyFr\n9s@>Lo-bH29 ?PMs>/Z w+}YOSXWE3͐3'~URQ@Š(((neif`O&,Nw;ם׮ ktD1 3888Ű^& ^HTG}x<>$]cEhmȧ!9a29DZ5SY*NIr,WHI:H-\rXZs`>N/*-a̷dS_Jm~LV7yt袊(/65W=>{UӚ^! Ü}wu#dװo ]MḲSg,xQEKš'Q3g%NIhײ}!%  0h((5Y^bƘ0 ߉?εh(sj{[W'5~տihQEQE50Hˏ«>"\Ik[I2Id?{דEP|7n~iD!G$I-_[Zgu)9U*bP'fEpJƯ\%aPGqǔzdu>-l[7X ?3>^0qҬA@!qr0CC_[8#8玙%t,!Dq#c*q߮3@~$\v-Fcqf-:L~5QE!Q@Q@Q@Q@Q@Q@Q@{1!K#Kv=^w:O[Am8>R1%}ߞz7V;Uh%XYJykNn5"Ҫӕe$r@UB:\-3m~uBėv`.70jp2yj&쏭gEQEwLXZ5tA@Q@TBAdP; &hҼmL0 QEQEs;?Gj<F-xz׏q]ͺ8p0?Bkh(ΆoqKe)Uޏdg@0}t2-Y'y.Cn;@:yE KR;bKtǾ=\P0((((((((o &ZL؍ؖ#\ciV1K4L5 r0yACK$S(1 PkkgZڶ4\4#FLlu灚Ơ((((ugzѬȻ׬(QVn o?jEP-?Q@Q@`'׷Չ#|P!d AAů(((((((((((^ngO*k(@=:x䫪<%,-2XiVA+:cszph9MP+失ElK0%r@oN}+((((ugzѬȻ׬(QU'<1*?_v((7W2ddF?y]zgĈ].!p,יEPEPEPEPEPEPEPEPEPEPEP]^j+蠊4js*/9'pr1"Jou_ R5\E>2x`=X:rW3Z.7qyP ?2delU_mfHb3maqpqqۭsQEQEQEQEwLXZ5tA@Q@qϧ5SKFK0]@3Њq+ʀ&(("ݷ}U[ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (=.+FF- [vl AۊР('Ȼm_K=y]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE{/?Pf "C5EP?meэWQT4vaΘF QEQEp.U4+R̖c =ּƽM#7^w@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@a–h~=N]q >$\c@Q@ :mѭZC:L/oc7y ]((~+t>uiIJ(((((((((((((((((_CܷR]W}@Q@Rg69Xn 5vտihEQEKhQ)+@;1^y]ß=-(((((((((((((((((H8 #!svO+|{%FVDgU..g^ Bۣn>( gn`bۼOWV>ܟH/x rPQ@Q@Y4Gۂmז +~'Fm5 q*ۯ,;6(((((((((((((((((4w %Ȑ ~:a@!cr-039{:޺:GW_xM1pA{jj Lܬ0S봶!izmN$QѐږI\kp4v((T9d`WD^=y{!CۂzƑ@2~ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (.iޝr.,xewi~9{GԣT\xԠAwxk-Fv"Wv1i#8+>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]Y a" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz?j((((((((ݡ[ݹ'W"[_YT;nO^1K饏fU?Jz<:rEVu5دIVQq9QEQEQEQEQEQEQEQEQEQZd'iS ;#0*#VuS4YIZ|P*C*RqqI} ^ɲTrsj(((((((((ܡ8ᦔAsV< T#[ʿSVs%id=JJ98<%H-~)kI{50mjxF-CݿeEQEQEQEQEQEQEQEQW,o%rz< _O\DOߎwwsV}jªW4c2bSS{aXkk1$y)^J\j2$:\.Gϭ^DCZ瞤$uEQEQEQEQEQEQEQEU[I&[9A}]X؂g^N:Eڟx>^Pj>(6}Tm溙a呺* Z^MAe6ڟuU+ɯ4QEPvsɲmf' ԓ}ӭ;I StW5ƅ3Zg-k(wI$zke=㕷,{('v-އ>DӴ{M>8P(Ё3דk^֟=8`q}z> 𦡡1iSͶ' 2 ҹ((((((+BKhD2[Z:SX ؀dne?쎊?3$i]Ffv9,NI4ʹzCGO]]otiG=̇+g+գ"cUFT`*Z(5\k[GyWc᡽܂hG!\{ϷOz;٪ ^_NOlQE2:V =++Ogc7^oX]iOmwE"z*QEQEQEQEQEjM&q18U4]0>J}V?֨Mw1y 08V"IXFwc2Mw>yqt}\]D+^i֚e!vQ'](((ivZdB =̼A 'ynb8~+7H:4(((()D8HEQZb?2r~QtMWԦA D6sEi Rk3v,0rzҽ_%kw  ^F=uQEQEQEQEQX^"͖oGpre0>[ޣ?T$O2r>-QEQEQEQZiFO@(̒?u1 46̒{}k.|7{ Ƥ#D iȒ2L=y+H0B(PNN;SOU 0)QEQEQEQEQEAuo ݻq 2F+@ACJ\QEQEQE[/w4IDϵYH.n;J*ʳ'KYrzX /4TgCä}ir-<½b!5$UQKEQEQEQEQEQEfzŮbWM}oA^i|Ald-DkȬKmswqu\Z̰ {d*((*HbyV8v2MiOdnj3WeHSBj'^JTcd=;I5nߙj3H`Y[C>fEQEQEQEQEQEAuu 5̩ Kg8=4qnGJ>Ay棩]Sni;<TOJ _G(((K}0{E ˸e(#'nA&讋u mMfVlq3K3%GIoV6'_MltI^\ ?NvJ@ `ڝEQEQEQES[E uz\(*sil1-3ׄZ-t"c8ܬU(_OYtQESK0U8wD ~vxj Jk!NSjWqi529f8cuz^gn36ޙ(((*9K JvNS,j* O(c0U,I=sמ5xd.pv)aek"𾹣J.gR2eץy1ܥUѱ*qۂ*U!obQ\˩_TE~L&+Hp}=ϰ 1|q>k-bY$M2iiq}:k +tT5>t ˜Eg^oo + Ij0)DGf(((((b&x<6L&C@כ:%Ċ$}+&eɷl"=l4>{o$SMc,cϡYVoisGTT$፤*I+OZr|hqzΩ^_Ozȣ *U)#"v8 I5n}.O5X|GG9m qv=kӴOѢ1۬y?iEQEQEQEQEQE`(tEe=8>[3Ҽ15hվkFr2{NM%WQnlrK!#m"!8 Y!E9*hæ*Mo30yzVVP-vJ+J -1=ۋKb2$|Fsޤ}NUhL97s+c?G{fdʹ򓃟S^ NhW袊(((((WX!2\$C)9f |B/ %Ƴn֖$囐zWE['6ꡚ)U=j/0 o-6Xħ풨k #_N۽Xnka̒7 cڮ:lJ/&Sq(>Vzf\Mu1VC՘Զ7D)_#p=Z[{;9ldDl\{t潻C^"EQEQEQEQEEo M<h2Wź  qGƼw3)sU(%b&#(%FAc}}ƼG%-/7(^[O+6~fi\7l[r߶m;Lbd䀸=q>ZE!H QaY%$O$ Q17EQk<5ua=~Vv eb kcc5f(((({O"{8V#+ blP=EQEQEuθc]D 1I@H1^O'J"}$_>Ux>Չ"qc(M 7$a|O5:/4؀hU<鑐tjT*juQEQEQEQP]]AgOu2Cg8~"X=GCpkk^YXIh(*e1!& ?U:*]X=U*(_>Ux%}21ɲy; 'I04=?+kځKڇzcDcP ((((9;><7eqڟs15j6f|}*fEQEQ^hI$.  2sr&xW9_vT,gM}_X%ٙ7Eo׭q`z)־×p2UcYݎTdw^5؎W& ^Ϟ~i- CԞޮE}@S5%QEQEV&M?B7SP{Q֙F6*6G^(((w\Oiբl 'b;rēSL߇pΡk}}q"~ƧypT,jv8ݔ{'Jx}kYEm%{Uz}N0 &Q'z zuQEg-QEQEROi&Hb^zל$a~W,M#I+cMGEQEQEQEQZ:EĶy^&"jZ2NW<4RZ*KOP:><1!z)!X\ɯk-m(TQA=Y(((i!A$$|E9^Gp h zczmQEQEQEV6 U2`OvkG;h1kfr<<htZ()#E,p$[xVɸ-X&<}߽|\I FȒ0RH=QEWfVҡėk+A*c4.WfpLH# pQ[׭~pt݌jkkM#N©IrIdrGLe*VU[WuEQEQEQQM4pDM"f8 Ŀc}Dt7$|קּk˙..$i%37sPEQEX#[yR~䌄+}zܿE u a h_>gᵳEI'PP ?J((I=4oEŵٓ欌7cR}=k:٬t)(X =;EQEQEG]ϧ1hfo0)*c$5^X{؄RI5Et^:yہ 2?J~%Qx` G;Tא֏a?TboMPJ((*ZO#Hf-_=I+#I#w%ROSQEih5޷z-*Rk,iww2MMU_53hS#Key=^{-2JCpn y FA]dZ5k[a"@O.ϵr54VieB8^߈t-J}<}qk#ʍ1ԑ}k(((=vkvҭՔ O?V&fƟxrˀOFjw\z@;aTCEEJv9؞j+ݼ6xsL#GEQEQX^1}a+^EgOXFe¨Vtσ].Z: 6^f+ᎬzuÒɼO{޷>$ȣ7uOX??Qоꗊj7 [QtOW33ZrDdJG3k*(((|=eF#y~!yJ7p{rjƵ_x^{Ȑ#X+3"{ ny{JvKPἩG񰮧MuY 'w$N ooZ/,,.Z&T0jih$+m;~"&"k|+M5;E>?@+v((Sw(?6Q^OYon`Qu-Co|@t9c^R+5 [oǘ0l>&j  (wa #s|KqYZYQP ,H?Ұ((((c[ѭkՁ J;v)5k:֪ H$oE>Et^Exac!^\GĻ;941w0 uyR}:޼tYDW3G^=ak:B[M1`ErH}yN((_'+袽w|HdYظF@#SVUmdVFµI'i'm;q\GY}.\Cl+3{=EQEQEQEQEhzi[i~#;u(u- bMC~*{Kio.cw!ŠOK_-,߉=;Ve}%EQEQEroY25QEQEQEQEQEQEni׎,tGAGR\iuƗwy\O!ecnٮ~ӶsP?Vn/nw<*/­ia( ?=rh((⣕mW㦼((((((_X$F v`(&'KE5 m99Ǡk'ܲM>;X]IF_sR%4K{DӯV=XE-QEQEpg_?+WEQEQEQEQEQE]0oؗ[QeFd7aXھ鈖 hWˆ{fQ[^x7핗-vG.Y}MGEWTQEQEW[@(((((((i~$b#˰SEP(((le54QEQEQEQEQEQEQEijNTuHmQE} YHe#Ah(+ {? *((((((+GWa#{XbuQE{|3u㢵(+>,?4f+(((((((meIQZ ާ) gӚ̸['#v=I=MCEQ^AEQEQ^ge'm(((((("1  G3${}O)'[F-maM߿9((~z_zԢ((1:_5;(((((gxK"B~g;cOj b{Nne^bzʖY&+uf9&(+ݼp>]I!a )UEQEQ^ck>t$k^wEQEQEQEQEQRʱt$k^wEQEQEQEQEVzgQfd/oDڧ ӓI?o8(((ܾg-Wk((/ӴWEQEQEQEQWtW;`;P}=~5`4Gν)v\<f9&EQEQEQ^7?}7EQEW|XB/ z`>(((*ͥiݖZz=s'?}uT'4Œ*( V(((+վ\t{`߾Y̅qJyEQEQ^QV`Ť;N=2QEQEQEQRJč#ª6XmNMG~ ֩,xUmms.xck>((((:^+Iڽ((⥒bZT13^EQEQERNj.-Yu9>ʤebeosIuAMoHXa iQEQEQEQEQEu:-dnjyRTqOoz+ v ֒P2q+f(+o\c5tQEQEQZv[tglyK+տiCf"̿4v_ßsY#v,ǒI4(((((*X&e 9?HA|z|ރ v<ס[³[J#t ztQE|RV:!fӃ^MEQEQWtPCniNԟaV>e`IFu?VuS4<7%䚊((((((5CCg( syO׌_cY_i(O,gn0(gc=7j TVi|aSߎw#ªEQEQEQEQEQEQEU>m.GU6ӆ8=5'QSQ:)q+Mk+Y7x$\uߧlH27c׆QET]J"s֯}r0_}U cb+ cj/;TQEQEQEQEQEQEQEQE9IR EjǪÏ(̯.e'}CN]f`y\<.+V2epU{ۈGw_W\K أ}AEEMoo5Ԣ+xYE@IXeu?d*NY0Dԟ0ǫsϢ(((((((()A j[kc6r@}B+~%~eq@ ГwϧA\~֠!J!={[D6I,OWͨ\]g?V迩ƤhHi(((((((((+w#M ȭ7tO^4zEQEQEQEQEQEQEQE endstream endobj 28 0 obj <>stream Adobed                g5   o!1"AQ#2aq 8BRv367ESbrw $CDs%&'()*459:FGHIJTUVWXYZcdefghijtuxyz^C !"#$%&'()*123456789:ABCDEFGHIJQRSTUVWXYZabcdefghijqrstuvwxyz ?пѿҿӿԿտ=uhז1Si` S[65c>>[ǿRŎc6q=}jH卒cAkF:B@@@@@@@@@@@@@@@@@@@@@@@@@@@@@Aֿߌ};:prͬ%W&uMȆ^6lLDzr7܉aۮ`?W7wڒOY٦6:mԴb= &qs#SS C DsN `8+LJ+AmyZَ8Cq(aɷNdcKר(9]GO7#:U=_{Ŭׯ#ߚ6[H:o!4؞(7#kbk|r=: xGái8r}9 j1K1^I;^ZAc<'0`x+VW0f]oelj0co*VkN3n'b|fƟyO@@@@@@@@@@@@@@@@@@@@@@@@@@@A׿?'k=cه h GY"ɕK.b$|n۳; )wr6kq y.aIb}I'O:@əj$厫v"zKe WtжCݽ䵍#N ,U勔ԡm7#Ʊ-oHtW `/8z,[%$hӝ^!2PC~X$ iAymG/!jk&,u AIoB7 o(vqC7vIj@7p |?S~ c(m!0۫e.HAtg jԧN+XuX'#>&`[2ѿU?5?k?|aU=hqju(sٓM!s}~%g_6ZҿU?5?k?|aU/j?|@@@@@AmăxA},1Aӿ˳7xwxi&ea$21eb+lS1$)Wug 3M|VygE y*uWVGN||ks &ːz,AFCy k@zTOdiߵZA`]?qC' vWS>-5[^bFbfcH>ݴzG]Vc2:rؘp7 =o3퇿Ax#.^7Y%b3|/ {~ _d_trPNR?{S)YD'o$_tù|<\@A6ɸ2jrӅ{}t[Ic&|{͉Xӆ4B Nҋ&y տ4׊IJ I#Xց.qC~)ib ,ji(ٙ7n2VRbz'kP@m%3ZnVdzQWyBq >=c}kݙFsڎ+7dkgkGȃ2xFc Lh#fpP~\G|r0Ʈ]āex^נV_ ҮbrB;\C2H0_*xs;g-,P3`<=ϕ6>p҉kX8n](?QM_'GEA<ֿWݢ%z2]<4'Yc}5@$'^27 V2Pڋ/S g:07kdCVAtр="BynWQJC1b9H=1xJ,mhB#fC^6G` pwLؕ숸!@ߪɮ8W\һ&Kq* ؆}x 2Ha-٠^ LOBŊ>xzwA?#ލnA>llp{yWX_]Ͼ$A523(A?c^1ē.egpwѽ-Tnԯb9g'e\\v=o0PXB׿5&XyS,N-4c>xQ;>Kt9^<WMA,8u{St,U~v^|m#U#j=6<+ːݴ Pj-|ſ(37ni cdevwc'G6 8^^d+זWD ss!lkoJvzWM۵gIKNc$:y.tT.Zͷ$#n5jY7j \ucifV1,B:rwcCANo1ewN27rmwdlv Gy3Pdtk Q6 j[|חwrtϿZ~N}"l%\I$Oᶘoi[sisb%&AWwN ֓7p湤 dп89߉Z<&--vg7#5j;3oِV>)//b#2VFGTZ)sdll2r$aЂn?j#kuTxg[85Spqwp gMɊi>(,7|u1p_ulpխ/w~܎[TfYI#jѿW 0Pq3\F'[s|Y^'W@A$MVZ c][76v6L/:+0+SJ(c#pAҚ50acݖ608\%jb;nʐ =.J5Bm=5af-7dJvi՚8mw='2>Ht$ ոUc)B*k!fc .$svkf~EFrYc#| xbq=q5=Jy[G |٬*Vpx!)]ή%s[că7#db9!lG$(gGVЂw`{Y&!#y9Bk63e)=EzDr|W4ɱ>;JYӑڳ˻%U#z8 XMmJU"(hy$^cp5's+7Kh,+#h?6_+`T`|Җ9r@Ps|S\.4m0o |0@. 1#3kz&ا l:xb~3vi/{YZdidյJ{UYMIa.vܮp;A\\Hj\]  nZ{lz(Ɍ y>HsM맟1{IǶGփZ@`:m?o%xx<]л,BͨRb6ߋxk+htN_b&j  IRxɢt>ЂOpӵOiհqGG6#\ؖc$<)@  pQڥ-1!be3RK(A91LfkS-b&ṟ^xk}}?ҿ=uhV1RUY(ѻ$];?Z[hh63' o}(9xĮݡuSmgtg/d \H-.kX]*;k;oSɪ2X{ZR}ܣ=ɿ#_jxBZs>[;&i Y#QAxmywFl<ݮIo< {ˇK( {' Tf)qWtVs/X>g@m@@o4i-ۙ?'3TӶֳSٍL:3"@;?tఘc7];Z8,y#ېA ך3 O_>NӐM;yk麱w}<#+ý}=%(?y1zJL12`㻋X(sC;FvjFFA`,pP1wm+we5%hKB,_ڽ1!*de=NZOߖUlsd_ͅ`>|K#isc =ē?s\k fv,Wdtuٖ;ϸs[C?7I>o6{Ԙ{YZ y"Y@p胬`5+3Jݻa'PA8stR= H?)ž]1tZ4jVJ^HQkZ6rI? kW6"4pjxG9rgn;zO+kiصc+v7a5k8cor1Fk ~GlPv}3=%Iɟn/c\וF[dO8sKFO!1j+[sE!00ys4l@C̎N9,}?{/x0]M7 ߔEaSߎ x25fp9;wS4}uGhgy414~P| *$GnﱮP~5NƧa6dn4Sw+tIXݜ G-y^rAf#J2FsBzYJ`W#CsH _%yᜳ=ɏ,a&.mq;lKckN ϊ%&[C<5^bXvn{`齜%q.p4%ž|TQ|Ee@le N| ӿ78!ț Nbs-(QYvשֿk[jPسDPz;Q>:#xu5q7ķx2 :~0wulqrPr.*Awɞ袍6 d`yws@ݤ!`ݧ$In'}r;z}Yii<{ GoOT,1N2hϋ^杽`zL>#)e]nylF8[lQ;ŜL10wC37[sغ66A( ~fgXe8V䒹 c 8; rzmktѾ`[Fd ZAb6(8WIl)Mj/6 F $4اf*<ǙcZ[t4uf Fzw0tgmOVS/uzܮq{KO?IlsRQC$bc^)smkAXIA?Qg02injiZ&o?ȩ" R?b܁kZݚ>O<r };w؁<lp=wkA8g?z ~&i5[E̫6DhnN}HK o4g'SO#gahrB 8 Wtn"S,Gnσ=(?aԿwTj+0uԸ){pҭs,cGGf*pwM8ߚqsfF7+~& ƼR=( +{Vl;Pq(N|9ߠ3 un OGxVFܝLXaNΒgqAd2LQ>yZ;>: ~S] w+ \:MW(X$,- Oۢ;j;0͌eѴuW؅֛NwsIx!!L< yjQ$AsC-g1 ܍-7&ct(ޱI׎9u8lT e7[oc|7`p 6ًǹ h)ǷpێZX\71f"fz#p}>6]d2WrAi[jk5.ϕ^p-a髏VyX_Nq} Ï/d8:6Ӽ])o:@@@@@@@@@@AտS{>fܸ<ޱUHҺF/flWww\743Z޵^'q]Pg:(Vuu҃5MkTfwe]Ss㒕Pv]Ջyl|齱1r96y(;;3/vf($)"#p=;uA(?l⿒bY 9yhl/;+=nV^!d/y SJ[#샘c._zi)[RYJ+T 0\"m w:W?#mOț4fJfa6Ӯ,OwݠI`Ij&2KoܭfJ*V-x;#p Tr;,a0籬Ǚۇ<3^=1iݯCtAH1O.RaBdI`_?=+@PszHXS\aj-1yq\#\n ֿˉgOc/ U$c2V6y=j;A_ O%f5{;hhF&;<l^5qG2MkjKIIi$\r:Ͱ I!>?X-Wc>P$ehx3|x Ls:]^{?Y7w#փZֱc֍h?/]);5 : idH2x6ZaS5cJ3G1kH_b*/xnd/ }JPlZc =e;\OVw`=y@wM'v™ŏ#HsCZ%ICQ@<<z4b2bulV*}O-mNN9yZ9{ o %m,/&w~IfǹkH$=)9B\KNK|;hÚ6kC|7A\i-;{ʣ2Вx'BHppJ7;I hCPvL<;5L)KNGԌe%wӛs҃j.#a7^?#m{a­Fsd^Zw卯w y<+\d sa{24 f*c<]IM ͊Vva4k)ǻ1A&I<@A#CWXy;;;Y۪ubG,P\i"9+ \Z潻ST=M%[1֐EbH؞|g!-{c(˲vn3_޳Ð#'mu[ql?m>d8/dm-`sqq;Ļ=DG4rE+Jǵp摱zx:UwKP۞Jd%kK=k˚7{JkS:VCfE N5q2ϛvz|\tdEc=#o7wEw$>Mk yޝM(?%YW7M,$9+#9^-$8ds9O{1'MK/mŖǝ/<󗵻4?TR=uس/#-`{#s繭kA$KpݮrkY$oc_pk方n=BהuC^-=fy-yk69}{񐰸 KA색f ߅j䤈>JVb)wsC(;o샵?O:K%. Y|q)hJ_Wk c>oⲹ,AVG=ls`l94y[[@A#Vr7BkrK$Rl"@ siwZ&xAUC`5?i؋H\Dh,;h;{@.<,[*k667аF< >.('gp~4:~yZ1n!#yZϒGְIL] x׿S+cY*U̶n]|浠z  .d8>K9blM6 $/ Ďͫ1\1-go1/ [/>;AGV&ۑLAҼoCBך. k>˽`A;vXyB_,K}J@@@@@AxmWLI_P hߔ奴fJ:i⚴ã>[+F0m(:~c4Lo/c<+&tm.5_$];tf~-GsI r29 dOvܠsm`Bp>J"yFXvX~M!t9s.Nk#Uk_4J5`wu>kD 8̞yG,QcCV=I'^Jւiakvd@ǁ6 W8ٳ9,> mmΚ\V{eyo7;?xxWs.wt 9ž%Zo#}t\`A5oc >/XG;Ñԋ]ZQ ZLk=]nWIWd'Xn|2U5f|{[A9b{dF5 A#@@@@@@@@@?ҘJ]RJ8V6Pيgl rvy{p7X\-;TȞZu+^,=3I #iw0uz#-1f0W%eiw,s\785_rKc2չQD1ѓ8׵; ڵ- 3\f~Niy|;h%zAaeg19XFhn([l-!Vgltrьdr4=7P|.ӿF7k` # K\9HA?3{Jmg6xy5Ճ <:7O$λ˹_$wzL/,E!:⮙Uܶ3]f4vIia5N,cZH_cwFA![lX[F QG#ji.Z)Ѹl$dcAcb늸S^YCA{ӝر^2OnhWϒW1rK@,i>]>x Z x 7X;A(;+NҜ5{~>k ]&>VZ xH&6 *пDqWOQ<:fg{o⎨}|"m=j u)s # JF27ň+εʺ8b%ɁU'TtdgJkoCSyCD6'26m 痬Y{ryR7zr )` sɣw~1rxxK9kDOxW6>@bkYfrptl?ӥcAUqM9ӱiǜ"'Zpy 1Ҏ0ߚ&i$hI^H$S7x^nt|oZG#XYk]| .Z{fGB6Z\KyS`uAе 9`Vvy'u;(JOu\Iq'҃֠ LD&wyrUI-0;AQc,xn6dm!67aRi־a5&V~Yde:~^ۺ'\GC?VzZ3^;Y5$#K{Y,kHi(=-OMK~ fv>G35x-bHÞD! /~: 0nk  X/ۚ@hy$O^cO /_g!:6lY#b&I#kGJ;^<'eXk[xKB@9p]f[ w6Ab$kW5n~YGXfBl mPIѿSYkü%I5f'Fj_܆(|`.>Mb ofÒq;2 *o x?RJs~t5Bø,~p[0fPpx霖cNf9IbKt $7%ۓPp<i𿠩SN^7?|J8≼1u`uA3٦~iێ$nx'fO2c]=Nɛ21$Sd-,pNnӞKw<̥EKYkq9)9ՙ!chzР9.ٯ/1ee0X.{6KA.hEɕx|Vc|ó|9:Bc 1wpR cci>A \lA!mU4M5VGJ2.GU#}"L|햱'ƿoQd8)ŖJ8V=@9$cr {Inۂl|<~K2r&&{TQΌY4 sUb9]@VqH 䲣+[&'y.sM?F AbL}6A&Ӽ ҎhӔ-3k4-g3Wm LOC.mlYi2YKS^CْYy}mj<fYfdv:+rvߦQ׊ ̓N "I`{4#]AΟ¸,1%g)sl8:H$N'ԃ+0MZfCb7E#A-%n|Azw3rCQj *Z[諵'40D^Iv8AL' Us*ʍe+bn^FA'#̿?w/~MԈ[ ײ _r-[I*gM6xeߖ6\vrʄˆ;Fk*M8okg60f"}q >Aa,ҒqW-㖡5΃6[OO(wCDnKtW?NaX(bNCfC Ǝҿ$W8isZԒO@nܝxXoF`<B3xM3b@qۧ}/MIإs5+gfhKH{KO71Ug1.euE2SB^Z{6"z1Cǻi62mm7Ja.C"6x'#9[އOm1wOpSf>\s)SPXvv 滯D'2MEè/aB΍ =Fd+FtGQI oWxݖip&=%>mֱfSOP}}?kUyy#ڹcvӨ=X6,^x(?uL+ZgUA> HoB[!Qp7e{"vGJgLgt^MI%15bՙ#A Fz%ew[]Uk26(rlpsAu=(Mcqv5/H,I!"0keHbπtA_-SnR@8x*HNޞI;XLɓ3Znݖ ljI!fh"'8ɹޞ6AǮ|䬴/Y[FfבvK10X# |SP8NR6D96ohG9փNtRrrwXɎHϮ9Z|PzuV6sQGAY SĈ;;Z:Yw>V{^Hv׷q>{f5${fùGzf؋7$K-bës$ՊFǴ0א +Ìxz_Scȡ9[si/ܫp2F@ۍkbQLe܆-=1|u퐽Z;:2w!nΨ4M ה6751}aBחlLiS?QkA. 7$6UI64xf NkR!ڃzr\_<+6'cgm}ԃ *v0k=vZPerR92{EAִAp ߎ;z⋚%o3a _6lbOPq,#8#;ҵͱs_s-`8>JO^WH@ ӿsbvH&TvvLt#IF:<{s \6  hy &6:'3:V KM#~F۩$ KQVnHZ*Jԫk9+$OG kw5%v#ZI, c=.s Q/%*&"[ yHk+"F27憸z7AWa O"o;#1g1ssN?̜A+KV54<4s;LEݖy=̃'|5+q;Mr<9˝3&q,K[ iR85kPe.LksNpA@@@@A$48ԓrQ@ؾ ;! ?)vD#  5g}5,9הkyG>$mOlDMaz3cZ~UOJ>*P {kأo]1>N:~YiX3giNR㚃er1ib.Xv;nC-jo$ Y 7>cƀ@@AۡN`]'sFǍz.zw*V3PS.wHE.?USM^C#^9\O6?&PYcrr> |mcKKك~ٯ1s\xr ٳx$ώ(@G1ӹ֘eiT|ZRY#*Z&BF?=Nq.;\,9W1( zulFcTbyM BH ĂG-!o&(0ak-V-σ7z头= &6"g%~likCY8kCA$Y(=)\7~wB6O+c;|A ;\dFn[4>Pq3uld{ةp1#D[ gg3mYZv|s05cq ?YK:r{mόc$ұc]3K pi~nvA5j֥Nu`i$GC$I~FwT2ȝAC |TP:G,NBqw9."gj>jILXwQ;ha$3IwdOYKGN#H |1iAjb)/F,}NsXl\:=>p!tAPi4ry0GB"+Ig73#nB6tmqo,\d`5odGA5I<4 {[ɽy$v胤㸚O=By䍞Js#tj*;gp8,Eb9v2v#:8mAcHeᴺќ^[1`;zpp~tG\mҚ\Xj[jsHy.ytg :f;'4ot+En:R­pTѱik ]sWpXh AZO^x [c,Wٖd x xf_!f%̖'r=6ki"{=N'>nձ!)G~'8m$m@=e {l2gt!s&l%/zZi˙P\ԃa) 5;߄c|j,iG7g|:>̭ gOt mo/I5؎RoFZOi.s3S$߻l5I>y As+l;Cِs,[RăPj 6lU-HUN&Ruag&4|@@@@@@A 8zt(?Ƌ,tF֞qH[p|.K AW8*LZ+caVWebCJ|,^^;-6HMFVŧvWN۴srsp9Cz?42PIqVtfg- Bc$*wvGƺ֞!= ^v{26Gx^0GtAd⎪a(_5Չbx9wRZ]N}huuފWK/#[S4"RSNY%h{`lnz|:8M[f:]ĺ[2GÆܥ茱i7A}K$LYH'W$%pҸ1&  }7)̜^mۮ?߇Xm/>q>&f]nq95O [c'j#v̎?GI ܵ3ڛAjl#ַqn^n*Ѿ *do'-d8|]f/9cqNfR4,TKL92YW0١"gfjOr<Ƣ^.X-: *I `n{܃ҡGB44"6ϧf7@A_^:cOY(?տwT+0xWXUG{w؝ۄݯ&? nc#i Ap˾{O>mKr[@ߣ}+ݓ)'\(:JXn"fF+Npc:8FZK;YV՘SS24zPVu_"r_壘p֐-W#wĐFUpZ#f" CCOnjOQcH1Mb#qsAxLNn{1Y*Chk,X'r_!!8wuc7R^k1q53Oni0|`yA.^ASGZ\c41ioF5d^nNh132J|45-s}!]1;ʕwv&q=wBwPu G 6l$ki+Y=,cCXƴF?I$p#b0\缆 $ 噮/|{[ %WWcKo-|pT-mui76Qt;Y9xFހX>eqw2P;e[Ĭ3#9αMpִw+ՆJi 4:|x+2ҋ~x,QZ7#HZC 8qx]MV[K :ܭs9Xp=(?mOb\Ew0oMU>>r'8\0DCTڃ>˥d.?9VXG2ZHR fu. _ msl~F ~̃#,[^rY)o˭{m߉nyGqP~MijH5ֿۋ]xƾֹ=ڂVEfp 3;b:5'$=qPӛc~'^'V^(zNf#\iIO4B1>2Icծq(>ںSКsJzL,H lkR$(h=K#.Tv? 3#qgkLk{pv|fcj6He l$.{ޗ8JM{!]F< k<LXtdj!OV6בgm?Rsʾ@a?'27LƽΎ X94t G|rV6oeg49 N!.s])qs:n2ˈPMZ$@ؓ8|A!.lv:ma-؊9n7+Xs\V\=H=*[!qVW2We,=iRFy TܻDєJ6t(cC>Ə}׳^,VvXOr $dLtrJ,8^|noai~} lX:%614ݻf\Zw  +W29Qnkj}1|ww&?*zMoxѥq.}5;ױ6Ztb֔a;?c,')e[JY 9`":ѱ~RI-:X[##rq$!cG1*aOd-=kdٯ3(InC{ᤚ F|[6\B8G^ a=lAO ŘnKb}k؝r|Ct< o12E=A]yY,{>.K״Z N /#6SZ7湧pA#Ѓ~6ӺVb.QY(V:[;B!c@M58%u-+] d0&gd okke φh+5&dgƍij^H %·kG 5uvD$GZ|<7H0ur=+/+3I2tC ;! n]f`s#6Gw9>43g/.6G/[ d7w"UŮ|[xpjUoˤ%D{rkSlvwU3X×B+MLJ3ղsG뼕<fȶ\*_id±cYmx0IN;ZûmYt&ySJۑGF0a9gLt+gwdiסuCv(ٳu&Jl3OqW9BÛ }+Nm I=qb|Ez19rF>g9swEXZne܈Ͼ(cq qpo+FῺb='_p+$m3մ7SF`ۜrnwfG9='FcpXK]ֽk[%bC=ug2s4Rp1ު^*r t^o#Atܹn:^6RtWq!s;V6@Lż;sƹpc ēAr7v6k%`zxo1hL0s tsdv67m=? xĎ,dŽ"&3r09ןSPY? >&ePw.n#kí[2|dTO3>?4{b"{?u_3PyO`.Ycf* XdtNkk`XG&K񆻄el8r~?U,[\%>Onu+zu>~b+lʣY~}^@/>K묝٧خ6*x˷[>CcoN4}_-plDvG71pnpVg@@@@@@@@@@@@Aпϊ=8#ku_ғ\?ͯ.j }܏u)c=n8vWDh ?+2Qψ]ńH*7Mq|c7Ou?56wzZ֖m)]1alt+"G pW֭uQ41kz5k@@?֜>F; 4/V.m|hA9.c=X^qK{+ ̟H^06i1&=s OflW0o8W\8^ᵶ85,ICl'nW!,-( \=uecu6>6b |@C=Zwkv;{?־XkKh:1pN a܍ [= YFե(d 59;P}MAW:7!>ddySrA[2FHkw*QܱX[\߃sA,غKqXɦ6ͯx﷘ AGW/ݙϵbl2N {:fឬY迨}c^o%?kz:?X;fɪ&VF`o?mv\4!uar;&dsVX }N{v􇂒SU Y.!fdmN](yypsm~йznlfbbpC^wCHQ<8l G'=#=`< 6hnQfnnn(<{Xi4c- kCUZxw42=OYs:[ͥ]qބ6κEPqWOin(/Twuud˴N]cp'@39`|4!$x4oA v[sIyx) ƥ֒ɾWsYN1@Ә-hh h kF?)}h՟=1֯sS|B[Nd1dt[﹐uG5;QKdk B 5Fk@IvPിkNYKyVdERFlJēL1HַsSO!Zhְ8=c-s\A?ѿ_qC<+Č5CHfLLOT]szgf-V1av:~Z`ۣMW:xݓJxp|rڂS M=oNJ:Gbto85' ev~gA=A57f^gj6 usU[rR?~ylyvwxAuwmKˎU(6p'Ed97<7Dvnlֳ=EGa! d^]JX5`]"yz4+b7= P}(?+7ULlfky U6Ys֏Y D<qWxtAR&f[0>])﵇j>)>X/spL9|&QѲTԶvKQ[&[ܼ_L@ؾk,~6Wzk7[Hޝ>%J4%c]e9+7rK)c#/pX{,Af6^fOp|ni$x[#!9HAy@Z6Y Wsq털lz56z;gblQv"X1dMpiN]ۥSRBc^݈? ,᧊^#4u!cv/D׻~mcu-#࣐[Q ӫ2@:#jlv6#s'8cLM3AI oِ~n7R]Gju3=|mbV:Cnv3{`Z6N]c)o/S-2is҂KZǙ(l"9pl wzd;wQXm9~kGeMgP8bYlcиo15 XЃ-B:mI@{Y8+{ <Y;]ot 6BLn>q@~vega I\|v%IzהY+p|(?c 2i!%/|gɮ[p eeibëWk Tr6o!cSM; siIUtF-kBKBӸmO~ӱ,r+,ȋeyX_fr8F:.KxHzJ[L~N~9XjA f}ހS+ӭKy-;Z+Nn?o. |GtA_%܍(Tnأh?}AqсuTo4MZ=&ָ݀{-vˎ &)f{6rnW}; %qsmFHw}b#{]?ɥlKb ?-gزlY~0dƗa/i߰$ &#`e?B =М4ǧ8{+p l,g3\<_r -]d_ZIDt]/#zY+)ob.;Mg0NV MD|Pi>pLfݫ/k}/)Vk;s<=zvwTi-3`5)e+Gjv ;V7; 8~Κgi6n{J\j #+sN2.]A+(y_؜m‡ɔx:Kx{nmZ9P<%:cS\D5ԭ7w=Vh#\cFb@@@@@@@AǀL^׵IKrW X(>>m:^F} ZVNp 3DYe'w{NJ,5pL(Ђj'Pq{[8lQgl~Mu_)EJb(>&68`h@As#MuiZUl9w =[i؏?kZ94P9es1[>=ͳf䍉50.pŤoJ,M#a#5w9;dxl#vR"]v{A-!"ln>ݩO$_r|v;UnԯǚӨa+6weBѲ7-ol9C8O+f-;VĹ t "aW<A57 }j6MSV4/`pVp٧|/# M;}Yrh)ZwkHd ?x?=ܚHh,MFpꌾ<1ϑ9:W=`҆խnq+/>Z>?'=9gX}+u{ϳ79-Ӿphfι U({Ǿ-"GJZcxap8/ 3[K.ZsL)փJ kByN_|ul͏'_#8p 6\Ȇɍb\0֗pG+ ~{gA:̵oƒo #@Y,P)$662kqu$h[dO 6,בן['G,d9o0n 7,1ӿpT_s]}!nShen#nF_P6Nh }^B>/hC$ 6fNQ[Əq_`Z^6RIՔlːblQ W zeVoәfڋW1bo7}יFp rO/!jf#9^b6NR.oa7>7nGxUQB:ldrDNPC8Յ}}bgӒ1Ǽoc2>[ d"lnR+|R5#AA |>f]ImexHZYkQ"k#kq]|Rm!%Iao3|I[g6w F2|( ?hN1>F18 $2 /44i&m\& 6*=`>Ctۙ>I%sQ{cfs ش2I$i;A3=ǃ QX;4b-n $DN{ޓ7(8b9oUCba5wOP|Y ml{t6.$f'vĴn<GDy6F$؞q&3QF$?V-hIClQڍKrI!eV+3J{E nٜNf%x~^h#wSV(c  1R{Jdž tR 5vFlTqіG^;KbgG!zpx<Ɍ dm֒f0"ִG@IASVc!6W2KNoox`ֺvkhhj]i>V|֢d8bq^YddmJgs\RW 8Iz Moo򽭀P%gO-RXv$%a- O*<.rOYO6bP -Hstpc;n\y:W9䍍5fjUI*֞$QfnR\z>AGX枫 r1㤱-x%,kbOFZj` sbatvѱm AY^Q ^['j|ĵ%vׅG ؒPs<:s +C^)6 *צ؝ߺ)$CRh"Y @'k\Ԏ8"Bw73Q~ni&ioRDg+9j&;Va$,Ρs+W 6$k]!ٿ3Jg_d~Q=5by.7WcZ.;⳸X2cy+4BalH5A^j]xdt -zk0i_aD&A;~+lj={RpҶNҔxy)vAlC_.u79`topr vSĬ xnx}C Mw+ <|o~3ڱռ)spWۙhPE [C4~=[f\,jtwkev:Ou&^KxCck@`<~><&o1a&>XZv!ұ`؍Ӳ5E<ӭ$F]b'E746cy l>9<ݶ ܄!<)8w5s+ fa,ύ`胹h, c4~d5rp?P"ZZpAf^Z,lCv]Ԉ-8f=?j(1u817Gۣ=7D'Erv RR{,9y&O!psHPsO~;3O7sZ >YDMBr7Ḁv:0bA$kJrOk$)ۧAT8P/'jrta i>s7pd,A+i*-}9soI='6qIkY4r${ashsqYsWxCٓ? nFt7Ze#T=H]!>/%w&lohMS|G܄B,sۻw 4_ȟj,91Z1a,2o/ Ղs4}nϕέosZxz?,tiA`,w 8'nN=Hyd1x#տQ٧%/_G, ygxY-[DӱlFךv.3r:&vٲNi h|:%77.73L 댆yH#А  \kE ?lc{8=!mR]{'8n7)2u.iÜy2:q2;x^m:n? =+p:GZ]y-7q+d$غZAݣ@q~ d9ΎuZHs+:g4?+yDdy{wllNwAؠ9UlZ`l@K&cews0oA Mr,:UEn؆rK##gۀ:hqbn]4r6nasٹc^y\|KEA9|?kurp6MIЃR#J5+B6dPFh3Ni2N87-ur-|O5^Z' Om$X]Gza"鬃N' U;45)Nqr ޱ{p|=s7P:osMz0(,7݇vkJߖd9#(>Vү-"VKw894kmٻl~+ƛؗꮚO,Ӕet3o9GCEÌV x1smZ+{r޳3}:N9ؚ3pΰf3  >Es7Zq5}|NG1G3bg2'G{(Ny.&̌ o[a>s-e6sk|];ń5ujEKhwv^X7Tp U242tp7R#(=(: Mc_Mc̵i,Zlw3݂IhZwn#坅N:̋%vG'}ܵq{HcoMc94椑{]F؝ed:Y[=` g<5+?7T+ݎB~(6%1;n9`t=?¬!-QV_(Y><kvg"Cd4 Ԭ<=Ppp\<)6scn4.NZ3blmZUK@6~ ZՇ1d< hnb~`b]e&xM[x1،yp߫]@AP ? vvt摇'=[Y9q6^O!]5v]s_ ֣q]i[i|X|+F.lGhϣ̧;A,h-vsBhI635˛y1DdskH,V$z|>NM=}̯71Ak6 $~vAzB?'L_F}ƍKÎպH}!dscbؙdDCBszs6Y9Cm5cuZkRU171FGB!sZJvf7 9+ c[DC6r88׀}[9m3nj;۬XI-a.|I,bGcz^8ڳM4wsI` ܍Pz6?sX7sFnNsEܚ{ o-?d&cz;3!`2<84sH7$ ZsDo1{l Z}Mj 8^ qXHӹkibMLlFޫ|hcTY5$oЄu#UieErԥ%,|^#m+v`v93y7K##a̖'L\.w;|vAp|SK Ѻ:EZX#١sHq|;EiMA+G/IkՒy2@ ,p^9 v:K^M]C.G+#V̠n7v~=YkԐ`c|yg y֮t#ӾŻA1Qە Dx ${㹌48TcvqگR˘٤fiq$Pymejz }I,1[\Dǻsmqudf?`d#+Yud%4p~oZQ<,Zȹ4vm˰zRvB˲8*n"Ľѓs];C-Z;{\iM8ذt0Xx͹ ּ0o#=zΦ֙'ͣYc`{1׌Ѓ8uU,vn7'&H~XPY'&׭o|VKה5`4 A@;8LQ?҂x?{\ p燸vhiC[&Fٖ vhAߐпQ٧%/_G, xϠ?ҫ GMiv+WifȭMlp6;w zn<:)DdcfjCq!([=08 gC]']F7LE@߾(=L6<6vn*ֳ.[1֭J9O' аOr[_Y*PqpZ^sm<}vfbH=Ps g}9f?iiwMt\ȰUdrXny$;~m9XuI*C;&RV118s]큎ƃjΚ/PfIRˌ5wYŻ{7|(#?oj-'y1!Ѿce$Tzv{|вUXic0 xmj%½ 9m8I7WGVzq\i*V8LX> I |ѸƸNǦE[ɸͻiϘYfO 3|NӜtÐ?dH[ $xVbf.6GfB$nN:ᗸ˲Hth1M%[`=6t5E(9,et{sqߚƍy#1cZ=f,t87JE*bF{vM,T00qq~"^ꑾ'~la{]Q_'#]l/kH^d|TVttoahkkq(?LeT0*wa#RvY7H>UrƼiA!yә *RhׅO9a܍oA2-fmx]Su؎ h$=^zucb-cQѲH\,۱jǝL>q(=v?]jD19'5$nx44D;s丷@,;uF>1^202Xb0pW+Z ѲKxȲ􏂿.9p䰶yiK v胬ete`mIGEX֗۸`%ɮ6i1,ᗳn5#phF;"FY9ʺ'qUqt4秚1Lmf{ ]u($XܘfW1s1t$g`:Igo՞(,?ϳ'8ۜdյr );W5ߗ' FeJm81*0EjѶ(bcz6z>ѿQ٧%/_G, xϠ?ҫ x=G :ﳧٽVS,Ky,Oᅫ;}9YdwȠѺ{Kp=!|ngshnRM=p s'mXkѷ(vc/>7[֖6͹bv0$s[ԞQ R~m{'F ʙ foQpt.Ԋi 紝'oKl6EQ45h٭h?Pdӻ#019گL?rtT{p&YB?~'Wlw9q;F3Deq AP~nc)s+f9%fGG C~X h$܏5!eoxm g+y,u>ӹ l3+L#<>o|C^;3ݱpW=k,ǻ'Z>K~>\nzh^y]!̊'7#JR֙3La$y4\N* s<8:` -{ظnTK6xG40z@AͲM+KK^o4V&>ÞՎH\ ݽyL'{nXx&{jF4ߌkępǃ'VɆa&"vܲ#Ď'e*rz.Cv;0q8]_/e2&)^L&JCf&uambV={:ҿQ٧%/_G, xϠ?ҫ m: s[1 [ޜmbej.OfXbi3Mfc QVdΎ&MǶg^'ٚC=68R;yw՞w;IK(xXfh{4zE}c>d}$ S{+jemx#~=%mqZ1I,WsΒHր!;nKFS,>~桱ͺi?8Z'JC.>;d؏.L};w]|7 Ȋ2j/9Nlv#|=r `uA4c/!/j5og}#u渟WG ڒmNb5#=AֱZYק)==!A-[uZvK F}ElUKO%J~tSD=-E9`a P~Ff1Tt`-K| ܲ#%ceĽ񃷧`mJؼmֵhd)g3 Mtn}AٲZKV\nG'Up vDŞV&c!AB0UXi6Sݾz#a2wؑ8yê|+B9 -i+7ٯap(8N('_%r MkLK$25wgB9O-9~"f]2YI-9HQz xe3-(͍y<.rTcݓ5~oPy 5vʒqzZK9gE7mH3ďKzli%n}/Ԝzh5q4d;:FBƸk98ŪEo\si35؂oSmwowg YӺ)[T`V_2=2G4㻁#mdӿQ٧%/_G, xϠ?ҫ rME Rv19Ѹ;x p'ibxO/eؘ:0`psXKA#+ŪîwY# ^G L3/>PD}x v_ ѐkQJXzdonz?fAvgGm=O>m1-`>q;TddnΙ,fN%wby2>S0@Vm_^%dw;+7rԃPYbwTL=9sxYK4V)Lu؄cs9y7%\Qg(䓐]P_/ },lh|7AhY|EjTr{ojV w{lA-a=|p ƬvC"3W/%I$6x\DK|MCNk&Xܮ6n-<6Vr]@G8zvAp{#zFÆ 7K$q29'#< A3|=>S#g.f@sNJK$l|?W@, G'6͐s$q.PC :V3q(?޽Ԯ}RY[:8i}YTUf5vvM2-6W#N۫{"pdd6Y\ydo4[%J,رV9\6m$HPc9vAֲ7NfdI#e|$s9xtΎ,KK@Y{b?m|;zI$sOO\˒=qH>fw؈>+r:V'2&K/wFg.CO* W=Ki#•ٮ:M|J _u\>86rf6iVw#ēA?sOiL]|" ~*4$01oԿQ٧%/_G, xϠ?ҫ  PF^.vE''4Ý$+rH]Xg#'!oim4? xl&ffa`;^ۊu^v>7u1Zꆺ tٷMf'= ^{VvYaH{7kH ЄGsM|sdhW*܆+89oPYbwUL/Pᤓ~V%Gf,i 6=PG:Jt1w5Ε" Vk3 b2G69h؟U9M--x%-jU6EWAY%Gcs9Hu(ؘc= @AܽK_ SX1 @*j 5,RAk#ލ{a.>k׬iSCJibF#$edd0\SMk2l7,2tVt;\ TụV=m5K3W!G'q%+Isyq;7Aq:w|jڙ۳#i$7y<+voEӔd}L|jl)gYZDQw5>B;Aa6mFO^P~A8 [{~"awGb\O_8鴾\?'d`,oRB0@uElɚZh}l*hYK, $Б @Au87%mK 1NY$u;LAː ;]q,F8&SBbY7wVlE.Ü23&iA?Î4Β:R@Cmap^Iy5\1Ց~IݼICZ qcJOk qڂ{G/sLAmc-h'ܰg,#Λief55JR`F62nH,(>]Wu,u{|lOiżzyӝ;g]i}+56F^kj 9{Aُil8i}9Vk [lq߫=y~v˨-9ln69" ۧaX? ;vyᅚYܶ~,kZ {r]cR#)A-.|PM10@􀀀ֿ+;إ}9 ̆ s경}iMQdY ׼;샍G5'Gh\baýgn?Gu{DbdqwUf0rK_!Z6^ }fnf?7q<X헇 ?H]/{w$`Kp|xPK'~ų3567Tbvm5%<A`.sv}"/f ~Μxk܉fv1#GswI~G_#_'$Jɠs$P}?}AH]pm>q-ۇJ  4wGm&y,l97}!p{W m-|3xq9q2[>2̊W<{vA;fbgb{c'ؔ?peOXE>:=oQX}P|ڮ[tVb] ,l;\`;9 =8mW~y͠9g39&1+u$# B$en |:*2J X)w$+o0kcyf\+&_o!| <&nQ2 vL:l 4TkpqL`d5fC+7rr?>tߴ[ܒc.$gO5u])Nw3I$X͹}#;{KPvvW,681sK!WkKZG .NN " fh{_exu&L,ɀ1՗g ZL~ 8aîaop#ӻaTHKtm(=׿g]>qIK>d=/z  W'/kW8S|]pF3s| Q]1üdOsC5QpWH\p;98psfCQ`A^9DY` iA$ۢ WFIk )-u2Ú@ 593hYdlv3X̯$u (?/9?bV>r Us.1DKd-TGv gncWmf7qUp7xbxܶ9;Y7x\sƵX;ZfO96hb^MVDݺ;;0EUڽ:vji[i5\} yʂv哱dX6;mA6I ?xyt.1O|a<3Y,L ӹ,0➺>Wn_Ӭlr ,']IBe)ȿ̼hpZVPvWѻӿK.YM1xٍӕȲZb ZῂKo^g5#Tk)qC_ ?Vir)Y12Wz;ga{x}#9aA8oCXpns|1_tOعYǻ0-/;vͿ<+]#~^(<Y(N;DMKSVAix mQPX>&D]4+ZY {\;leXޠUAa\i W!!zvI6td<\<[e$п8zNbNHoUt3FW& 3νΤp_?+KW''_/h=h|C cМ>b޶<<M4wj-1ڧ aܭ<Lo$~pq|ExuM7Wlo6~9A~(7r!M)/ FKv!Us)r\9oMsSCؙO[jAA{ۯp~v'CK4p1| =O^;7ӇUc JW0} 6Auhpܘa- yNaI+U:!JX ZZjиج^nQ}錯dLƗ{SWcVF~rC˙M8 [<.J̆F+SylFAM-YvJ O}mdxk-ɽ& W- ޽`lNca" MVM≬cG~ѿیx "e^1j/qxStZEjngH6"|]}Q[gc,˽Yk?Rя9nϩ-ۡX(x Xw@n*z8#Ԍ˄PcZ6GY+8&neY$xAn 5ltāldO@yZS+&fH5pZH P~sXb,5^ \ۿkߡpzSi˳3| oxo81L|Ziќ18Q&Krǭ4q v1LyۨF#[aqtϱR1ѽl1W A_PՖN|u(X+!ykGܔJPJipHexh<|~^YuGD$j8䓈i=,7~-r),Z'쮱/ag)j*CLg17'Ԃ$kN٘ F^֓3v;`;L+4p"a;Dksr:9˻=ø.C+"KuGe' , NXsڜ~OTyz3DhԹSdjI%O+K^9cn0[UC ~m¼ƮB׻c4>cZD2۔wS5dw;xue cun2o7XbIdud_Ѡ]'zҿOPcewt6܉ך7 "kG _M_% xl;YIq&||+ˋ;Cp<] vI-;ѵy埛L}#of2\Rl:tWΨKlvw0*c88SqtK9{,cƒĞcc&;ȡ#۠#HЫ>ѐj<{)nsemx |6i,h=c2W4zk4lsw=EA/9XceA<5wB= -GOgMi?Jb->r JK[v;~\дnÚB5$Y#/6;JB87$Fsi$1'`>5n$vb0%ӿ[x%FxÌFC :7U@Ѿ͇#UZ`6@=O1s[ LP%åh%lv:v4Mi?j\OL²oO]25'쫦JgߙX~@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@AԿb{pMVڭa9a}E.W--f|pRN&fKI8O^d4$63$1:L5ɥWzwކqy+x<w#rj5=i1o1P02Kn 4CZ5*gr5dm{HߡH~ƉhއԜzue1K~862oק|m]J $.2q23yhhi;?1v1=Wupoo8К i%տۈgĽ֕B8\yDл>'/e]n/]dmLvc( 0ll6;&3Qcj1JP7kZzN՘Z^R-k2N~b{'H O .&mI_pMw+Zn;qZNk״by{Hu9tl =-; '*+86yϖٽ͗PJ=1F<5;PA?ֿ5^ѸQ.^40wA /m^bY1}iS>t;/}v  9 ݡNxxSWFB }\݇XJݎ>DOe+JYIvαg:ĤO!A !qlkw@$x ^z*gu0s\ӱtG}~oO!?e]5;2|o|Ԙ#iGjm{Z(9:r+eQJ=*cn; ]pVށA0.|.8<^2s.bdV %oPKq{V܏uX݉$r9l:иlk@(:2׿㵖ռ5Žsrquv2&2?s(,q yۘă*Nչ3Qjg/{Ak-GW81~7c V.DH6;7#dV#z2;H,FSK&N*(DZc`{alqpPiq 5,LboɳX4M;c#_>gD#uKTxKN}dNTvh98lbXj2V[}z]ؐv!:d1Rh_Z۱CDիf7XثQkMIO [g]j>oR`gnv,Fj$3wuA$|Kܐ?п7:JKFj|sSTVLFX=&$2g05cpb.3\+7yEg2~bNc+^C"kX?w7h^8K)?]MsrGSYb=7uje\[,)_Zb;pA`3zڌ6"fe}~o_K2ļ&O82YrV71PIX7e$7'Rd+e1Aj Y̒2{v뿥Ir2V6Vn7ic~V{P42kSf=AײV#^19h}vڄE½#]k/r; `9ѴmPggTDiZ_>JI=8<39_Lo=!y<,sN~R-g6.i129Hs$KؓڶXr\qzKY>|7ioXe1WſD îinи4-% lع+>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( O<}ɚHo#==+kſhq[h}EY}>y?wƫQ@~y?wƏoPoq[jgq[h}EY}>y?wƫQ@~y?wƏoPoq[jgq[h}EY}>y?wƫQ@~y?wƏoPoq[jgq[h}EY}'P(QB2FOl9ִѺ7FoASQ$[ryw9p?J7^Nwćh9UUE @` Z[SsʊOfy?wơ'J$ÐzL ?oG?j(?5Z4}~?V,~?oU ?oG?j(?5Z4}~?V,~?oU ?oG?j(?5Z4}~?V,~?oU ?oG?j(?5Z4}~?V: ,4<%#.Ĝ`Va[Ac((((((((((((kſkkſh(((((((((((U;Fq_Jia%ҫFB9}VZ'<hi\*#/QxSS,dLw.;:pltZom|{P-˂05PEPEPsf9]Ocx{&E;&Tqs׾(eHqx#Ѓ4]YtpUNLrxӾGҲ :#sϸȠ袊(((((((((((<- ?K[o]Zܠ(((((((((((˧e˧er(((((((( :\*խDyXe!N=Gqַ-tHy06 9=(.KaCN?N@Sy<8kXmY0(i.zzɹeJaC=N~ Y,ǯTbS!H ٳ{l)p9T: ~8SMof<ɜ1չ?MyiMԏ\C.2s7jz޴r82΀.xdQȊtW=qϽSH;H=)H`zhf)V6 uYb=񜚖)\)11W{g0~-<`?_ϯF R#Pfg?G!?O>n0'_%ʷ>j´l,a@>~?J׶"eAG#P*m;_#SG:HyYF{((((4Xs%Rr.Q1լ}W_f7 ?hXt+(䬬Wό`s:ޯiY)[Rx#Rz9KBP?/sqvEC+l9 ޴b91'/* >ڠ+4s aǥE$f9 6 ߮G ^I@Yp^vpGi! JHׯN>oޢKࡌ3m%*0EBT #g@-9s qU) 6qh N31I@Q@Q@Q@Q@Q@Q@g'v?kr- ?K[QEQEQEQEQEQEQEQEQEQEQEW5t5tQEQEQEQE>*BqӴt̓o `sӷ^blģXDZZOc6LLsǎ Λ`P}}V4)T1_Ӿ*z|G4f9Q]P"3ۏܓqo}΀'xC/Veȩ((v6{ZT_VhbLdCq %a?UI%|޸$Ƴ4gvyے8c9U C1R[IjٸO!IӔ'CZԾ  8\zPZqŚA2~uMk&$^Ct Y M2mǁ!$*?z.SP.@ykKLӮ?:3jGB{z~?Pl{R$5܌JԶv_/17_uŭ6D(oǖoS"8cXE5*S袀 q"22)K(>~4};mK!? =0qA vIM%Wד"ˑFۗ#*CYl&W / 0{v=r*̈U~B@=1玼".U\mٌ29((_ixH:PȫtP){i;n#H9GUFBʷvTaǸ#k˝iKr2Gc0hQ%5ܜ/wAge'Y^}jԶ/..rqԟR{z9}qʛ'Ly/qQ:8in=O9go4n1͌ aoyj9m|@?^=6s*(PPcTP0PmΑcoߧuۏcX[Kmv7ӧ^]m2X3a(%o=ߵcb9\dAA(((((d-nV'v?kr ( ( ( ( ( ( ( ( ( ( ( [.?Z[.?9( ( ( 8+r+8ޡ--1 CHfm=PMPMj`0$CǡGOETD]=ou_#U`H*FABF>I=s,u C:Z{c3EH(,w@:pD[U x]FSu2[E.@%d7ÿnx>D 7۷٠ QEQE#*`HdYz1'/=bnmI6 0Jso&^rqVR^J>QǽuA …$A5%PҭLs5_=p; EQQl(3b[4ߙf_(Rc *p [ BL@$$ܽRKKF9#ҿc}=\tpE9ʓ$>=z` Ԅ Hr|G'z` /e  N r8᱀Qfd*tw VAہUiOYz얒Nctc6t[K崑ywbH 'О(S$ ˍ~椠((((((~~` 3:TP=w2sW#+;#ʨeԌrJc;z7EYb7T٪ VpA@Q@Q@Q@g'v?kr- ?K[QEQEQEQEQEQEQEQEQEQEQEW5t5tQEQEQEWAó|222!V aL;I\WS:ɞcP( ($i*Ѹ* o*{~mR7#?`U(nca_qjR=91rW$2T;-s~qQ9͛jX`MoǷcV"9SQ41Λe@9}A}T;oN$_ߎ4fu-gЃ?Z((`r:pGЎEBZ,Iy wP"`(̀`I5ò}X@>^K`w"M6uVs@y~(?PL@5FBZIb==ZEeId.7uOJqw v3hG:IcTΦ͝_r79}p* ާ4F&n Fz~?kKqPnB!x;?rjo,tw}?~;α %P'3HP˨H1ɏ8~ߏ8wCo>88"k -{?ޚדc tN\IO-#y=k;FƪDjܰ$d{sx.Ѣ9˰h%GT13^ia!eVv*>o}ßE[e X"7IwJ}#ּyQpB{*bI??f fǠ~bq6FՇOߵ1f k=\\~0H^8[Ьw^}߿N ddKy' ?~9HE =08(((+#ЎiP?&[n`#` |4W ! ~G=?N)IA8q?vKET-=yF?ɿjrySR+@(((((((ՔD^"4魉bF@qӸ}+8X=Ai+ᔗܜNXw60RNn 䵹X~l%(((((((((((+oZkoZ(((+\A2q:+3D33mb_hqJ纴(((((KMTWCXd}S˸&/C\G8m_0KPmK~/N$uWFGPET6[jۓxxo1N'8Lv5:Wb9Tr@Ia d 5,Epe]>AU)H]WQO@@^1RhWAQ+ui/EFLj@?'=A+O^<QP =d>K߹Xs˷,9<z?_^=9bP6[.#^OV=OMMEQEQEQEQEQEQEUy-TYFӑ X*DvΥfU>j2F dr +(az^`Ǔ'tn@ ymgI={bz՚(((((("D`;䑓-X>S(YfH^ZF,犯:Gyd[G8h^w$T!M7&GNDz{`Uv. NYh U  q[CAZՠ(((((((((((˧e˧er((=b܌))B$WqGL##uZJlվoWi P;REPEPEPEPEPEPEPUa0zfo>ΧaZgjy%hfHC06/6]3p@3I h}p䣖=IGsӧN(boy]°1x Hr1>8[(11꿨(Fg}:wY Aj'K0Le5n0e<NUnNOEPEPEPEPEPEPEPEPEP$d0gҫTw~J* 7 )ʾ3zpGȩPxw}}ɭ 럛~Mƀ-LTIF> ( yⷉE5p*ۚJoC8ĉ# ﯗGaޠ #Ԣiʒ8̛dTf=[=T "ȗKU[G8O_i-PHTvܸg?Œ2 ((((((((AdZ(r2 7E^&HU9D`?W#ߎ2I#-,sݻ9A1F7ߞaNx$/1׷NIB>G ~:~X87}co8-+uX@@aNx8F~j((4TP{ zq8}sZ@Q@Q@hVk+C}@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@s^-O]-s^-O@Q@$*b x`szUveNu)8m폧 =}G^(_,H 9'$V,B_5V@Ud,rpOGlZ1mÐ~>^&CVGPU8)wtK1 q횵MeWR) (ZkEw2FWOxt?Nئ[sW?猄q$7Q29@ɎAǯ" iB/_~x5bfY#ƀ&Rc==QEQEQEQEQEQET2,n#PFDiǩW/&˃Id#\&AQ+B͐4xQGsצOPH hҨE|0 ߻~ڭ ڀ+jA,ftfԚERHrIE щ;G @'wWj8cJITP((?+ZCAZՠ(((((((((((˧eż[J?w?y9h^YWllC`q#See1H2m\k=y!5yd!t< g8\ G]i5Oegy.$#d<GdդDk93o涴6 8<לm/e7c g8rdeIk5MvlF<ERudڿ-IOm>z2jR4k$N2NAr˶E TP-9Go՘ftGci #hD8lzzr(L]Iڍ0vU8䊴R29P((((+3]blV%^yV4aٺ֝f^~Xr#I۪ ΅p tT|+t끟h(((9d؃c*J(Es &O Λ`B;cRTZ$ GȠ %91ʊzU~䛈;|ǯtKnqt!~2H*He# EWxC/Veȩ(9:S_ W-=Ꙣ$ku E5$@ 4(((f 2IV3Q<.=@#߁@M4p(ih'u,}O%AͬLp ҿ82qH#f%nau8=p [8X/)29lڀ V]d29Y = ZM UN;S}J(((W.;0v~uvZ~yz9U*EPEPEPZ?Z!_mjEPEPEPEPEPEPEPEPEPEPEPXz'vižN1q x]?-sVhk۝:"[zvp;~ QGjkuIZHiO2<&=AAN:ߝ$#+d\'FI_Ǐ~q@s!IcYVVX]#(P][G)x1w^r1Wh,u3G&TICzcx-yċ}zGQfgˉ׆9G5XMqjC^v/c$ۧ8< PD 7۷٫BQ܀L1#QTiDzF[1H#ڀ,QPp[lg~= mn^'%H1#c5b+GpDdXjUD(` PCye#r?os EAsFZ7awz ( ( ( ˷.`bε+#O,˩L?֙5>r +n`V=s֥PBREPEPEPEPEPUZcQ__q|ժ(wXqycwp}5$FI]` *5Fo21,nG{t-e<C;*/4$-ڈ8oǷnᚱ s9A~n0GH  x ̀_UG3dnȇnXp3j1' 888U>2})K1 r[s ,GOv$j"b x\z0;ytl݆&?q%) r}uUD T`0PEPEPEPEPsXiO~FQ&d?U{눧ilh]o'v?hPEQEQE1okV?+ZQEQEQEQEQEQEQEQEQEQEQEW;umpT>7c%G:*[.?9/C.Z$ <ѐAVnGuDK`w;r <R-nb8~~8>>"MFhs 1z,>QJu=hJKF` ֤}.cч9#ךq[az8/QIXY]#V)h9`<^U!G"[/P9SG֤r[961MӶ(meux Ru(K" XaYLf=*)O(kUuMy²'1۟4Y6?Xǣ`P( ( ( ( ( ( ( (").JC/ЎGP?_PϱtPY/ldf`3D~TŘ}t.[x`oCa>DeHxRr<3W,bxlI?tyc@ԬKz>I5-PEPEPEPEEqq -5ċk՘VTvis`-=hB++͎z̞.I|Uy1ߙ88lTgʖd$d}xqZRq Ͳd)+J S ֭SU~?AU(((C}YZ?Z(((((((((((+oZkoZ|= [kj>U#[r%\zu3@TQEŬ7!|/u%Y~9TP0vlb@='jSE4k$R$!uSH>EG>;ր3,Isy&DʻT,u< 8r@5ζaYIv0}ߌ۳k[m]ݙWh1nO:7q=wd6\~'r`91+M XԂNJ} #ѵ[?hA=Qܚϱhq.Ag9XnxU+򷨠 #r~q'GrK91 pk4$ 2t!gӑ%"l0dUʑ/QUٸ"2#fm:+Ln0zq@QE\[6#?pGH5,WjS ǢFt~SFI]UahEEŷn">zV 6/Feq~Ģ#H;*xin GPa Ib# #ss KEPEPEPEPEPEPEPEPEI3djXPw!:϶{h\|n'ZQEQEQEfŽ4J z.}= ǟZ4+tYp;YjLZpD8 =G;ִŴ.cE?q~Rǫ00N=2 n'%qx lnʮv.T{Ԡt bVpa=?Z>?'~4!~Ͻ~YOB՚dQQ$iQBEPEPEH @ހ t+Zi,fv9-1=skj ( ( ( ( ( ( ( ( ( ( ( [.?Z[.?8׷V oЍtj>y cT~3J((jf8TdP:b2f"H>O|] ),HR*zqךՠ fa¶61PQD2 =sNdKYV%UKR-#wpu۸oO4i7W72o,MW8_ހ5$/?Ƹ=0xj9lcqul&?(OCq@\*- B5x{@/CO( ( C֥35-2en\zuJn KI>]2y+SoyT3=Ԩ"y[P<qGDJ#^se.иA'r ]́;S؃hp~k(7r@r:մ`af}3ۗiM@vݨv}@h+d_"0{)q5 F?ky 7IS[$,y=L 4]eeT%pS@4iYs!$FsIcj(I'NM:(((((.X* o)by#k0 ~@] <\bґ*)SOVy7&QXE6ʝ;4(+|vgD#3||㠄!0nگc/#[_H:;?:6MrQEQEul%d-nPEPEPEPEPEPEPEPEPEPEPEP\׋K\׋#]-?\ oЍt:o?^(((((#t#㱪7PD rG׏SZ4~"2r܌Ƭ-y+Dw)^b*LL|JBi?;??&W1<@T՗!U_n9c1ހi$9IaeC==GP/kʆ?m%| ѕNAǽ:=Gi,.#R89gnfh~bg7cx+$+`dzqG(=Z7]!Ž;Rq ؆H}D u}1ޫ}*X=Xxg8#N(r8F%ӥO,91}8Gprc=Ӿ+,lIp<#k`C"5~쟯N9&.P4WDG8qcyܰ2o'&|z`XP;1ZtQEQEQEQEQEFY[h>u[L6dl{?~biBSnmAҟom m5^$ V 6 ${m Ʊy*&( cPz!ǰjx?՜}#@E^_ey`ơA6<3'W1sdJ9Z( (: 6Oa[Ac((((((((((((kſkkſhm_M{F 5yq]_(((((()UPx ?Zu%C&Q R{Q:vQEQEEC# 2GT'F1Nrsxێv S9Id l<:԰qZ,oq#Aq r~jx#@ܬU9eOɨCVR*AxMZ02LJƿk>P8N3Pܭ 9c&R09$&?fkm(cgR`#ڶ!GŒ=ORO\49׭ nܕVm|47#!pOȬ{/(Ad˜*bN6C7g\tzr:BS,cZƼ $@7!ӵQ4%&[pe^y߯]NJ l<׎x8EWA]'89F>DZ8X$q -`H Oqr*j(u%H hW$V*:mn^c\e1EUn'!89V?ڭPg771~YYc|1|$֝QEQEQEQEQEQES`~SV ;A5V7ss&"0ߓ8J*D `P-aB}zb)MQQ|jN:^_䥿juʀ8( (: 6Oa[Ac((((((((((((kſkkſhm_7>X>- B5dHLg9XJJ ( ( ( ( ( ( ( ( ( ( J*7'9PyO?CZVʹ x90oj<>u_S@B! ~[=HƉHB's}Qo\ɠ:,Q2V;,쮖ў}6&1z8ngr,+b9ӯ8u2YZ:Q#wslY_UH\WjM<}/Z(((((( G(jq!_( T$5w_7֭Ǥ76(( 䵹X~l%(((((((((((+oZkoZ|= [kntppbx{@/G@Q"#.֊tќ:iQEQEQEQEQHi1t)h((((*j nr1=.>z-$vd9%9I|jBgk`c &w y9={gqة#8 )Cd6ȧ֛=sj1&)G9`~\6+&?2qסro,d\0PS2;Tsp&]G#Sby̝۬,gr;&!)I#$ :chG/m; 11שƋ$,wgi?Ҁ#PCQU4PzIY((((() djsdXγ!t,@)i1*:ȊC+A:( R{i[IoU'}Kn=&qqQEQEul%d-nPEPEPEPEPEPEPEPEPEPEPEP\׋K\׋ᘞm&8Գ`x{o vv0~T}k /-'ۙ~Iðҵn?(@M3LPEPEPEPEPMeVpzN@<:Ie#|2$V"[`?w&3(*(&Gߌt@(KQe9Ca~QEQEQE c6bPq}:dV~pCGfLz>9p |֐ԕ`GZ4`z Z(FUV-'^?VW&}:O&( $* ZZ((((((8 *Z=&qT46c].'88ӯjdHcP˜Q}.fޭEPt7Zuʪ&(W_7((d-nV'v?kr ( ( ( ( ( ( ( ( ( ( ( H קҺ:zZb[}Jy +lcGt4VT&ۜ/XZc~ ;ocPQ@Q@Q@Q@Q@Q@Er3m(JR`J _GYg%;?@Q@$IuS#玸˧eR[)`[e syYXȹ)ۀ `+r+8Q&q >cщYLpUS;:ҖT*rzȊy"9\t́yP*#.nctԤpx" ( ( (I2:gSRPdz֨ @͗̌5X(mgvVN2C7d/6EuhR~qc-zZA)h(((((( ZC9hyk[rU@f.yŌ\s֯xit{lL| +i\z$3nLOo7%w6¿~B|O?LSy[leݎ n!O++OFx7\y?jF?w=~ί}3@QEQEQEQEQEQEQEQ|hU|j7j%Qѕ Ո0!U\J(/uA1Q*<2dW: ,^aF܈~I"j@Hkd%zcF]3g(0F~y㷊%`bܖɩhP+._}+Rtoy_-iPEPEPEPEPEPEPEPEPEPEPEP\׋K\׋9EPEPEP]P:gۏ*mrc#k6JA An}6߯#צ*JΎB)::g?EjLD̃';y=z):)QEQEPUe#884T@(( ( ( )(gfAREPv]̹SGP}EgAHrآ(ިK4*_~Ni_?j/NҿOF^fշXt?V+}v(87εk+Ec{uw(V((((((( r|d@1V!:$1sjUͳ*3bB |?@ o! HqL *(h㕢 HT[) ݻ}9>՜ܬF5QG8ס#mQEQE!ZU}o+J ( ( ( ( ( ( ( ( ( ( ( [.?Z[.?9( ( ( ( Tv#aJ(W1@l>Гu=zU;L'p:=zY$S9ٌQ@*ñSZԈ'׃ȩ€yfz7Cu4-Q@zIr2:PEQEQEQE Q`O7 3Hc'ǧ)`ieXZVp 8e>!,[2VIyj+:h.r\S`z+D{PEQEF>?*J]+=dkR_ޛ_EIOWmo1jC5m$?i]շ@ehEqeFլmAp}jQEQEQEQEQEQE$X\=;UoI-(p3*͟o׵Mym϶i*Ht(Ȉ%aIՇy Z+m*IY¤]_Oaܨ^}v%er)fVFzf\ܢ̲5$aKdb2x \G r=Vgv,Q[%G'Lr)܍P2p'uJY7xtPFpqGOV"@S((v[ Ҭ!ZTQEQEQEQEQEQEQEQEQEQEQEW5t5tQEQEQEQEQEQEU{ }@Q:guۧq8ާYӕ|gkuۦqV*rdlZdYRg^1@[Z(ev2[=xRH8L[2%r2x/ mn#ƭ6b^?fp;4SUwЎڝ@Q@Q@Q@Q@.mx98ΥH?* 1_Ki5T<־Ńn3j ^8hUkVg1%Źa 1PQ@Q@Q@Q@Q@Q@Q@2D"t$Hi%uD^K1Cld7bPGrY{: JJbhèl((((7ݼB7FVQEQEQEQEQEQEQEQEQEQEQEx]?-tx]?-sQEQEQEQEQEQEQEQEQEQEQE3aG/dcڠ8#22' 27?1~4P@}A؃֐~s\#t+;S؎m&,Q%y8y0::Ҙ(((V(˻g4s_Ci5T4$ < [N`㲄`Iޠ\h嬧ԂkV_@fRO [N$鶥&$~EPEPEPEPEPER-NdD=O`=gM-&fmE xb ]P5[q0"ұ%KFO*Ap 70\l yew dwڸ^O"(UQ3Ξ#3 !(Urxlg $yBWkÂ?Ou20GVfY_4 +F-EQEQE!ZU}o+J ( ( ( ( ( ( ( ( ( ( ( [.?Z[.?9( ( ( ( ( ( ( ( ( ( ( ( ]T.NrxIE'PGb#TH(6F|OpO=GJ7W{Do$&iXI;znvح[/ w3"0u%W(d idfvִܑ0GqI,}I櫒XOQEx[Ac+ 䵹@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@s^-O]-s^-O@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@)%)/^ӓCޞ3FJ:64A;vxNa?RjV.ye ZxX.q-Ο6JN 1~)W=Y{R%]DPL<?j^-] 2ds~\淠+\:>ڹZ<ׁXݣ`ʥ׮ =z(Y6@Hp ߨ+UY]C) 29Pm%Q.GuqU/\`c܀KbMf< *OȠ 5 42QVc̸Xt#v|Tki8/ur2}ϡlQ|ŷneADQ;ujI$lq:/N߭[}CŘh}נ=*$nynOSRQ@qCBbWs' i0{ZТ5G*Qh34ld16q';k\4I63-u9ܫ$kF.8M gImH)z?#2NAƝXvڜ*P&7nx8W?_R2)8"f^H8qsu @W7Ma^Nb>Dgp:Vt䳷f^ W$Q@Q@Q@g'v?kr- ?K[QEQEQEQEQEQEQEQEQEQEQEW5t5tQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEP@}Q@ H M%(xy*}ݩ*G2CчҥH&&|QAtB#Kg>ͱ\. װǽ^ՋBA9*FTqY|[abj:8;ۂ:20}<Ɛ7rٳ愉Br@>d~5AbRdO!޶(rղ>PNO CH(o,L1FbZ'Lrm #cq:/Rk'`# -ϐ|SZu-%PEPeT"ѭLcv7MUXcFDT]8e??_zBpfk(1ƤO.+x$cu?ڒc (aұ,ŘI'PEPEPEPEPYo]Zܬ? 6OQEQEQEQEQEQEQEQEQEQEQEx]?-tx]?-sQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE3#>]G>9Ƥ"K;gS4x'͍9;1r)hzr AQ4#sI fcu?#)^9V+*c2^ |9=x8?Q]W-;(p?(?{;RO%#`9})beԱ[ls{cuJ!T8$Ol,dPXd*60}_c@:bY>(;O;(Ye'8U(((((((((((((((((((((((((((((((((((((((((((((((((( endstream endobj 26 0 obj <>/Length 7931>>stream xytUs:ܘbF`RnJi.E1\)b!PVh("U2(p8 aFA@ 2147 gx>kUCew>{`; g`9#ArF30 g`9#ArF30 g`9#ArF30 g`9#ArF2E/[z ސ"bY&ROrV"~zf$(!gbG g 9Lٷo;Dpec`JYCruCu[z gܣb,?n,:݁F1r&n+V21$Oyr[yK Y 瘄35YLeܠWMb,^A>/˲F1 r-n'ky֤5E-eqw@rFAΑrLH!xI/n rPVo̗*9"HY[_V r@|I[Umuȹq 4y,t=ȹQ~w*r36P`/ȹ]ې^΋S`+ȹA٭!(#[qGnKςpca9 obeȹ^yrٕ{yߝ\4 "0˹OT} {ʹsO˩G;7o/ bcMTz*Kopu"$?$z./\O=BX2uN@ɜg[_C=/r!&ROoLb #_9o+ a?G`)pbV !?ߤ*B}ήG˻XWi3>eC=C(yU/ x 躷9L@ΒyU9K= g!HͦᘳUaY[QYSvhiA= .grUz *gwE<PaF┳Gl&}'(ɹӽ_Stj,g5iMێXlɹ7TcnM&gw3Άcp{t &cS=69[*Ql8>9{*#I'r|rT4l: g`9#ArF30(gwѶ5F9[Ggpr쪲 } 'ZǓ'Z|rUhɹ79H=s,wX-R爚U_;{-jrM4l%GϦᛳ59#ArF30 g`ŭ#~q!g@Ő3%!g %9TrvuEf+t9{ @?|)t={c Pz[T-vA T~%3иﮃE@7U+%$].rn\_"ge KdA_|ې3ԦGΠοfKz3(#!gPw@Πa9{ґ3gGfX8Ͱ;Ust匃 ސ3%@ΠȠ=A;}5wAΠ=w9| T*9t\h' ݔ@蚀A_DK?=w r-{i!gJC }_K 2!g`9#ArF30 g`9#ArF30 g`9#ArF30 g`9#ArF30 g`9#ArF30 g`9#19O LٓpqrF30 g`9#ArF30 sŻtF猏gn30 g`9#ArF30svd g^ g`[}D6`vx$.3f rF30bvQ'd0;g7d &t}A8 gr1_2g;t؜i' L9*53dj#d04gg5823gwdf-p̓i9U}r?0.g&ȗq9;g[C=H g`ĴM=HdZ߸' }*xɬ2+g+SlpfX8vͬ_NRw9sfX @"rk_Y3-g\fʹO%RO ά )ݎ4gG í[+܎`ThveQ# #5"YmYQ(R񧳧"cf"ϬV-H޴#/{@.9+-w[]IgE;_ώ#)K9ם|nTS oZ"uc5VL*.1'يp #[>ZnE=p7Z9~BG]-zPgO_!ge{|ߴ2Ľkk/A[ԓ 7/8,aY{|᪴\߁WKg-r._?Ch&eK"jsvԸ+j( Ůh/~){bls=Q Rq5gJ}@}s#|ç[9[ΜsgٌٲZfzˠ-K]8fͨ-5zȰ-w 34lY{gIٲzĿChLٲT7Kfl=zМ-c}3hlYY@0suo^Lٲ2K=dvΖ% BazΖZH=-O3z,ȹVۮq( pr%{9_=|[wԌOg oˊI=D9_Ǒ[Ch!z7…oйs  <ȹܳG 熸rO€8 2ܘ)og!\G=97!:׽ xȹYoRACuo eof􀜃ޚ@uo- Eື!%^}zhrAg SG 9&:Q9T{.zhr]f3@s8rS AaގgrS.ϩgsFϢA=W rD+qNsdື H RW ym9 о璏g:Y^0ӆPOYl<  9F= garq=,ʐ%x)!9,Lny%JZ! fCBH/ &C΂ O=h1B2Y

4Y8P 9K)%2?z gzܝڅz gw Y1Y,pġrV4̀H)z gE:Mlўz2q-rVg`9=w !gZlߊzƐj%SrVN9SxiH4C΄#0)Eѵ% gZ&&㲊8șڀ-q[La89ۀgjDx@ζa\G9Dq.b6x7&E 9G^3h9IQ]p.^MAgfCӿ;vF;vbGxVH<.$ن:[{]ݩ rSJ/8C7a4Qs\8rp#8Y1C~A=ڎLJ=L&wlSZY߿J=-V&n5,DlQ "CΒJeJĮ%)lѳ&r 97pCEd8hPi$,r~+0rcGߊ"I+#g٢ :W+ <|=,_7+|% [90xA_4ir#g5eڲf,rV$ CC+I9+}ZџZ+}c-czZ.Y=yʑgT샜J޼CxGNY^g Um{GJm 8u3znIdD-Rr&wb,[GwaQ31p=Ux :=iWX "gJi3[1=鐄o69~ڻ;dcL~>-8A5r&7#C%ɿs18::G;rckXPwE& zp{i'@v4;9zȼO g31Z#)Bml'ό$o,l39t|};?6,fҦ&SG 9΀WgѤ4_^;̹P!gz.N[isſog΅ 9Q)v~{/ӿcӳ (lO=_qKN_ޓ)g9Oc5=+?~tg9ۖkVWviݹwPJݿw)zLu؊;8iufsVym-{t{݇-k`bt׭o8"loCxaΉ gk91⤝oA;BۘΎY]#% rրczo V ]WYD!g-|7%n6RR!gMIxpa^a|rdI!g RB>޴E}orf"sb|H'9ݠ @l<7<蕞vH r#j~냾<[,M;&zU r$ufҵײe/󺿵~K:)șjw;u<hY{~U9s .m,̒ jSQ@9#ArF30 g`9#ArF30 g`9#@- endstream endobj 16 0 obj <>stream xv@ P?sڴ HH+3lckffffff,XU[s8mg+9U"/yXGf]RqYJtʖuI]M}bQW-Y|IWӠpk9ݨ|)[/H7 qy̘Wa܏y>/Length 4342>>stream xy\FdzrJxZ' /mK"*cKTxInZ«R3Bu̔cTnjP3:f=̦5D̑ćc+ǦGNUUFTs{Ͼll זaDzdv7SUaHT+rt<ޘ d;=.USaȎ+BV05gF԰ז򤝧tx>lٜa̖2Ʒ]'[:9Yon9?l9i ƈeNMztiJq?faXə\ʤIM. `s3szP-×~@[G85Z!2v~@S4y{E8¸{:XyHl}T1bl|N|w|%$`@rxhѝ;a'O 7PЙGGc3{L"{  Mtdh恱^#ۻx>/؋B5f'|z'E/Nǣ8KQCVtF q̋׀:(R`pྀ(f sr{;@650]$sfl0*OusIPˇqv`6!B߿CJ{r̭X,yUG[:zU8sl<]cMoQ8]ixtӚfB_GJEǣ rK6&͏l bnC>Oھ}QӔ66 7kIX{ bBF]^#8eh )SzsbJ|fGËz1USJcM3OGݣ$a!̆^d_&H?lKƼz&ևW,=[kXԖy0;&?\p pݏ"~MligveUoлBGxI"}|l41p =n7٨oӕ¼t!)aẢQm|iK<' .m 'yhr3'\R)aP`}0g\gA,Ar; 6MD~ 7>`6!WIKSm`Y K?2 H ̶۞0k2%'Vx_&+0#fgHǸHŸwLE.7KPa;"Wـ07UӣϙE64ʓhttpG064׊ؐ09MfI,:E5p\͊LVq[1s^jCdlPO\SgD{Hf^b&_yĞz3˯g9[ Q<-L&)1u[1WluYv+3%NmC4Ik$,\QFwt:e fv>b ^}&!д\V:5}5짿obn01GAog+0F9 '`q/GAWU*VwBwPFEVP u70W!He%KĘ*@?{YLJWfi.H3@hD4\;+% N 79^t|; n52ÉH'?YCw)e 0HC5ʀ ?4vO6Q|ՎLdDP:$ی2<^ր$Qd^}~ޔ Tu2ˈ8\7B<yp\V; b8>UI*"HuFą5dVL/bT0 =8Nu g.t2e0G'7 'BV1B O5N۰**! Z; !BQFQs!&.4 mT$> $4_uF+fh}%تGX~ȜhLm5Aj`QxdCv8%*϶o:iOL2`7QGkPVܖ-Ұ pg83^05? 秔,Rm~UGE܃C1A jvYUݯJb3R|mBY/ !U ҕ{#N.;-'~Jcp8^FӽPsJV YxEcI!Boo8QN=|[3">߈֤.-yxӜ }|Y(f¼kXSw>&⨋ڟ-AQG1jޭ#bC 8a&eM(_Bb{:FU[PڅnϤw2[<33r,IOŀU2$`f&hj[e!|,)s335BCQX$̄v*Uyr}ME5԰0+Ɉ4bbUאD̄ծWQ+XJzqU*f>Kоh:;i ɘ ?[Ue5m)/[/)3AL_EwvXNz4W ed&چċ7e٪׿=Q@\.S<:i1B»2}/($13axr彃C>dvn,4qrHJ3.۲qխr*rvg&Y+z"N~>Xѫu+L|aY0&d6.:8}H}0~Aַ2|A]ꉙT N &@sTw#Ng3w?ֈid/3)6m]r]'ٙ63Swi޼E5.{Qj-ͪfiU\;c1׎kGF/[ endstream endobj 35 0 obj <> endobj 20 0 obj <>endobj 11 0 obj <>endobj 6 0 obj << /Title(MI_AZ6_WSCLAPI_ES) /Dest [4 0 R /FitH 599.0] /Parent 5 0 R /Next 37 0 R >> endobj 40 0 obj << /Title(\376\377\000S\000y\000s\000t\000e\000m\000 \000i\000d\000e\000n\000t\000i\000f\000i\000c\000a\000t\000i\000o\000n) /Dest [38 0 R /Fit] /Parent 39 0 R /Next 42 0 R >> endobj 42 0 obj << /Title(\376\377\000R\000e\000q\000u\000e\000s\000t\000s\000 \000w\000o\000r\000k\000f\000l\000o\000w) /Dest [41 0 R /Fit] /Parent 39 0 R /Prev 40 0 R /Next 43 0 R >> endobj 45 0 obj << /Title(\376\377\000P\000O\000S\000T\000 \000r\000e\000q\000u\000e\000s\000t\000 \000p\000a\000r\000a\000m\000e\000t\000e\000r\000s) /Dest [44 0 R /Fit] /Parent 43 0 R >> endobj 43 0 obj << /Title(\376\377\000P\000O\000S\000T\000 \000m\000e\000t\000h\000o\000d) /Dest [41 0 R /Fit] /Count 1 /Parent 39 0 R /Prev 42 0 R /Next 47 0 R /First 45 0 R /Last 45 0 R >> endobj 49 0 obj << /Title(\376\377\000P\000U\000T\000 \000r\000e\000q\000u\000e\000s\000t\000 \000p\000a\000r\000a\000m\000e\000t\000e\000r\000s) /Dest [48 0 R /Fit] /Parent 47 0 R >> endobj 47 0 obj << /Title(\376\377\000P\000U\000T\000 \000m\000e\000t\000h\000o\000d) /Dest [46 0 R /Fit] /Count 1 /Parent 39 0 R /Prev 43 0 R /Next 51 0 R /First 49 0 R /Last 49 0 R >> endobj 52 0 obj << /Title(\376\377\000C\000h\000e\000c\000k\000 \000i\000n\000t\000e\000g\000r\000a\000t\000i\000o\000n) /Dest [50 0 R /Fit] /Parent 51 0 R /Next 53 0 R >> endobj 53 0 obj << /Title(\376\377\000M\000o\000d\000i\000f\000y\000 \000i\000n\000t\000e\000g\000r\000a\000t\000i\000o\000n) /Dest [50 0 R /Fit] /Parent 51 0 R /Prev 52 0 R >> endobj 51 0 obj << /Title(\376\377\000I\000n\000t\000e\000g\000r\000a\000t\000i\000o\000n) /Dest [50 0 R /Fit] /Count 2 /Parent 39 0 R /Prev 47 0 R /First 52 0 R /Last 53 0 R >> endobj 39 0 obj << /Title(\376\377\000I\000n\000t\000e\000g\000r\000a\000t\000i\000o\000n\000 \000w\000i\000t\000h\000 \000A\000i\000r\000z\000o\000n\000e\000 \000S\000y\000s\000t\000e\000m\000s) /Dest [38 0 R /Fit] /Count 9 /Parent 37 0 R /First 40 0 R /Last 51 0 R >> endobj 58 0 obj <>endobj 61 0 obj <>endobj 65 0 obj [/Separation /All /DeviceCMYK 64 0 R]endobj 69 0 obj [/Separation /PANTONE#20327#20U#202 /DeviceCMYK 68 0 R]endobj 74 0 obj <>endobj 75 0 obj << /Registry(Adobe) /Ordering(Identity) /Supplement 0 >> endobj 79 0 obj <> /Rect [70.8661 523.031 347.136 512.734] /Border [0 0 0] /Dest [38 0 R /Fit] /Subtype/Link>>endobj 80 0 obj <> /Rect [99.2126 509.196 347.136 498.899] /Border [0 0 0] /Dest [38 0 R /Fit] /Subtype/Link>>endobj 81 0 obj <> /Rect [99.2126 498.196 347.395 487.899] /Border [0 0 0] /Dest [41 0 R /Fit] /Subtype/Link>>endobj 82 0 obj <> /Rect [99.2126 487.196 347.395 476.899] /Border [0 0 0] /Dest [41 0 R /Fit] /Subtype/Link>>endobj 83 0 obj <> /Rect [113.386 475.196 346.744 464.899] /Border [0 0 0] /Dest [44 0 R /Fit] /Subtype/Link>>endobj 84 0 obj <> /Rect [99.2126 464.196 348.697 453.899] /Border [0 0 0] /Dest [46 0 R /Fit] /Subtype/Link>>endobj 85 0 obj <> /Rect [113.386 452.196 348.641 441.899] /Border [0 0 0] /Dest [48 0 R /Fit] /Subtype/Link>>endobj 86 0 obj <> /Rect [99.2126 441.196 348.767 430.899] /Border [0 0 0] /Dest [50 0 R /Fit] /Subtype/Link>>endobj 87 0 obj <> /Rect [113.386 429.196 348.767 418.899] /Border [0 0 0] /Dest [50 0 R /Fit] /Subtype/Link>>endobj 88 0 obj <> /Rect [113.386 417.196 348.767 406.899] /Border [0 0 0] /Dest [50 0 R /Fit] /Subtype/Link>>endobj 91 0 obj <>endobj 92 0 obj <> endobj 93 0 obj <> endobj 94 0 obj <> endobj 68 0 obj <>endobj 64 0 obj <>endobj 57 0 obj <>stream x1 endstream endobj 56 0 obj <>stream xc` endstream endobj 97 0 obj <>endobj 104 0 obj <>endobj 105 0 obj [/Indexed /DeviceRGB 16 (\377\377\377\360\370\367\341\361\360\322\352\350\303\343\341\264\334\331\245\325\322\226\316\312\207\307\303w\277\273h\270\263Y\261\254J\252\244;\243\235,\234\225\035\225\216\016\216\206)]endobj 109 0 obj <>endobj 111 0 obj <> /Rect [223.213 304.55 274.835 291.573] /Border [0 0 0] /A<> /Subtype/Link>>endobj 112 0 obj <> /Rect [220.945 292.054 272.567 279.077] /Border [0 0 0] /A<> /Subtype/Link>>endobj 113 0 obj <> /Rect [223.213 279.451 274.835 266.475] /Border [0 0 0] /A<> /Subtype/Link>>endobj 114 0 obj <> /Rect [220.945 266.849 272.567 253.873] /Border [0 0 0] /A<> /Subtype/Link>>endobj 115 0 obj <> /Rect [223.213 251.565 274.835 238.589] /Border [0 0 0] /A<> /Subtype/Link>>endobj 116 0 obj <> /Rect [297.132 305.235 386.094 292.258] /Border [0 0 0] /A<> /Subtype/Link>>endobj 117 0 obj <> /Rect [293.087 292.739 382.049 279.762] /Border [0 0 0] /A<> /Subtype/Link>>endobj 118 0 obj <> /Rect [297.132 280.136 386.094 267.16] /Border [0 0 0] /A<> /Subtype/Link>>endobj 119 0 obj <> /Rect [293.087 267.534 382.049 254.558] /Border [0 0 0] /A<> /Subtype/Link>>endobj 120 0 obj <> /Rect [297.132 252.251 386.094 239.274] /Border [0 0 0] /A<> /Subtype/Link>>endobj 123 0 obj <> endobj 124 0 obj <> endobj 125 0 obj <> endobj 110 0 obj <>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYa" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?j( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ZJвҮ$=@^}=լsnbxU A@Q@Q@Vv 5vmT&}6(׫<,~b3袊((((((((((((((((p@ ූąO 3K2ַ@Jr$cֻM>sb<^¥)cW6ossf[7ӠƻM&}>X6m\crt'W!+X$Y#nC!5JGO>2m`sw.mu 2R˼d2 pǃKf~|> QHU8[s}:T:<IgqB%8.Q$AAI1I-wo 0HoSXGn/;ENGB=I 4Oj7eA=נ4UcoC44J 34MSVI摿߆jdQ@=A%bI_O>֣ҭ&P0ѓA{ %;dr5:`mF7?A[GIoP[ լ&ghNIFͶB%s$,R{NQSf{e_cZ5ƵvQƶ2u88|\GkYfSySٽGd gyy#Cr:Y>µ-(=KRzP%Ķ' p8 } X?2g @;d AY)\ busy$reN3Z6VVq1?{ڴmu;)mu;;EP0@I8 L%3l)ϧq@ʈ]*%GH0_ߵڠQsÏҴA){Q KHƽ(Ƶ_ vCxpC`gv8wk=UM#rp8ܧGAsRdHCbJPǺ7T:ppxwQn =R(((((((((r;aFMkWr@n^H{ f5u.Zo{xsO }%p]:֕m~1NƃU2"ѭ/%eeoUHLoa&nWΣ)-d+NP|R,pSATIcO$Ln eVF-XEC17׺5WXQE?ι 1?OjF{YF#Jϵ4:|LG2g S["?–=BkiÔu\k$e+  bo,,  HaSsp:t_@)ɧeb9Xb5?*T]}˙׃SsR6<2O$HKU8g-4d-A,wwQHb_M,Χ/ J#QE,*O@=jΥkY1GPvf'8CiF'rA rzdKyƚmm!h6*0UsPiAU_O*Y$̢KonshevkVg $( q۞+;I4˨X'YFxNTnp:Uk+=7SWVytr[Fi#(I-/5ϓ78ݑ:{ i0Mt>ZՏcpz MH[E4ϩ=ϹfG-Ƥہ*g+i+V?[0+Q/D@Q@T7W0Zsu" o:T{[ȄǺP{hs:[wFSxW{^^\p9#'' 4QEQEQEQEQEQEQEQVl漝aK;tg?JZnwL#wl|5B6j*y=0}ӵ[D g$"UBFϱU?²Ӭ[v#??a5D<j$dBe=Uky7D<5i͹=ދ ֗=C'Csb8Y[RkH⺂XnѪUWV8E}k=H)؉rm#\g֟go=݇RYUJ pp84ao<9d ##M&82\HnU1rsT/Ϫ{vmnO$T@*g昶4.7MJovn$:䚞ۉe ;V5ڻ˪͵#>4>fP֐}s+e>ee@Lzi>=(6~{34l,+*gl rG@}NEt6}gDTPj (Q@$Jh 3P:k|kiIogk"|[#ֿkSo6™;O<}x|:j}q"S\ӂZ_[n<|¹؀aPׁe@Q@Q@Q@Q@Q@Q@Q@IO+b5-*:y5%0=9ɠh50 tQYr-Kul,dP0=znPZD:68 >k!-,g!Ƞ攥ԃOԭp8/l# F2 UѭȜr%~j:>dS.%'}E.m9>էG8uSͩN0SS"noK#/|n?3ZA{!Q}OzcmHIx#?%7V?0_"@qp:ƝPN؞Úo^+ۈ)'v q#^j@ V+ 7 /TLdAO25+gDvC~};T Yh6:ǰrK3v{(j_^"Ef^Bջi{MprN Ow(ؤ-'܄ GNsw9-,^KltfO&Q!b!AվSkAyYnGIeGE:QAAEPPQEQEU diRwlG%C ?#](I$kf8kenoxs!oڀ<ĺ]rjdI TbII㟯Rן gMB++#f6-eLQEQEQEQEQEQEQESY*N$ֶXyl4p촭2@n's"Di~[sw;e]m_aҺ`,z\ chW>y\'N@hm"M4\V=+\G1~"(86W9JG3e=Mَ.xga YiXnO=UԴgϜc'Uu9mkS j61U3Lo]چ+`a%F.Aޫ G2hO~b?^SSYnK"w;}MwW뉵y~{  ")*$}qҙ&uze1䁴qH.Bl%bV2]>[)rg$/⢷Gq/umM:AԐ^%1[V奝e-XՏShأq"ƃ(((ּWi e1On@ąR@U$=Mq, \ J~puhO*@ݣgj:ltXcDr8s>?μqQEQEQEQEQEQEz׆X*G ]džK")|cѽ:'"Տ AoVAiyFǧJX&TW YZ23y;v)R {w\tPcLVM-Eۅ?zs[P#i\'فc?ϳdtǒ< Cl|}8U4Ցa\$~ʽ?ԧW F\vt¨qV PUD}&{ ׭s6IK LKP 4^cw涷1N HC3V'엚nz[ۈ$RI6*ptךt2]Je&yH$~?@=Ay'OWm@nqSiSLw"ɃBՏFm4dd*1Gw4{G2SaW(P((- c9@VvhЗUF9f? !Xp=z`WO-443@VD<6`[0s?r$I$zIEQEQE5̖1B@3I桢6WdᏩZ((((((+ucdοeom +xOE[B5Y mV'=H09nªj6BF\)ܧϸqVIZPW,CMY׶iyfD;>Q{C^Iqq}Ge*cH3G3?ǥD"$3[C(avր/{ElaT?*}bӐcW/PCB(#X^T4A )}m[k$50*օ# AEPXQEQER3*Vni;"@r ;r{8N1^sxKo= T e^0zb(|aiqF;3.pgAy^Tm p`1Zt 66&һx!`!z".k@M[ce?t7/gXެV6]^y-db@s=}hGdNH{V. #8{9?=?{FDwRh Vħi$$]a͟U*?l ``qjޭvYdvS>(4$KyZljzddg L.>Mc=ZG`(((K*CI+E$5M##˖JvR#\\o1f ^X\QkAd #2'x!\ YX[$csXzP`SGBEQEQEQE,q*F*O@E^ ǥXG,3W$}Oq@k&I&v3@;ϮWX$o5\M)@Tr1k(ևA&dXq.a܁Y4QEQEQEu귦7YG?u?E'[3zVh67ܳzAZ]8(({{2p?-uL'̗ gAsںZw{ UbZY?œAO(mŔ[ `^1ӆP][2} g&YF&9[xesn?-b'[UUSlgXrI{Pe`H*FA'ؿf9%@n}߹5DIl=Fpn8h"T+K+uȆIfCb#[:AnA; &y7CrүZiW%f):yv"$3{ X{3zgUM ҋ-?pzKK8,=KRzS8!UF*Zi}yOsAji00`B:==JpЮ![98$nx}zq\>4JQv.-}zMt׌nŰ0!Q7,/8%rYKԓTPEPEPEPEPEP]|cwi2Cg H'+45Ե;FGbCXAYQ@YnorB*# REzHFgC+)bq2 ( (&KwI+QIv,"4{8@ 1'ܚ=m|-$vQEQER;h#*"1{l LI,ecTw5]-}@t-ew<ޤ7wNqt7R/OӞ=m#W9g8U((eWR)4P =OH$z]ϵ_($I ԚKhE̅Dיk+u.`t6rץvZmP\!W}5ڍ֣1wv?\:EQEQEQVlg&Yr' nPq~N s##4$ޡShP6@QEQEQE薞 A2a$`szW׫ QlS">k9 !xPs(1%m7AE7q7y(,]*\ʌ' \ zuKm;‘-+Aw eTQG< EEzӭ4 \ a#F?J((OCP1WW5-܁Fnր ( N&7{C}O),A=KQ2HGc5EQEQEQEdx]A3II.{wqq -5ĩK՜WXG\(q,Airޟks}bCa!r2p=CŖsV$u!w]c%# 1u+_VB 19zZl"mZkfXb7  N}s: :[xyFr\n9s@>Lo-bH29 ?PMs>/Z w+}YOSXWE3͐3'~URQ@Š(((neif`O&,Nw;ם׮ ktD1 3888Ű^& ^HTG}x<>$]cEhmȧ!9a29DZ5SY*NIr,WHI:H-\rXZs`>N/*-a̷dS_Jm~LV7yt袊(/65W=>{UӚ^! Ü}wu#dװo ]MḲSg,xQEKš'Q3g%NIhײ}!%  0h((5Y^bƘ0 ߉?εh(sj{[W'5~տihQEQE50Hˏ«>"\Ik[I2Id?{דEP|7n~iD!G$I-_[Zgu)9U*bP'fEpJƯ\%aPGqǔzdu>-l[7X ?3>^0qҬA@!qr0CC_[8#8玙%t,!Dq#c*q߮3@~$\v-Fcqf-:L~5QE!Q@Q@Q@Q@Q@Q@Q@{1!K#Kv=^w:O[Am8>R1%}ߞz7V;Uh%XYJykNn5"Ҫӕe$r@UB:\-3m~uBėv`.70jp2yj&쏭gEQEwLXZ5tA@Q@TBAdP; &hҼmL0 QEQEs;?Gj<F-xz׏q]ͺ8p0?Bkh(ΆoqKe)Uޏdg@0}t2-Y'y.Cn;@:yE KR;bKtǾ=\P0((((((((o &ZL؍ؖ#\ciV1K4L5 r0yACK$S(1 PkkgZڶ4\4#FLlu灚Ơ((((ugzѬȻ׬(QVn o?jEP-?Q@Q@`'׷Չ#|P!d AAů(((((((((((^ngO*k(@=:x䫪<%,-2XiVA+:cszph9MP+失ElK0%r@oN}+((((ugzѬȻ׬(QU'<1*?_v((7W2ddF?y]zgĈ].!p,יEPEPEPEPEPEPEPEPEPEPEP]^j+蠊4js*/9'pr1"Jou_ R5\E>2x`=X:rW3Z.7qyP ?2delU_mfHb3maqpqqۭsQEQEQEQEwLXZ5tA@Q@qϧ5SKFK0]@3Њq+ʀ&(("ݷ}U[ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (=.+FF- [vl AۊР('Ȼm_K=y]QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE{/?Pf "C5EP?meэWQT4vaΘF QEQEp.U4+R̖c =ּƽM#7^w@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@a–h~=N]q >$\c@Q@ :mѭZC:L/oc7y ]((~+t>uiIJ(((((((((((((((((_CܷR]W}@Q@Rg69Xn 5vտihEQEKhQ)+@;1^y]ß=-(((((((((((((((((H8 #!svO+|{%FVDgU..g^ Bۣn>( gn`bۼOWV>ܟH/x rPQ@Q@Y4Gۂmז +~'Fm5 q*ۯ,;6(((((((((((((((((4w %Ȑ ~:a@!cr-039{:޺:GW_xM1pA{jj Lܬ0S봶!izmN$QѐږI\kp4v((T9d`WD^=y{!CۂzƑ@2~ ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (.iޝr.,xewi~9{GԣT\xԠAwxk-Fv"Wv1i#8+>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?JY8˶p;U2|#h2}M[74`(L/RDaD*zaqCU0@[h~}*/:nhl8n23A#H >}*qcNU.w:ր'L/Q?_تЌq?w զqq`b=WvgtՁ aPi{E}>mfEVVb`taVh2ia{zbخq\!Zk[y@|g1ZY4e3!FP8b7hEEv襀'ߙc_&},<_VIA_sڪ|S۲a&W,e`#2]F3ӭcZK GU +&Ԍx=*+>GEz A|[33隑dF +),7 ֱRh9/p GlӞj*Ŷ!1;R?٠foэ Kvc w 7ֱ~7Tvfe9#.S2aXg>e`tfySʶѝ})ʎpkMRy^6U>h,pwIv㞝s[ZQ졟:7 ed\'=ԧ?M֧-Qm.TP`zRS e_n c)<ȳɜ׭KiFP/2,Ǵ嗧Oʏ2/(0.T\.߿:q}5$7SC@hqGhOXY?ZSw_z{ǚ7F3RԄ,_g6G*>跛o3@dO%-rceLUʥt'ؙx'E?6.ƀ'ӣ hrgq2`RǮj ⦱2p݃yy?}\R#+UbFAmY]TI:׀֥%ý–M0/׉7==;Vy[V,?)W4ĺ:.-Ak~ozUCVX<^6+:=7s?&sC{X\NbW`]yg?Le ) ?i"v8zw?ro洱c$cQE%V,H7d~n\"8oW>OZI p}Vh.̰ʛHI@˟*S$yTT˟(EeiK-.JρPҒD ;QKu![r8Ю:̢E)N gdV Dά+Pr4O'{?™-ĆG>01 y#Ҵ Qۿ9pWuOUw?QEK;;{{Kĭ)qݕٞhqXa^2<>sѓhRWPh&mF(,'0 ~c7qǥ\>Ғt;.|?$o~k7BY]X+wc+MQE+7G\Eo k R=RG\Gϻ7:~4,C#~td2F A4C?Ez1@ iO`~;nNW'uIo #Fc4E o,W9jI1zڐ̢)i*3EWCϴOCh罈7@?K2'#Y$l2zgٙL'p?\uD*y?E `4݄jNqǽYS U4! 2#q LԜGjI1RG*Ko1L7~nS?rsGDЅY?c'_Z(((((( &kPZѹno?Nroϟ7n7H ~4$?sT_ǫGC?Ez1@ iO^\#V(z*W!@)?JZCOҀ!8?*8?*#S U5j_O5WOGUbv}Vj榽FGپO3;btxUs/4QEQEQEQEQEQEY?j }W8`glE[no7Zq}9a& nI~\Q#G*LC0rGJk $qҀ$W'uӥ8u'i!?} )T>\$?,,8;3V_0h"f՚( onXқNU=g鞴{չURzZhHduK5WOGԱ3=}'m#H@@^ǣ}WT_# 9PAA;I'tPmHfs/蝿f\=|ЅY(((((( ?M֒<}c~\0zw=)53/hQ@mm1H {cN{vR}qS XXmD1${g4UrqU2FZh7#~kjТ}?Љ@9*r:H14UۻPܣmL1D~9b`a"*EȢXjI=I55^CgQڬTYN3t}zKqTЅX @Q@Q@Q@Q@Q@Q@\JC 7SMB^BO+( %ȊW\#~\C"+*tO@Q@??ՊmYs29+F< 4Uo*oh%p 0# LH?*aVZx8PS@00>(ʮ6fFA&ϓ|sL%BUII9:[((((((."2F6rCU2cdF'-=*P<q4Cf$cҺe!v w(,_?4Kmǟ.v;:<9$O—{O ehɳoڤ>rOKI7D?Q= q:F3=MRu]?.7G>?"A/R?wޞAxS?Q=%:N{v;XYI㜟IQ=?4+(e* T%: cy6==3Si-cVAwرJ,!R5=qjPEPEPEPEPEPEP endstream endobj 107 0 obj <>/Length 168>>stream x0?큣Ӕsg8 endstream endobj 106 0 obj <>stream xZң05xyj%1b3;$Dƍ>0tnSY3~Ҍ/iSY}s)-o;pR K |Z9{בSn[s6g۷ n[;QMZB,K#BSCODZKݸU`poxI311r #% P[:#R%KʒM@GgSAn^>`Vܽ#~vbXtj:B2H !hPj(ud\B8"}j^-IZhԅvHHlzD&6oYdMڎ*\X_% AL\ ltC6o+|5^BC@|_g(R NAׯ:j&{B(g] \@O,PPt[@02*ӥZv6M]|2c?~|Aa_sU ?sTnIBIjE.iB8Zmzz*P.?Jj "*V~P>﹈VUsd* Lk l+ $˯W,E!wE Q R.X k^.&C@j3럾@ٻ}`W-~NW:F,-%"j`$OM +z=)2w&q[ԣMZ$jA`cҐĒ ݑq9:r3CPxE M"-۴`aJf `N>[zR(*ÙR?tBvl`pTLHb,!IKDZ^!U2ǽP~d#d9D͐s2ŝOG[ 5)~b!q"5菸oDk g$ieP_"Yal Oyxǽg|zo!9 endstream endobj 126 0 obj <> endobj 131 0 obj <> /Rect [90.7624 521.57 219.106 512.442] /Border [0 0 0] /A<> /Subtype/Link>>endobj 132 0 obj <> /Rect [114.52 413.808 248.526 404.414] /Border [0 0 0] /A<> /Subtype/Link>>endobj 133 0 obj <> endobj 134 0 obj <> endobj 135 0 obj <> endobj 130 0 obj <>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYKg" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Q+0-ʸB1άU9,S!f;hW6POl~^O=zt18sQN-ŮYǔJRX\`V裮iGހ0 nغڋ? cr'yS_]T} l$rF~z1Jy5¼9XcPr_J m1 ^HjUTr$ Wjn4n*bI79ceHEd(鿒z=[zh2Q{#8N ?µ;yK@F[:n[o)oe忻і@F[:n[o)oe忻і@F[:n[o)oe忻і@F[:n[o)oe忻і@ T&%)*Wi2hg?_'G8- 4)ly^ͫ98'_U0J Nd<5S9d6pB;F1,`1qˎت>LG/Z=ʴov4?VЍqT]?1=Mai[ٗŪ롫Y@Jǖ?FZشQEQEQUո3F%ʀ,QEQE7 22(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE7jʍ?*u vھQ@7jʗ-QEQEQEVa2\$>Ƒ]#$xQZtP|!j.7 8|{5HdTeI6m@Q@Q@Q@Q@Q@Q@S 0<&GրEGѹT{IEGѹT{IL`J44 9Ji&aG?hhfc~NF$2P`9hhJ*=֍ր$hhJ*=֍ր$hhJ*=֍րESy`#yF>ik|?ƝD{4f}>w@>G4>s./:_G/OM˹5Kw'KhAc2hR]4}9{X̻,Ŵ# L]4}9{XyܬƮ(oԖGko G/Ow'eњKh]4r?w4f}>w@h=|]_kt?Ə:_G#k?sFj/Ow'eњKh]4r?~3_FRhIEGѹT{IEGѹT{)66v*8֍ր?ކ6P- H0I;G&ѹ! r>Q=֍ր$hhJ*=֍ր$hhJ*=֍ր$iT<րEPP 2~OTdWI7?wqҀ-:aZ~Gg=cs 9ϠG*6tcڀ,f`=r=EQΏ Xc=)?.I%==P"q'ҟ*Z'v8xzuӉ$Rq'=N1 +%QEQEQE rNҦRYFs}G:Ompr2>.EPzPPs=lQm 6`zZz`H ŵ۫*rsOEPEPREQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE{["X, [5Zh"^0ԣƀ3[W6Ͷ˙63"Ye$K*">o93k4dс~>9ch-fX#+eW(3m~5s&̮E;t->_/8PcB09 dg=exq=奈@ 褅@$5QHcF-g*X/:vH[,W11V,od]2G5sOCNqnOM?4)?4dtO3\MnU3wV2hV7)7"&T+側1yn)~LΎD*X2bJ~gƬ{ .Uv"\¯"؂6qd ^RtPO]_)ȌHYF =G `U iZ\ @幰YfV88F11NQM'E7'uܟ2hSrhQM'E7'uܟ2hZia cq'H>h9E: ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( *op'c`㓟JiA9p9խEZ][,pASi} (Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@K?J&)m!sN W)gykO寥ZSykG(ZQ寥>gykO寥ZSykG(ZQ寥>gykO寥ZSykG(ZQ寥>gykO寥ZSykG(ZQ寥>gykO寥ZSykG(ZQ寥>gykO寥ZSykG(ZQ寥>gykO寥ZSykG0c /ZykQf0y +ˎyP_JkMVp *JoBQE*?P9\**|7xnZy#b#և>" D̃p ) I$n5(&fQ+Fl. ϖg!}|u+aY3+t??rBB!K[ Ъլ]΃Ვxײ}ޤMKhݝW'su9ռUF\`(m+>:rV$$$Q@Q@4u%7KlMEfg ʼnEkI)xfecWg[?ޥ<\7ן$ #czV_̗.ZlM.T[F8nɈ1ҤKq*9 ؎'?ި/nK*8 u{HY#3$; GzI0U4l=]zSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSzSz;斊())h ł{['U۷Me,Ί+6`c]ófj M'QIEgmy1lkB(C,jRRKqׯҥ55H*(IEG",X`޳?!bY[s,=jע!X@ (((((((((((((((((((((((((((((((((d(->99UhlK2u4%u8`9`Fv{zEխYJcyfϗ@4U(VA#8:楸ɶ3/3@(N@2{U((((((((((((((((((((((((((((((((((((-%Y$SOda]6o\!Dy=))ZHZczsVj9#IPa@Urwg1`HSY$RWhSvͥrY #"'}y jV#Ώ!fM9k?xۇxS"@$?>:Hgc`tVUf‚zQbU@֋y%Ys'\vi]K(1W;Ƀ)-QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE$(^WTAԱ)0#4 m:|c#A!.ܣ^M.9fvv ݿim;FXI$ŷuM͵wdTNծDGu|CPקݿZ-c ;R&Pk)GE*zH"ѤYI)!!A${PV+CKEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQET3fIT "DZďIɸWXQ]1%FG#9+{s$  giSןҪ]4Ť`;@֒]!YuEw =@Z#9es0 $}l;-KIA* 9~I$PyZ[wat?>.>dNoڝ$ 1mŻL?@@sbXB Xx#DېUk[jRf*XE)G޹ QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE ygXP R1s9%.DeW@jWR9O0w Hɤ}j4,|ecn7}~4 "I!'̒E-gTV;UFvϵ_Jo2vY_06cmր  I!img6eim|#chVWID wO@W¬#TU#QqI +J\FL5q#FA#қw~vrfls?ƀ3Tb7t ;}AȮ2x90Ԩmw1| q@ ȤPI"? BCtJvy8e'_\ք%p)$SrV]A փR7%ۿ^i;c u>YxGwnnTJ|=G$I##:ʜHP6}}?Z4,KH`UvzFh^̕rd ޣ> jX,DeSڬɩU$h9-OMBP,&D5g@oi ɾ<65z~ybFNxD(*$`L8_WQזF2`ޙgy;)H*РmI!W?-UqSQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE ,3FOSRPgH*zTRjd dGӴqf96 e*oFzb- M.J<_p N E4bvOI**I99IUn!xRAs{Unm!XF~^zP4,Te~J[Ba+JK,׷ҬEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPP w]z +DD'b"(}K1];Q% \;;7oo5)kH_ހyrg 8Ήo&Pimʯqހ47rG$O%AD}ϸAʹYU0JcX@ wɏ-Lѓ:*6〧R*EἲI45RXN2;gU獤 $@bB/v9&((((((((((((((((((((((((((((((((* k+1I;PG4e%Et=C o+a9h74}(}.wHP3$][,@Ǵ1NkVPwrJtUȠ!ݞ'ʶ( m*ᤖKat!ӥiTSU}׏jEw`no`Ʌ䯸9VQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQESNpqɧS['ڀ!6= ƬU]=BD`bPHFz@ Z(((((((((((((((((((((((((((((((((((( endstream endobj 129 0 obj <>/Length 1542>>stream xԱPDO ZD jb> endobj 139 0 obj <>endobj 142 0 obj <> endobj 143 0 obj <> endobj 144 0 obj <> endobj 148 0 obj <> endobj 149 0 obj <> endobj 150 0 obj <> endobj 154 0 obj <> endobj 155 0 obj <> endobj 156 0 obj <> endobj 160 0 obj <> endobj 161 0 obj <> endobj 165 0 obj <> endobj 166 0 obj <> endobj 167 0 obj <> endobj 172 0 obj <> /Rect [114.52 512.808 248.526 503.414] /Border [0 0 0] /A<> /Subtype/Link>>endobj 173 0 obj <> endobj 174 0 obj <> endobj 175 0 obj <> endobj 171 0 obj <>stream AdobedC  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYg" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?Q+0/ʸB1άU)my4199?S@=1 "{g?22yU#6=9#y -68<~}r9XXdX"*z(*#9^A5bKa-@H2:]GVjx\dӿb۳MX7 rAMP 8\Mk5(1\tn ll yȠ S@ NO: ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (*) :?*ݕr=EF4h^ٮߩ˹iG#ڸ 4h^٣R/()Rןh^ٮCI^W.9ck%vΌ>25ʕJ)*65bgQ#UI`vEQEW)wom6 #7qGo8˰#𮮠8U\<9끁P&>C|=qIj" Rc^W4o%W2,Qz:cOANI,ORIh?VhvCm Rk0h9$uʯko6B X,2"@$FXF[:;فbN,f''?ݠ ok\yU XaߜՃzA GQP>WZfFhdbT6s9@=OcWuiu4bn^8. .ITGodwJ̒i?$uxg/S/Ҙǩ T6 [Z``(ڎrEci<3}P.A㎇ڟcrZ)\Rr*8J8n&{vTYfa8>*k˧QȒNWr`;A޶q6zRz 9U1QȠ|E]aL0U!sߒwospсҀ2罚(`S owSXP|~q7g+g\l,j?,}x 俐[s hAFXGR3Wy$OR =_@7A cn 79c%H\Ed(鿒z=[zfYj/w$|Gr=V3o=)rhSrhwQMw-րE7-֌ZuܷZ2hSrhwQMw-րE7-֌ZuܷZ2hSrhwQMw-րE7-֌ZuܷZ2hSrhwQMw-րE7-֌ZuܷZ2hk>inR®\OO]n-֓-ք&(z7_/TbP2J?*kqKgU|n9$>~5\(81#G'׶}Ob۴"i|;T=F?(bS" 2NxTo|fF@ѹ)  0 9Ji d|R~n~٠<ߺ:zSQ 9-i۟>fF@ѹn~n~)>fI?<"yF>Q|Ԋveњhee4н|^?ll?Ə^6_G#k?Fj/?/O헟 'eњhee4r?{5TYZ/hGAwee4}9{X̔Ya]j8QߙApBc` hee4r?{4f>y@h=|^?ll?Ə^6_G#k?Fj/?/O헟 'eњhee4r?{4f>y@h=|hQQ|KԚѹn~n~)>fF@  FmlfѴ|Rn~n~@1"'$ssssŴ! r>Q雟>fF@ѹn~n~)>fB'@(љLy^??Ƨ2Y+on~ӯZ2 uBstZ{ x) \JK%r1PgL=jΪg$HirXc=)fbNOOcZ%1`Tb1 ,S<N€3Vfل7|ŒsT7\ͲHwG.#ʮyi|w)0q%ܟ2hSrhUTfhܪ/>fҬdtѓ@9nRa2I_, +l}1L sg xtr!RW RS<~5` \EEr? b xǧ._3 s7ƞSI12$z3Z);Ӽ%O^Ҁ+Ssa2wlqW5F1x;'E7'uܟ2hSrhQM'E7'uܟ2hSrhUk/M>OM?4 20})s؊uQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUCyIp'c`N})C i󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]tn>󤢀q?(}OF:J(wSѸΒ]t-=*:|_DQE1F~*3C*ܡuEW-$rJw9篵Xnl5W;KhF>Oiy6$*~R6q* D5R-|*UbmsNG $xsқ@r#uҞ+2q!]Ǯ(n/2/P"z(?xԕƀ"oj!6Ynҥx4렭a12ѝi>@O9E1dBIffb0OH,P3($3f|\ T'${g?awlm۳uQRkOoMkۂ0N1G5=P X(QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEL OESTmԕueU|ea4S6Š(aEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPRE &)*6RTmր#>sQE ((((((((((((((((((((((((((((((((((*HTu$_x(5QLAH@4Q@QOQI}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bQ}( F6/Ҋ(ؾl_J(bR QE:( endstream endobj 170 0 obj <>/Length 767>>stream xٱJ\Aѻj  h^m <_|nYj[t錂(茂(茂(茂(茂(茂(茂(茂(k fuc fuv|٭oG`vkg?G`vkg?<^.^{i fv~mns f߉t :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :3 :v{ fv~5z[;{sqn:3 :3 :3 :#p:QQQQQQQQQQQQ8{ endstream endobj 176 0 obj <> endobj 179 0 obj <> endobj 180 0 obj <> endobj 181 0 obj <> endobj 185 0 obj <> endobj 186 0 obj <> endobj 187 0 obj <> endobj 190 0 obj <> /Rect [78.5197 359.808 235.212 350.414] /Border [0 0 0] /A<> /Subtype/Link>>endobj 191 0 obj <> /Rect [78.5197 503.808 235.212 494.414] /Border [0 0 0] /A<> /Subtype/Link>>endobj 192 0 obj <> endobj 193 0 obj <> endobj 194 0 obj <> endobj 7 0 obj <>endobj 77 0 obj <> endobj 210 0 obj <>stream x] <on.%x #8s<| m~OqͩB1BUcRm}t$fl6_ߙ4?0>9w-Q;3[GŦ;)&T0ʢL?̺Eգ T5_ PdP`(Qm̪ݫJlH6P~KcUQn endstream endobj 98 0 obj <> endobj 211 0 obj <>stream x]An0E06)MɢU "dĮT$ij=ç:_/yc[8F*{Mst9La=1ݯEu~J1Fy~w>R, @|Dž񸰠y7\kYՏkԀQiMf6+0X9Ȫk0HDžF"4FbiDИ2V5&E5b"Xe&`(.o0lƶrsZNjxN6Ar D9Ny!wU/&8D1t" CkFE1 Bp|Fѯtt ENb\ exJtF<{)*Uq` endstream endobj 76 0 obj <> endobj 212 0 obj <>stream x]10 E"7hZhJUX@@p4 e00|K/l7v⢛cጋ1M+bRmˇgl>_^55`|wlN][*<#{ hCp 5T>uru$P%p`'!ž^ܞ]KײkWNBb$ϊkٵI߉xd^g)\ GsfKSZmZ endstream endobj 22 0 obj <> endobj 213 0 obj <>stream x]1n@E{N7`3cKhq(Jr5 qN`wCy<}O8a|[{ grQaA5-_?loG]5AKuM[.FQ ÿWus?q[klĈvxb ll [[ÃxCP GҵZ݉:{z %EdE^> endobj 214 0 obj <>stream x] <oیE/4F}dpKuMc·>8Lɢ|Qղ6/վMp6(/VW UszLxk {@`s-/vvEw"0HhMt h iÓJ5SJ23h>oMqK 3f> endstream endobj 24 0 obj <> endobj 100 0 obj <> endobj 215 0 obj <>stream x]=n0 FwB7 .ɒEL"3H':<!ERL}#P`TYmj%K hTEBQu}(Eb[-,y5ˁ endstream endobj 66 0 obj <> endobj 140 0 obj <> endobj 216 0 obj <>stream x]1n0 EwB70:6 \%C 㔟= p?:T8YwD'TX'Qwވ kM&QBD3*\ԥJeYz endstream endobj 18 0 obj <> endobj 70 0 obj <> endobj 217 0 obj <>stream x]=n@{N l=Hhq(JrX€0.ř)fv9_eiatnKLy1+ʼ _?sB\,zT7ũK׹iim>diUmxīv5+ A*(hAуt.(qx @<x`:k&kpFik&kp&o`KAFb`GLA&b DT:/qu^ X9OaX=(eIlq1<ܕ#/)ǵ9 endstream endobj 59 0 obj <> endobj 218 0 obj <>stream x] w7684,u1 P8t:8ܟ~D#zB:5*ui*N5@X 9msOVR^c R!J7tAߪ ٓ> endobj 219 0 obj <>stream x]O10 N ]Z2D! }I$[.%Mů F0Sau@vz&kYU{%hy"(6ֶIG`BmRgh.q\c$Nii 83 ]S endstream endobj 72 0 obj <> endobj 220 0 obj <>stream x] w7t( @(|{'G e.SvOA?u$,aMt<5Nݵ*6\U|}"h n`!x{궒KTI\JoVb+LvOβ2l*~O@zM >7Vybm"V endstream endobj 121 0 obj <> endobj 221 0 obj <>stream x]=n@{7~$n\$\@V S,}fFrC`(]?.۰q=Ͽm8õ?s2NtwϥC?{_,eҭ|>nWO؞Ƕ῟6th8F síޔvǦ<ŭ3+7Y7MAݰngwMAT~WT`0E(*0*1ԉ4֦VVk JRFSP5RT0 @h4BMFH i4A! &H#i4 na䦱n511r؍cw5v6.#e4rظFa2r9l\FN#iq93v9g7u ȁRp*xCAo9 1! s7dFܷ+xCAo9Ȍݣ> endobj 222 0 obj <> endobj 99 0 obj <> endobj 195 0 obj <>stream x[ml[y~%E[ԷE[ԕdY]Q_e&EI噖t%[-[ήiQ,rE7(Cy),ӡbC 6"q{/)K-AӮ-ypyysuΥńU5rP=d}BvC$sX7Qم#/>Xd+gl~4?zWF`<\Y_D{o_XlwMyb.k%8 ˋ:|\?e"ev·D޿g߂.^~,=]Z ;;~l}A5޼NQ.n"VQX[fH2uI'S'nSձԎJ^Jg+'SR~Mn/)-)fX&”zLP*>דR<%⹸)ttO o%dCOi=o CŞ.|3_Othե'`DgQ?/N}Vzb9IS)I &%d+Dv qI3l6-E 6$%SstP:L;Y%SiDfDa9TMyLTg ute9CWl[2I_v*2.#SiJW,J[ ]#j FF4+3R䠅tvYխEWL)KUJxۿ ss<*"b;W,G08 RAɒw״A[<AYi%.g#AYe26hZVqo *X.ձe55fD*Ύeq9(fX*q1^gךyLkjbRd:Yl+Q#ʳ`mtŶU~Jgy aP= y:ފI:*bUgR1z\nEm1pQ=_TMJ&_ _TA`3MfmWfܶy[wq̻a+e%K-zH|@{q]- lv laI2!{B/q}ܶ>n m;q }ܚ>liĶ=fbV(qLՐ))܍0? Fv; [8gݝwx [ y,X߿'}uqjs6r$;}fi8(7*:BDzH=2fHC nFM%aʊX`n%d ܻjDKl]f.KRtt:ZQ_ݸ k1f_@J,3kHG,; 3|;' pFY`lb7p"[+Ux&^H/t:;0}"oQ^w!c{Z҉THƻ/W1R@Ư vh7h++%5?BQ鍥>It(87I|M}>l!SaԔ{+Ѝs F=d3bɜcDm9A N.&cJS>`Jm{lU!_颞q\WFEYw\`{| ^rNOrMQpKޚ49e+ w檰F18s`-܅z̼%=f`tyzy8,t.`d=d3 090e9K(K/Fg-[z1zҋѓ^Yz1:oh>Z'^AEvՋ޵EΒ s"gճf|†Ioi ԅ7ů Gw6Vj"\9 q؝-FLkEQ p:i/I_įAxb˱"ih1)&ӑz܈]k:ۼnOS@8h4 (}F57f>ygU/w Ciy;C ?8vt0;Euwv()&l#bv4ORR RE´jovv4Tx|jlw9:[(!M0qe޵^5܍wPڦ~ o e0 d[\54J=RdL\v"hREYBv UM$H%ho]Gi}5|sH VYڎ?=S~zn3_2<ځB*WXkjɛ1 L,1ͩx?vrSQ%mJ`琺ǯnэ-6$&)mv&~ocfSIG- ^a>n3n4c ÂSJ/O[U9ax\lv^Rщw(#L"M²SSC Eqv.x_Bi;Hsر0ȡ ĕ`.33ėE_1.ߔMSw7o|ꭻ'G}p. 5RivY|-p;3^--h <+qH[D6ᝡ>5+r)[hF9s_DQjr(r)r)r)ߒ R.,rPit bR#V0^PU UzPI?&UB!~~m/{s-zy Qbvkk:WeZ7ۨr _j'*MuIEFkkkkkkkUtS?f ۯNW\iնFϵI&> endobj 196 0 obj <>stream xYml[y~dI$%QHɾח%R/[eY҉#ҒLەjvgk2KTE14?v@C42`芭mkbd@n"y%˟a{}y{suιIQ#$fO%z3˥I$~:u: +?nں.fwXZx$4/BVI#ߨ]"ei|ɮw?\.XQ~} ϖ7 __\v}Agܾruq_PCk)34q~.> )?x} ۷сK$$g 6uOIɳy9݅mt^*+]ټ.‰ W J-1W(h !4w3\^7VK勐hVhp1\, axK6%KbzxJng}tG̸B EPZP`*$Ԡ-&w:n#%=F @bB,wB}>q#TyјVUUC\t7 zAɓy/UUmʺt|;Tt1R% <ބ35ֶ ft^dX`J1ci[o5Q:7g5QD*azQˮ%l s@5-Zh|HwA/ lݝLˠۍ bz^(YP$UӤ7IN@>)|0?]Vm lNWԅL!" bNOaKjV9}:_inNKQJIR+Uˇm#W}7cZ_n[ v̒,n CBX!j1௴5!IRHiYلk."[!)BloJRV(J'.`l A"l0+.CfE2lV\v]fvrY2n@HW.7>)IHƶٍcWF$?`MV6ntاAKq}\Fa1.e7q 31+sML^԰bڊ-Vc'nSe?I!a5J {k4zrOo),"F0H8}bDV"J8nυDBn6z}˜I9.dclOK:pUzUC!]QԎcA^).MҵpK^k!Nbc/N`: p }ݢܖ>GڔGM1*tQ"}r7zd-9?cKRK٦sX>}XyfN0'?,fx3Uݕ/z$6x+GP;׍_l(\=>K[0VOa8"Gr?ib&p׼5crBiY9ู&, Kr94sa' @y ay[ز@) FO3Bggg93 P9xNyN9ap9 ʖ^^.Yz1l3^,-[z1+fWL~ΆӭZ k8k6dγGT9ϡQjV6/ؐ鿇q߷!>oC&x/Y56dM2e`C& 6UdC *zV _!VmȄ/k۬O\ #_O ,JG$wvX'E"R/xO`ɖ%%}H*~&~ՊmM:Z砀=*.8P.^fuAX@*VI8A_**v/L͞:?ye gJW?tV(}ti:"ʓ`-TSW2Z-azEȮ5Իqck4JyC+ 2ƻi,=(eQoiK ]hK_o0[ƛC?<0u|V'JܩRћ;TG]K}߃@nlCm[͵JDzīy/d*\![@UŢ<7@WOzSW.T_`sE t!.^ot+BS!#­w%p`IL DL7z]Ih&ocS7xRZ˘/^ѹL۩+L^ab#I+͎F"ϧGƏLNd3Tػgw_ŒGz"=ݻbѐ2vb]`/W׏Nݯ#00d XKndx}Gb%o%n%q'RR{VIJڭ[RL|fǯwXh⢞eѤ^z~-15bZ'Q[)$qP|pO?4D#(JgmĆ\..n7U7<687rnp\>=1z?ւa?%K{x|OO%ǚή=Kz=\zpC嫱P)Cq"RN%hY&U%};bcbXmJ֛&ĠU;<&38pybޝzK@4stv zo4'R7f>ڷ#wO~r3PzG8miW2BlkASjΉTlVg,Mx!ܡ{W[Vfzx4a9WCb/1=A+=¾P[+M1;o 1l%튟.-3&&fM~lF~Ϝϟyz/:^qϔ9F;}M~ (Bx\x!R-MW2YKwyQ+rOݴ}o܉֛ WlOolTYӌzpz{?'F{'H{DEkݏwoy~C^ǿrOr/YZ3v8BՍ̺E endstream endobj 223 0 obj <>stream x```p`F ĀXX@PH= endstream endobj 78 0 obj <> endobj 197 0 obj <>stream x:kl[y%/K]KZ%ڢHHJheɒ88&mJ\͊ c6k:0tÆ ۊf [d[t:,&zw.)SeI=s'BjS'Mk]FwV}kÏ*!HH}73;!gϞY]7t"9X< +a_yه\_!tڪ>?OTKª) xK_j:̿B&iN#{.R܋^o}0S|휬R<(B|^.ثZ,g֋ lZ|+2"4`U.yE#3|HU:W219D%IJkUx:!R)_+Li|6 {N`2+̻VsJޗB\Z&!A8]RLWwzCk@&4*#uɵrZx!(qFQa#ɉDoW5^]  Ķza".& eu\kqaz1te_"Mh!ͤ,k4~k 8; F8D )Y f&5+ r5L6Y]>XfHrƤFWZcm,9QǟѨ4w(<6 k{| |V]>~+yd 蟂սXVR#GnPJ,*)nb15* yBUre6Sb"v,] &+f 4Z8A8:"cZplU"N(RjшcZ[U*rפHX=z67UG%ZC<@ plpT?;?;?.GUcLC*\k.Im!T U-B{hQY*B{wUKځHm9djܽݧщ/r\'X?D}Ԇ X5숅 ,x Ї@'))|ej{{JP 6+4 !ʮ5Qi5"˱m8^9 Vnr2/nr~ޙO` 5BLV2 .|?0yKO6\u\@oV,$(S&?-p>(z JD,SD䨓zs~"VEYJ D42bCf&ԗef+o?.kJA>\EIoe#((I)wAʔcp1Lࠣ{v~_Z4x v( }SD0|d,mVtfS!@ x꽲[CTL(Te!| `E0;)eI9ez %?Oc=QE8jC0dPn `yriER3*pV ̩7([dg 8SoBh ʠe&rkyģG<=x :x :w&8w"w"P;XEIN#k: pѕhх& .1bt! .*iG^BYf4Zƹáe+1/AD >Dnl'،?1D×et>I={ާ،Z3:O×egt "|VY#pb5Ԍg4cj%@g"$6\=)W8+H.A""=d|`'ƒwBuIKEbHu2Bc3pHj\xk`VxġOƋZZȯ8)B5(B[Fy&|Mq]"x40s5.xmOw% f}~.lp0g۬A@FWDG 訹c1;svo4s[hv6{W&'Q޳6Q9%<&GW/Z30#+@;>$_!aCj%n '\~x[f܂ӹpnv髿A!S+o=\;4;Ik9Á5y›y-1GK3_$))Ǚ33LFh1[,3+<4bX?~ !{ +\OYvs'@.Kx7 5tEt&o&#t{z1mOolԆqןg}SE~eෞ8HO< Sg Lb@`37cj[&/KGPǬʯ(u^XrDb摱8mDjd_i?W*7o1I;I4>`DiA ".SItx%_`v)pDcG=casӡ#TGNML^\}~gOff"d(<熫y `<ϭ=`AK(Z@"O4X?0vZv3 1ʠ+ီˏ6?Q3Q᫊ۡRotbo<K4\CM}@`1R#GݵP[_i,'1`і cG36lA2)73u8H`T¯:x5#;P]Af\W+ǬKKSN+=}'*8>d0tMSNCL)wRI3?ƉLJb wbfqK7Upac<^/Gٯ  Zvywpۯ9U'%HQ@S~H#zb}&0e\+}כjvvJgG{U~ҽ_)UNZ=YWl8>b$N6\7uYwH)x{X!{R4QQʒo, H;^*r+,4⾷c`HAV yG'PnL"k)^-^Պ24f _B|H(z@9=`c푦#2aycFl|v^^J|{zH}AmţK4r͔z.ۺnO/ PGzc)X8074= Mz`0[,V0fsd/72:Ҙ͊M3KS]/K?w/K=,ÝXA5kA'"x AYقՀj5.]\Zr'OR.+[eu̼7Z,u׈!P"7icKzi)nZd.(eC=t|=cWqLb{Pv9 BTX'.*>A~Cf9lFT_?rXK]|wic:9-jK9z, d+~ˌe#j˦뫺B -5ayRV@DڝXhg9*܈wlL$qv%UpWDpx8fZ;mk|&|dpŽlsF,L^U7rؘ(Mx}mZIt M8*zeRZ`3lj'Q"[Gv#,ݘ?G 7KQ͔X|wݍ/O!(Gv+ra%aŌq)C -ľ)xP\e2 a>5 J{<~ s-̳063?frⓆ'fN. dYgvZwT!H)0V'daRUH;C]E]Pg'Rls579it/OQcT*ŷ.߾z<)fkBquk;l?گѶw܎vqu4jN/W/i& Mfbm~O{AܹIݒn: iiFw^2A3e^΍zϽν 4po?(I>IK0z;2_{w.NJd\+[0gK}~9Wc"Yg'Ĺ_2x:x' wJ}FBQwVcLxop_|=|}Z ,Z endstream endobj 63 0 obj <> endobj 198 0 obj <>stream xZ L[ϹM(K0`'/I !HFLllZ.6)cR54ۤe6LkImZUUmQDӤU]eBe՚lLJ&ZJTwa#t2pP-3r21?&Y2L'\3pxx&@Q"+ȟAl KFGҚQ+@ ܵ%:<:&*{HxĖ?~z-INobBLK `fc `HraeI9#:,=OSXO4ρ%^|ˑnonv GCk  a:0!" 5A|vgFo{gwO;=w̽#9±q"<ԪF=bC/Ӌ|?La)bb mb@#f"Jm ӱQj:qƗu#"C͢_jUzbs]'~1NM{n7g.>s*)vϸ3}3D]@E&1PV6pJruj|*~JVsb魲5y\ռ}qoR{|c6_nqTM>3c]MSk"--]5+6&/i"WhPJk;"2]Nx{_G<0;!TW!c2PQHh)zVN*GT:!${$hSJ3cI8Lv^co>Y絷zgg9-7 Y031)>߮>7< \"5PtHX7+uveћc$bcprqjuf&B'TYJZ-R!-#VGfӏ9Wֽ~1CB'rZ>klHʓjjzYq^zĽ^jF*q㼉Sɦ_rļ&dmu4\)PWהiwG,=bbA MU߾q<1ޯ/ &ʾY#{hR^>30<նXb6x]2=nv֛ SZGVo(آBf|WY[ʗh3k՘Y,[p,&I꜏9<sdY;$TtH*q7@O ctKgKO?[q-EAC5B%_^eQDW$ɇk\6닫Lc_<+Ep$<ϓƧ(E)JQ^I&7WiD4)$wn4YtGlH>n E)?.f=*#BYy쉞C TKzI^?Q| ]"}WLoҿܢm5`F2$R˱,z%ކm`)B#wP )B )B )|@GAQ2 x?A N@KPiACd<)â>G2W]"0PJJ(E@|WO~Wz7{;5sAmul9ZB77 endstream endobj 25 0 obj <> endobj 199 0 obj <>stream xZkl>wfa;w ,xg7bfk'ل8 ٵwǡ hJj%菔w"(??ďRGZT  ڂ̬@xB==scvG&AD0ٱsr>{t,?%{GԻk9_qmO< 02v.Y"C|. g>%ˉ衱.(0rhޭ7q+u{~O~mO> =GMΞ#|'㯥/?}Ov)1JF౒:k+B _Snfk٧-#"% gSQ`Zzwu!aSYDFGP8,ɖd}$%cRRI ZQcFSқʆȆph*LeK.;_Pk.f^dux3eu&FjF݌sm!x+QICYIi&ere2㜇Flڶ!)-vLHxLVzIDj.&5@$zI1\.}ԨT;h)} <e0˄Cvְö.;h q2Ǥǔ>+:M[/FɼTFH1 /3&}ή.A,lA&r6Sr}~sڷT3e=3+[ }v-'b2h靮27% XR `IO(rdouIтP#lhS_ [6mv hO&Dh [BgM*ڕ#, .0 )ʕQx4ijBlјl6KVr,i\^g<\^o\̒̒R+Mc.қC =.> 1ٹe^1}qm\f~/qF|\.G|\6eqَ\@|\,ӘarYTb뙼V㦌Ee p6~Y4=We8--MJ:Kќ(W/Hϕ8kL}ZXs9OzY_XO-sa}̱C>&z3ݟEł&LD>Rejjɑ3-Nn!X-2C4JՔUV87tw 6o^L=9.s|$g]Cgvz;G0Ki8X!U+_@bC9>.퓇k8~̱䷜Q`2Idx< Uq{~2-sСsa"M曤i7xPr0n%^Lxu%=Ubϱ^GՂ~pDҏѰQK9 wƸ\] -0'z୦ EB~:7ng͢Xm䣝/nL?B#c9*(S+Xs~ӹ ԅVU&aX:Q*u2ր2G|бGf_5}o>G᷍x_gx|D}GzQS+9Ɵ2_VO!oֻ{֥D<.DS"M$|ЌJ,AR8=lI-ڹaQ|?mo;Mz{|K:ωǤc|;)xyAG|2:&T!aGl4ͧOmk1 G`όGz9}Jǯ; #EجU 5IQؒH}N&jU_4p+JqF'r7ߚ*vPH=B}4mQPvKl1hkl U5'?>ۂ%ڸ6h.8/9%~U!ElTEQi]TJqϞƫkU/-Ex/WV"HE*RT"HE ! ֳ}%-DY'q|4J8w]U.=N34Ms_\Hg)o4u|z_7RkV~+ʊT"HE*RT"ȗ]oڒ{'O~𗚙~\|tn_ڍ$}7_-`F endstream endobj 101 0 obj <> endobj 200 0 obj <>stream xV{TT?;ȣ8>JD7Bb  (7ԖꡈTh\`Dy䫋z#_o~u[3k9ۿ眝8U3YT!8l1)#=4J|aQ웢>5'#=՘4ėg.H2p}b!ۘ3%7)yz腺ظ9#x ~ٗ^䢹 n  ₹8o.q!. qsn8q)Tn7EqxΕszr> 3n(iީ'$VgW1;?"*J6faPA=^rZz<2LM_a+uxb}0M δ=Dy$-TyޔJh{sNfa}~g $sM$cZ T]I5%4ͺ2OEKZF R$RVIs{[-8K7+懼|9"Jl٬-ҊZޒ\[Lb`9A9/ ?9r]}`qh!?OLŶQ5^=fzȘ-Tz¬ϫqPX+<^z[h/5+`w"@0<}#P8 #[^N-_B_p ¾~aݟw*Tߞ!bC~: ѐ~>\wҐ%11 ]&+m*l*a4BV mVb_$s CnAMwNQ X!ʸY6x]k#P7p ++MV0+_g6[/#kZЕ-!h&TTOp%f2 %e:DFy4L-;yy8o?Z^27ȌDYꢺ\%O1ܞr`i0n[T_UR&n,Py51tߦhcI+%%j%Uͬ MS(a(A,/2mD2Lx//_|&;]dY K7lذfMD55.崊cqAa THoO~.[KX. 8 .7שּׂԊY-e@6vMv{|ە+.Db9~gCƱȌܥt)O˥:6͢b9DIJ̈>~8]'b|~2k%ǟ;u-b82.aKqpg> UP4x~0ix`Pǯ'ڕ dbPCvqSm jfG FhDBCM5KɟeXT|THZ3" ]H`kbg_Bs ˦S6NHtưmpF,ݣT./ h VmYu'ijoW\,;rD$/nGM?\/CSh*\lwXRv7rNA3 !7LW$E^ߠ:M `l48!:s:lz 0G 'FP4DDa)`'Iq$e] ^Qv&|`f O*i:˟9T+fOLyQ.df3u1[o%>* ӦVm7rKٙ-9;eЩV7 ^LŅj=.-PѪ ڱq$ٝ+Ar&/R . 0]a?S2֌ H@&jFM-;,tH._Dp!>طBSB)yҏvV_"UK5109[~C}:'^bmr]l1z)8rqT}suI7{nΟz$]~~;^πo][Yxrp#$|̾PbsliD3^CH{>Lupldcpk ̛! (v"Ϯ2vVhqgC8W7T H2~PKf8ǩr'HX 2`-QZ063qa뫮:\`+>Ev&Em*X 8/ P4A&&෱A ܫӘĴ68 !U }2.o?~8[m~Zq nEІ LG5~_ K*V1/<57ӟŃ#5X0yZ}H#sU~vl%%_l\VUj,*WVJefS̗.gZmc{nw=\MF endstream endobj 67 0 obj <> endobj 201 0 obj <>stream xZ{l\Uz?>focq;1wl8=ۉlv3 3+ qb. %.JhU%!ԞIn vjKT[-U**j=;wؐt]j;|;;2㌱SYTk;OxYL-}߳{/li_ZXwK "3m|柜K:w2 vĀo1VDK6o97ȃ [0ŹʂE1v kx.>/-- ~,32F: ckڹ>cv Ň1vN ѸDrX9J}]ff^O`3zqI[Hbx#&4ѫ<7243$CqP>]"P(5D/ASCٞ!h0߈ p2Dn,ĈAsuԝ&W@"a %AZh)pGbqa0Ð#!x2(4_lZ?6h8[(ɡ601Vln!D<&q3K"4ǜD%UؚrkMh rb^p!pZQdPQɪ˺cpoC9V[Tx,D wZ1Sd/% &׹uT"EV1 6/ʷ@WrUk;(Ҋ2$fSѠ(h"?2J($zEK 4D!xZ&Dy)(D|l0^*K4+"¢(@> OUXBӤ7Hrj$5i%NR"Bz7aL; C>4rл&25d Ag韢 $Qd*o쇊vmL 6GhS^Hxя#׺ |ƻ[g-eoIdTI|Ru QEq$4-^2;m tgcgm x#PDA[D VD6!4,-3jyG:G(%)(EB<ė8X0z;>3$B@r;q@,N[9^V*Zm 6qkj"c[W|'uˑ92A8C& )! pȺa@\BGk :Jx:Fx$t$`$ I{"a'!`p|KZ|tRE)A%_-"䋠EoÀgeO  4fxbdxgNgvQSZcq6̢l%X`\M|Aoϻ#+_($PUR9ڪpb\ˆJs¯wr ʢ:~eǕxZ{<)teY@tzۥ7_kEiG K[ uhimn[} =]~Q^n:;=2A#ήrQ{f?[]kܡG5O֯UVV_6v{oW_Z/ Ux. x=p!FQcp^v;_iMէdbp-=~οEƔ+YP<$2l͎oGcL%:6V2YFx_M~Zb?Q^ؿ2\( pn皮riH\ט>TU<U$u~Sƻ:Y_vE9Q^twtcZl[;^_}0YgrzzwWrY!TGU6qRC_Wn Q3!#"2?j̦^UU?l[b_uօ|N$bG+*!(C\Ħ'V5';~]^N:C#J3?wy;;:2ktP1P?8JZ3c[eCc]w4CtqFœ**Pm]G;:=)ϭ40XsH%ӿ--ϯmx?\T{פ8PP:vMu;MnQPwX@ 2B5L] i\mvM(9Z}7< ߆#BTtMpEr* Ͽ}ţ/9>b|S铕=|"p[1C Ge"EU2.29%ӃXɫn9<\/g@`@,IyqT`G -tDj+ggWbFA"QrDp0G3]=o?ne8:utLxc~sE-xG1WKkqki6QOwz9n)9P&P{f,6zǒ}wte::yV`̕[Cºp׏(w U:U:8 Цc}Ν*yX,P_﫯ƃ<.@ıgy6sXtq׾m\hj\nMkݵk`iWk4#kV!BÁ Xh4< Q eb^5y`yGӵX vv3ɻ]7Ϊ@weR<9oZHo~̟흘>anش]'*'05rXfCk-nYО"8w18i^mA&u*U*u43&,4${NҼ΁ܼ{]ES#GZNUWE|efhUL\Gv {MN9+5]\fFBPeYP)6EٍEHvx=+3w2?Q'>'K?O;f!/#u[tq,'B /Ep4[. tsuٲ,gs7)iKy&/\FQ >UnZ/i(j/v2 0e,7q/~%WC[*E7Aвm.l [dI~{{>CIC:ZؗΟ> endobj 202 0 obj <>stream xmU{T(Qv%eug$V  Y!n#P*y"kYbdi9irU`\J 4J@졠#lR{W?w~}P |wˍee9n20銋Q8?yCb@Yyo om͋JJ@B⒪2Ӷ6<4,L\`>ӣ55rmO 萐m&sAEnp^}EhI) yXF~f8dmY’[g7RwhzmDȀ@ %~t(mFkz-GIh%zQȟ@Ч/ASSlPsh=!*fsEdQ`;01HT9 9OQ W'ǃp+*[M*WX40ؗ=s^X};Ds[8W*4r/ݜ+ e';puWA|H,Ǿ$dMBʔrT0~A'X a=IooH&DHqwYEY;Wn]jR PB(i*B4x 8 {cGH,DIaSS=c7zPXy/T }"ptK+Mi%:5|a S$]-sEjܾ \FiOBpWI(uIei90ԇN-TG hI `C5]bzbkoc[$m͠H|W*pP !|+%Ld*׈H t20qgD M)gO[ @uu>p෿y_^/޴M֩rhCCsjO2@}ɕ};5gn3U%(a /I؞IbLgFKGgd¡8,Y#`> endobj 203 0 obj <>stream xYoLSW?{(#bH&ZSDEkQ@_N TZuneq>8d(ٿlY`6?Oda;MɖwO~s9sυȅ)AYP8uCo GY17 EoPD8׸v#'e eC3:ӿEl$4 h<LtU_,H.usK&{П+y% %cT?qz*!W~%Jg^ jU*©6_琏wm bC#ăF! $`WP7XfJ ^CȊj/Grrl)6A*:G}މL}.;]S3M4Fz_R/xǓ,*j2r5X auS%fufs@jZ^ndUF)݇t3MVͲXLAq&6_PXu̇]ts2.J{CҗZx$e-oq'=>`_EBԺjC)nUjV*Tgg[='|Z=vgP55ƓΝSOx[fVo> endobj 204 0 obj <>stream xE]L[u=MKsE2Z e/ u fGmv0,11&.⒙ed~&DH dZ;Q"xsOn'?y^u㸆cd>?t egjo!ũnX|UI<~h tyXEZFL[t=60ş}<;ϤҊxo$NN*9%zf:;S%=7qx2Bgxn&Tc3Gm3DƢ1W1? nBʚq̬}y-w s繟uu Mu&Z^_*|lSױW]78TF!q^+ؐC/:D4)8-CғcN8wauONrv*W ^\핈F> ?dڥjM]?X>Y+.}vy5;zp=<4L2ެw'+npM: >m7jnynT{o#RDC|i.錺b! vVFN-[&yUҖa YuSwn$hwpJBU&`&qI> endobj 205 0 obj <>stream xY l[y>>H(ReRWdփ"%ٖ[#&m"˖vkI$AZK$Űfv9aI@ml(-hb 3Kʒ_͂,Vߣs9"R@"<޺hOݗr.5kB}'I/`_PΝz6k~_NM`> ~$>O5~;c;!Ó)!.uj^m <Ci_'+?|5m#V#rd/ivpf3wmCaåfֿG;GTH*KH5bT wun.wUΛzDLdrRp{<*I$&^!ĒрJUJTN$aURGͱ>7mO#{܋qIP$6%RZNMu$71ׇYLIy8Ĉ jaPKҝL$n Y%D":7p\j#d@|ISiq"*Ʊ[瀽U.7 LƤEiD/'ݩm$$5=97-@_!)rThJ&fT: .T!jM^Ȅ(d${5VM e)ma֩P?XAԷ(]4}7ө*dKXGN[X\*!E&ݲ' JԩTo@*@$5?6 W^@- ľ5jMO S`Z>PH|p>`ܡ)iROT~h: R 4S.¾le9حϳ%6$F6 &!C[1t]jr($MqPJ}gpQ)_)ht1.2Ջ~w5T TҔ.虵%JgmX[E֖+ikJ %mb%zE]5$aY tH@mX1Z\'+&}˓GJ!__cV||B> kk!k kE4`[{RɘfJ|5@6"6JwjYF+IߴlZR"uőȘkWjfE kGny8q}K;'zt3u2A #*Rm5K:jCO6!.6ȇ.7/.n7"UqD "P,Jې\ hWCSbŠ,I׾E T#$KUN%UǗ',e [Gn$KfŒSRSb)7$Kd7I-wƔC?CzDS,#,$jL={sz0*z; SI7M:DŽjʙEo]7:k)v Nb9S%fsB2-X|؍#SHAZ^5=j6r۵w[ѣmmUv"xc  V4olRFg:^!9oIRT,C<,}ȭmk{&, B;ϛ0hC PV4Lo}Pԏ4!:> -(*W62 @a8}1lg8 pS~@Tv)W>%enA{2< c{e{2`ɀ$ۓ)`0`0`0`Z+ hFA_ ڧŠ_ :Š4tPAsezj: p)]E[19 稆C8ǰx2ZO[qBي:ON2 C,p;zYd C?Yt!< s\ CTcXEx\dyFiZkOa|VDB?+FV>\E2也7 P"GdġS#vda fߓ,L19b, eap" I| 9 ANd#Lj&ځiI G0'5r>!Gѯ4G3GI4`9ANjxDc4$Иƚi' i;9UH\ܧQN6bG\IbbڳiW>>Ŀ _% I|W*d zI68 e ΋ʜ%*T{ɝ7͗~y]< /pa+}}Kry5&rtٺl=2唓!_#KA^tap\V'xuu'6UO Ml6⩧Xo6+g$+)H7 C:3qja1[ՠ@˷YN$]~GVYnm,eOOMf8;qC{J2V,1`wr{d$ (P),ve̕Q ԟGSg^ vvn-RA(g Uz}e?ua*S+a* ^(qJa2&frXW{4l0}݃46yLB.OЮZf"Es/Yo/,ztᵜg5}FjXSL3 b5Wd+ȼ@#ԗ1f~B=ׇuʓKSHeiF'4.0_E'HZw&5nBBꝳ ޡU2݃.aTE*tw<qHD]2% __7`m!ӓE_5D=RDBR;3%^ֹC CW%9C34M&U0XFY5rmi s90,CP4er̫C8HTh#}<<<DcZ"(7<j6!*[e VqJ4p?>.IOH҉шv8GM{B{g>SdsŨfejOla-pXK>eCg9_8e1퉪;XH4d{tsm8@g'Rd?F3'9^MfM^|.Xrչjk|O)׮ ZM cmm$+EN3o-je.prgnqs$ޢyKI[$lDF*e0-scکcȞ:me &Y\+s[w~8ٱkM}ߵ [:*2ũ=朙`R[Hkdq="!5DZSVbH6b #el_mmJJPuuaT;Vy`ֆEfsn[8g1{6alұsWŔ4MfG Wm7)~$D*!7v|T?Yrzsmܿˠ9 NFA`f|>w:$ΚFڋ 1ey(9ta뾡.67@ev?NXo,g,# 5d,NQ )EX,_ZbIxҕ#'/\KSN*Z&h$rD2A['BF^y(jX';'eL/2oos*~0WQ1H>2fWo߹l5c,ȫrC6s\Mn ҋ[}PՐ(\HuN Ō.bޠ.fL0YZ[Z0W$/cJks5|wz9xuh|] bggwݿU#x&CҔ_6s Pt_"aCP!}h92pYCYVFjF-8.)8rƵ8^vRxkXseU^a@`tf'F"pٳmU=ꏯ8ڴaCwrSioܠԵ-&q{#=?l碸2 "'jIV%"|9?T]MHꦆ:,5~vHZ@h_p^P`+;P{"|d O4=7(1{kjO"MUJ4W6lBAl)$ hTØTl½[;%e.ӟe~mMv~f[x#fr9g#r1LS$"5J:HTnGg%/N*@w{ٙM]JB9y;.)uWGz"e^S`[O[c m,̞h[϶}__XXo/̼YPhTV1P_wB[ٸk7vEqa[F.U-Su6%0ؕb12i+0#l8zbe+=0ݝ:S@O~HWl9++ʯo.#MUo_BMe/w6͏[|,BmȊ*/(?WO&iwFg ?7Z+#ߊbv$N'YÙ<ΌF>WYm&w&Kф= S #'ɗS2y|+^bi o%'xW<{Kqߨ89 MkmUpmG*S'̯ײlwv}/e;j)m}*Ry#{^WYGϑ*nD1=FCd w?#;?7JxaFka_׮?9#+$?*~op, endstream endobj 90 0 obj <> endobj 206 0 obj <>stream xZ]l>3;;B:nLILxmbvdH:ıqmS(mت'ڇR%x*R8ErQ"J<yC*TR wfvAPhs{w=;wvV!Z(sȮ~?r4`/y/IXQ_~?sK'&z^x+pp92t]hͼvho3CPjI6~93 oO-m*Q ЯZ6 z^(>Þ_!Ϭ1_erq7WK>džLӴ|tȿ, o/k8*B;WSK?ڥdwJFm UA3ٔ5jLboD[94r(f#Ѩ KDfΈ Ijn..d]-JF(}KMf*T6*|C٨dJZU 3,vACUb̼ɪd1A\$gYVъF3/h*+(dHZ`#mSk~m )fYLhYqU̬of¯"\\(LԂ5Tp/f`.(ZRK,\&bOYY͊ZHbT .fldt5Ci-9!EPW9fЬD2g1%7WdhmͫybD95Ul^IUjAVzj7EUǩmԬ 65="ZErJѸhATUbaVM׊^\M[c^fRNm(Z\鬣FREXOOfG5rId|pTkm w4g\A>.,v8' 650?B; m1杤 ,)YANЏFW R me_3kyR^őZ 9҂ unT9R@/zm-VZo7DbV+%>ruHpDf"x#VIHp޳:l&{u1ք.1$uU*4!a)*oFR I`3::絋9'>F4UKԪę<Ve bQRJ6]m ;b/{mCMB1Q+;@JJz<,AA*W V*8c3e{%>Q,a_4mk{-km{/94 Ctk~ eBހC2{A[ 7'oHnK'[Ml`&q >uzx\:¨ǽC ^-o600CJBV5P/?`oɗQ ݖc"$7''t&E *pe{3~~ȗ??z︓pR&ڕY"2{t@8f2q'XMaovq'rq]O/Uo{$!LtHɲlqW6PC#oHa ShߗgRԥ.uK]Rԥ._B_ywH7%q`? u/>P.+ mȭ9Hz,w&EN9M?kt:Yԥ.uK]Rԥ.u q^m= oZsIkL(0dﷀ endstream endobj 73 0 obj <> endobj 207 0 obj <>stream xYKTQ=׌cS&)+ F҄t8x͌3xQh2E iS-ZmK`3ywp{82x1! +hJ _6pZQĐVڢUG<35=?;58uL~x>(a\L! Hb $CE'uNf9 /{܋ʊk(rM+Q"Z/aUiH]1֮k|fxic<l7F<\^b 'I; hi=xhA5(6[{kc5v*׍GZj|P5"ީm; r9_6"]8"549 n]7!.A6뵼;53f zO1,Tk0T.4555555555/gqf g\}n<}_:+w~k_ Ԕ endstream endobj 122 0 obj <> endobj 208 0 obj <>stream xZ]l>3;;B:nLILxmbvdH:ıqmS(mت'ڇR%x*R8ErQ"J<yC*TR wfvAPhs{w=;wvV!Z(sȮ~?r4`/y/IXQ_~?sK'&z^x+pp92t]hͼvho3CPjI6~93 oO-m*Q ЯZ6 z^(>Þ_!Ϭ1_erq7WK>džLӴ|tȿ, o/k8*B;WSK?ڥdwJFm UA3ٔ5jLboD[94r(f#Ѩ KDfΈ Ijn..d]-JF(}KMf*T6*|C٨dJZU 3,vACUb̼ɪd1A\$gYVъF3/h*+(dHZ`#mSk~m )fYLhYqU̬of¯"\\(LԂ5Tp/f`.(ZRK,\&bOYY͊ZHbT .fldt5Ci-9!EPW9fЬD2g1%7WdhmͫybD95Ul^IUjAVzj7EUǩmԬ 65="ZErJѸhATUbaVM׊^\M[c^fRNm(Z\鬣FREXOOfG5rId|pTkm w4g\A>.,v8' 650?B; m1杤 ,)YANЏFW R me_3kyR^őZ 9҂ unT9R@/zm-VZo7DbV+%>ruHpDf"x#VIHp޳:l&{u1ք.1$uU*4!a)*oFR I`3::絋9'>F4UKԪę<Ve bQRJ6]m ;b/{mCMB1Q+;@JJz<,AA*W V*8c3e{%>Q,a_4mk{-km{/94 Ctk~ eBހC2{A[ 7'oHnK'[Ml`&q >uzx\:¨ǽC ^-o600CJBV5P/?`oɗQ ݖc"$7''t&E *pe{3~~ȗ??z︓pR&ڕY"2{t@8f2q'XMaovq'rq]O/Uo{$!LtHɲlqW6PC#oHa ShߗgRԥ.uK]Rԥ._B_ywH7%q`? u/>P.+ mȭ9Hz,w&EN9M?kt:Yԥ.uK]Rԥ.u q^m= oZsIkL(0dﷀ endstream endobj 103 0 obj <> endobj 209 0 obj <>stream xW \GanlFYŘ`QDPFPB$&a]aq7MDL/TȀ$&#lT4k[=`}dd2łܜ\u3R'ȳIܳ&]c X\ظ/10VfidD5Wں:F[uv)Irӳsl&gSuIY׭Z*闥K_}"B|3e X;7;0'(w޺y o$OZPN[rQdFԪ1o>mt[3'}+ljl9| (yIHB&2ZH Bhdbb䇖 bQrDqh.rB!4 #w4 h@ah" @ˑ%G چ֣5R"B+F1yh,ڄ2lģ,d sIKG\^Ym@UFlrqqq ø2yL;g] pWGRGEF>?f7ƴĚ3oSIEmccsn<1 565cnkap\mS,",,x(TZrΖ+-k,{-!I7tʾ˿tj67}PMش$bnW7*KujƏCjp[y  XS5̷A ^,ԐQQ3/B9Sm'0 r9/9C,U<:_7\"{'1^,I%Ig%= H3Äf 7[ۃzR{OYXYuuk`]ڈReViQ.{DbӉ`OEKJRKIN󒈙(ne]X%K8 ΄9qKę8KQl1&7 o҉}Č_hEťu:bn" 0Y;6uuL#$f*I S!U䛈o,"4+R̍7 %k :ѕ֯{wQ1n4XDUUH|] B97ݓEpwhd5X+;Ml^7XxH&n9Vp\ v4V;+clϐ; 00%c71Rp[k7v\&L~ܕĞD܈;*OV)n9>}$)li6* bARgJ4Z#N[F;ՔTE堁ץa~T(;8ys_]t;Gt=p~7/>G']yf?9z^F eM\6a9g OWU?wZ )o4$=Z<ĕS|>;XĤVgRDIV^"c`)ɜ~yQ~ΜbmswJkm" q7c&i3#EM$\-g~*b((WZ5@nթҪV_ +8\5)y,nk5x3>)i`1Up&|ɑJMJv\F#VCYYڗ}JFW* @E%G%8e-}۸*_KT +*uu7^ĄdIK]b)fb@ zhq굊SPAYkGc՚}u3$&hs0uCbg;\ U>G=|DfSGI?"PWH\!*)Ē$1Y5E0|SqKjN/A0<ӂV)6 ?!Tw  W1p 7,%U=B?Ag.U5^˗e$KUxG7w13uƺʔ~V|=8T) O/X·+ګ &:mQNp7( b}ӑ0稫P6.T"pYV+8yN4LؐZϱp@` Z%xj ϑKL=r_,sxXN? EܨV1m5-v]ŝ|<47 a@XR#Dy?[9} nB,ַ2:5˿U!,r|v j 2 d#,}B*Y" Fq(}X( ^!G;.w=bMAj7HTpŸ 7FI< ]l3m΂E/iE9E/iyrHM0mDq> l,#gM0YihLGũMȵ:ҬPY Ɏe*w$7kAB *tһ $ !۞Z#b+$[0Ғ~']/ӖԱ§M*%kG5s*G/q*,4gUZs^J&̧='rd>\ f\VjUlUgVjU7Es-o4nߛB|j|^rIE}LR=$iOlomd/,V@/W;|De} $\+N-QeF0QgS(tޣm4Yo ,_0dgu%ACkƒK (,9~Em:KQc[)J@a''rd,bC&̺BK''sd&Aʀ;:XXQӆWBJ'PWO^ <ߺ?b6'>(EtWS߿07^^"C{'~MK{V>+S,~V(E$D dIPh8r(!߃=*'EzgqSx r БLhɡn0]Rc!xjH8 #a_|3<̻Ʊd؛?΍6sNi%kE&ںDά`k[>|ڰdf܆޳Dr6ɔ:ns1BUBopUge.I dWޡBUFᆼ%915cu'TmՑ&*jMP4jl'P3xƑhG&4V!Qd#u&@C%69׈ve8KSD ,<Ë) `vA(r| J94XY 5zF3* \.$&.^?O]*^}$ַPN܏Ouseݵ~&11'jP1;lo[V ~cQ3\hC.t10SϞܘS*ٿBaD+ڇD 0,oQ,# Pqe:3 1!CZE!itzƩ/ju6ʉxS'dGBY9$aAwڦL=XctV{ziPe"_^ 2YS0z O}yvYffNUrEXm{i?7[ݱ=A@ׁ/d8wmE84'{7lbi5o՚8VSoQuVFTdR*m pB|ee5>(rUhdq6UB5&}EUZUw`}&=` lxT]Js}W}*%!r>m6OZȁ#`ı D>w޴^]slOdҶ'0 :qB9lL&AǴQvW߼gǝrXCvl!(8\UKL' &Uv_V]0 < BsЅ׉Q@I`:q"6t<e9r(Drt&Bn5e2ɉqsɸ`\f2,+" OX3= |F6YP ݒr\*p8L^fHT\K*1A{ }Lj7Jĕl|QQ9*JPV,#4^hX~2\<.bщeMQ5Ǿ8pU)YI"VZJd:W7:M"zn/Q)nϼI(zBtQMI].&`Y ~Wg|Fǝ4LOJIw=)B+ }`{'޻sGe;o,ɒ7vr1&nTc.15'* endstream endobj 37 0 obj << /Title(MI_AZ6_WSCLAPI_EN) /Dest [36 0 R /FitH 599.0] /Count -10 /Parent 5 0 R /Prev 6 0 R /First 39 0 R /Last 39 0 R >> endobj 224 0 obj <>stream 2022-03-02T19:09:10+01:00 2022-03-02T19:09:10+01:00 Adobe InDesign 17.1 (Windows) endstream endobj 2 0 obj <>endobj xref 0 225 0000000000 65535 f 0000033208 00000 n 0000293393 00000 n 0000033003 00000 n 0000028537 00000 n 0000033152 00000 n 0000171588 00000 n 0000231506 00000 n 0000000085 00000 n 0000002180 00000 n 0000033290 00000 n 0000171476 00000 n 0000033341 00000 n 0000033412 00000 n 0000033454 00000 n 0000166772 00000 n 0000165806 00000 n 0000033520 00000 n 0000236798 00000 n 0000270707 00000 n 0000171361 00000 n 0000033562 00000 n 0000233411 00000 n 0000244945 00000 n 0000234775 00000 n 0000257138 00000 n 0000157682 00000 n 0000121317 00000 n 0000061020 00000 n 0000048881 00000 n 0000033901 00000 n 0000033629 00000 n 0000033684 00000 n 0000033738 00000 n 0000033803 00000 n 0000171307 00000 n 0000028891 00000 n 0000291991 00000 n 0000029280 00000 n 0000173342 00000 n 0000171688 00000 n 0000029728 00000 n 0000171880 00000 n 0000172254 00000 n 0000030105 00000 n 0000172065 00000 n 0000031664 00000 n 0000172629 00000 n 0000032033 00000 n 0000172445 00000 n 0000032664 00000 n 0000173164 00000 n 0000172815 00000 n 0000172987 00000 n 0000002200 00000 n 0000003195 00000 n 0000176211 00000 n 0000176042 00000 n 0000173612 00000 n 0000237860 00000 n 0000273563 00000 n 0000173699 00000 n 0000234118 00000 n 0000254795 00000 n 0000175939 00000 n 0000173744 00000 n 0000235767 00000 n 0000263741 00000 n 0000175830 00000 n 0000173797 00000 n 0000237152 00000 n 0000272382 00000 n 0000238843 00000 n 0000282372 00000 n 0000173868 00000 n 0000173912 00000 n 0000232991 00000 n 0000231575 00000 n 0000249331 00000 n 0000173985 00000 n 0000174140 00000 n 0000174295 00000 n 0000174450 00000 n 0000174605 00000 n 0000174760 00000 n 0000174915 00000 n 0000175070 00000 n 0000175225 00000 n 0000175380 00000 n 0000238431 00000 n 0000279289 00000 n 0000175535 00000 n 0000175613 00000 n 0000175667 00000 n 0000175732 00000 n 0000003215 00000 n 0000005435 00000 n 0000176359 00000 n 0000232258 00000 n 0000240922 00000 n 0000234979 00000 n 0000260677 00000 n 0000240169 00000 n 0000286397 00000 n 0000176410 00000 n 0000176463 00000 n 0000199746 00000 n 0000199385 00000 n 0000194575 00000 n 0000176693 00000 n 0000179594 00000 n 0000176745 00000 n 0000177019 00000 n 0000177292 00000 n 0000177557 00000 n 0000177810 00000 n 0000178063 00000 n 0000178332 00000 n 0000178599 00000 n 0000178857 00000 n 0000179103 00000 n 0000239239 00000 n 0000283313 00000 n 0000179349 00000 n 0000179406 00000 n 0000179509 00000 n 0000200942 00000 n 0000005456 00000 n 0000007262 00000 n 0000218142 00000 n 0000201618 00000 n 0000201036 00000 n 0000201241 00000 n 0000201447 00000 n 0000201480 00000 n 0000201570 00000 n 0000219878 00000 n 0000007284 00000 n 0000010805 00000 n 0000219946 00000 n 0000236093 00000 n 0000268494 00000 n 0000219991 00000 n 0000220035 00000 n 0000220114 00000 n 0000030420 00000 n 0000010827 00000 n 0000013123 00000 n 0000220208 00000 n 0000220241 00000 n 0000220320 00000 n 0000030736 00000 n 0000013145 00000 n 0000015581 00000 n 0000220390 00000 n 0000220423 00000 n 0000220502 00000 n 0000031052 00000 n 0000015603 00000 n 0000018706 00000 n 0000220574 00000 n 0000220642 00000 n 0000031348 00000 n 0000018728 00000 n 0000020757 00000 n 0000220699 00000 n 0000220732 00000 n 0000220811 00000 n 0000020779 00000 n 0000022001 00000 n 0000229460 00000 n 0000221271 00000 n 0000220894 00000 n 0000221100 00000 n 0000221133 00000 n 0000221223 00000 n 0000230420 00000 n 0000022023 00000 n 0000024716 00000 n 0000230488 00000 n 0000230532 00000 n 0000230611 00000 n 0000032348 00000 n 0000024738 00000 n 0000026690 00000 n 0000230694 00000 n 0000230727 00000 n 0000230806 00000 n 0000026712 00000 n 0000028515 00000 n 0000230887 00000 n 0000231101 00000 n 0000231315 00000 n 0000231359 00000 n 0000231425 00000 n 0000241140 00000 n 0000245154 00000 n 0000249548 00000 n 0000255008 00000 n 0000257346 00000 n 0000261027 00000 n 0000263949 00000 n 0000268789 00000 n 0000270915 00000 n 0000272634 00000 n 0000273770 00000 n 0000279496 00000 n 0000282575 00000 n 0000283521 00000 n 0000287002 00000 n 0000231978 00000 n 0000232558 00000 n 0000233130 00000 n 0000233745 00000 n 0000234507 00000 n 0000235404 00000 n 0000236491 00000 n 0000237438 00000 n 0000238193 00000 n 0000238613 00000 n 0000239003 00000 n 0000239423 00000 n 0000240829 00000 n 0000249207 00000 n 0000292130 00000 n trailer << /Size 225 /Root 1 0 R /Info 2 0 R /ID [] >> startxref 293569 %%EOF aioairzone-1.0.0/docs/api-demo.json000066400000000000000000000042161477442374000172050ustar00rootroot00000000000000{ "data": [ { "systemID": 1, "zoneID": 1, "name": "zona01", "on": 1, "coolsetpoint": 25, "coolmaxtemp": 30, "coolmintemp": 18, "heatsetpoint": 22, "heatmaxtemp": 30, "heatmintemp": 15, "maxTemp": 30, "minTemp": 15, "setpoint": 26, "roomTemp": 20, "modes": [ 1, 2, 3, 4 ], "mode": 3, "speeds": 5, "speed": 2, "coldStages": 3, "coldStage": 2, "heatStages": 3, "heatStage": 1, "humidity": 43, "units": 0, "errors": [ { "Zone": "Error 3" }, { "Zone": "Error 4" }, { "Zone": "Error 5" }, { "Zone": "Error 6" }, { "Zone": "Error 7" }, { "Zone": "Error 8" }, { "Zone": "Presence alarm" }, { "Zone": "Window alarm" }, { "Zone": "Anti-freezing alarm" }, { "Zone": "Zone without thermostat" }, { "Zone": "Low battery" }, { "Zone": "Active dew" }, { "Zone": "Active dew protection" }, { "system": "Error 9" }, { "system": "Error 11" }, { "system": "Error 13" }, { "system": "Error 15" }, { "system": "Error 16" }, { "system": "C09" }, { "system": "C11" }, { "system": "IU error IU4" }, { "system": "Error IAQ1" }, { "system": "Error IAQ4" }, { "zone": "Error IAQ2" }, { "Zone": "Error IAQ3" } ], "air_demand": 1, "floor_demand": 1, "aq_mode": 2, "aq_quality": 2, "aq_thrlow": 10, "aq_thrhigh": 40, "slats_vertical": 2, "slats_horizontal": 3, "slats_vswing": 1, "slats_hswing": 0 } ] } aioairzone-1.0.0/docs/api-hvac-s0-altherma-error.json000066400000000000000000000001121477442374000224330ustar00rootroot00000000000000{ "errors": [ { "error": "altherma not connected" } ] } aioairzone-1.0.0/docs/api-hvac-s0-z0-multiple-systems.json000066400000000000000000000117411477442374000234100ustar00rootroot00000000000000{ "systems": [ { "data": [ { "systemID": 2, "zoneID": 1, "name": "Utility", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 20, "roomTemp": 20.5, "modes": [ 1, 4, 3 ], "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 47, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 2, "zoneID": 2, "name": "Kitchen", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 20, "roomTemp": 20.700001, "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 45, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 2, "zoneID": 3, "name": "Den", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 20, "roomTemp": 20.9, "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 46, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 2, "zoneID": 4, "name": "Down hall", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 20, "roomTemp": 20.5, "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 61, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 2, "zoneID": 5, "name": "Sitting rm", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 17, "roomTemp": 18.200001, "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 56, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 2, "zoneID": 6, "name": "Down bed", "on": 1, "maxTemp": 24, "minTemp": 15, "setpoint": 17, "roomTemp": 19.799999, "mode": 3, "coldStages": 0, "coldStage": 0, "heatStages": 2, "heatStage": 2, "humidity": 51, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 } ] }, { "data": [ { "systemID": 3, "zoneID": 1, "name": "Master bed", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19, "roomTemp": 19.200001, "modes": [ 1, 4, 2, 3 ], "mode": 3, "speeds": 3, "speed": 2, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 44, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 3, "zoneID": 2, "name": "Back bed", "on": 1, "maxTemp": 30, "minTemp": 15, "setpoint": 19, "roomTemp": 20, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 39, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 3, "zoneID": 3, "name": "Front bed", "on": 1, "maxTemp": 30, "minTemp": 15, "setpoint": 17, "roomTemp": 19.9, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 47, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 3, "zoneID": 4, "name": "Middle bed", "on": 1, "maxTemp": 30, "minTemp": 15, "setpoint": 17, "roomTemp": 20.5, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 45, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 } ] } ] } aioairzone-1.0.0/docs/api-hvac-s0-z0-multiple-zones.json000066400000000000000000000050021477442374000230300ustar00rootroot00000000000000{ "systems": [ { "data": [ { "systemID": 1, "zoneID": 1, "name": "Salon", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19.5, "roomTemp": 19.6, "modes": [ 1, 4, 2, 3, 5 ], "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 35, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 2, "name": "Dorm Ppal", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19.5, "roomTemp": 21.1, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 39, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 3, "name": "Dorm #1", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19.5, "roomTemp": 20.799999, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 36, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 4, "name": "Despacho", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19.5, "roomTemp": 21.200001, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 36, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 5, "name": "Dorm #2", "on": 0, "maxTemp": 30, "minTemp": 15, "setpoint": 19.5, "roomTemp": 20.5, "mode": 3, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 40, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 } ] } ] } aioairzone-1.0.0/docs/api-hvac-s0-z0-single-system-zone-1.json000066400000000000000000000014571477442374000237650ustar00rootroot00000000000000{ "systems": [ { "data": [ { "systemID": 1, "zoneID": 1, "name": "DKN Plus", "on": 1, "coolsetpoint": 81, "coolmaxtemp": 90, "coolmintemp": 63, "heatsetpoint": 79, "heatmaxtemp": 88, "heatmintemp": 60, "maxTemp": 88, "minTemp": 60, "setpoint": 79, "roomTemp": 75, "modes": [ 4, 3 ], "mode": 3, "speeds": 2, "speed": 2, "coldStages": 0, "coldStage": 0, "heatStages": 0, "heatStage": 0, "humidity": 0, "units": 1, "errors": [], "air_demand": 1, "floor_demand": 0 } ] } ] } aioairzone-1.0.0/docs/api-hvac-s0-z0-single-system-zone-2.json000066400000000000000000000010701477442374000237550ustar00rootroot00000000000000{ "systems": [ { "data": [ { "systemID": 1, "zoneID": 1, "name": "", "on": 0, "maxTemp": 88, "minTemp": 63, "setpoint": 74, "roomTemp": 71, "mode": 3, "speeds": 3, "speed": 3, "coldStages": 0, "coldStage": 0, "heatStages": 0, "heatStage": 0, "humidity": 0, "units": 1, "errors": [], "air_demand": 0, "floor_demand": 0 } ] } ] } aioairzone-1.0.0/docs/api-hvac-s0-z0-single-system-zone-3.json000066400000000000000000000010701477442374000237560ustar00rootroot00000000000000{ "systems": [ { "data": [ { "systemID": 1, "zoneID": 1, "name": "", "on": 0, "maxTemp": 88, "minTemp": 63, "setpoint": 68, "roomTemp": 71, "mode": 3, "speeds": 3, "speed": 1, "coldStages": 0, "coldStage": 0, "heatStages": 0, "heatStage": 0, "humidity": 0, "units": 1, "errors": [], "air_demand": 0, "floor_demand": 0 } ] } ] } aioairzone-1.0.0/docs/api-hvac-s1-z0-multiple-zones.json000066400000000000000000000032151477442374000230350ustar00rootroot00000000000000{ "data": [ { "systemID": 1, "zoneID": 1, "name": "Sala", "on": 0, "maxTemp": 30, "minTemp": 18, "setpoint": 24, "roomTemp": 21.1, "modes": [ 1, 4, 2, 3, 5 ], "mode": 2, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 60, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 2, "name": "Cucina", "on": 0, "maxTemp": 30, "minTemp": 18, "setpoint": 24, "roomTemp": 22.299999, "mode": 2, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 55, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 3, "name": "Ospiti", "on": 0, "maxTemp": 30, "minTemp": 18, "setpoint": 24, "roomTemp": 21.6, "mode": 2, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 56, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 }, { "systemID": 1, "zoneID": 4, "name": "Letto", "on": 0, "maxTemp": 30, "minTemp": 18, "setpoint": 24, "roomTemp": 21.5, "mode": 2, "coldStages": 1, "coldStage": 1, "heatStages": 1, "heatStage": 1, "humidity": 52, "units": 0, "errors": [], "air_demand": 0, "floor_demand": 0 } ] } aioairzone-1.0.0/docs/api-hvac-s127-single-system.json000066400000000000000000000002021477442374000224640ustar00rootroot00000000000000{ "systems": [ { "systemID": 1, "power": 0, "system_firmware": "3.31", "system_type": 1 } ] } aioairzone-1.0.0/docs/api-integration.json000066400000000000000000000000231477442374000205740ustar00rootroot00000000000000{ "driver": "" } aioairzone-1.0.0/docs/api-version.json000066400000000000000000000000311477442374000177350ustar00rootroot00000000000000{ "version": "1.621" } aioairzone-1.0.0/docs/api-webserver.json000066400000000000000000000002471477442374000202650ustar00rootroot00000000000000{ "mac": "11:22:33:44:55:66", "wifi_channel": 6, "wifi_quality": 60, "wifi_rssi": -46, "interface": "wifi", "ws_firmware": "3.408", "ws_type": "ws_az" } aioairzone-1.0.0/examples/000077500000000000000000000000001477442374000155025ustar00rootroot00000000000000aioairzone-1.0.0/examples/_config.py000066400000000000000000000003771477442374000174670ustar00rootroot00000000000000"""Airzone Local API Config.""" from aioairzone.const import DEFAULT_PORT, DEFAULT_SYSTEM_ID from aioairzone.localapi import ConnectionOptions AIRZONE_OPTIONS = ConnectionOptions( "192.168.1.25", DEFAULT_PORT, DEFAULT_SYSTEM_ID, True, ) aioairzone-1.0.0/examples/basic.py000066400000000000000000000023231477442374000171350ustar00rootroot00000000000000"""Airzone basic example.""" import asyncio import json import timeit import _config import aiohttp from aiohttp.client_exceptions import ClientConnectorError from aioairzone.exceptions import InvalidHost from aioairzone.localapi import AirzoneLocalApi async def main() -> None: """Airzone basic example.""" async with aiohttp.ClientSession() as aiohttp_session: airzone = AirzoneLocalApi(aiohttp_session, _config.AIRZONE_OPTIONS) try: validate_start = timeit.default_timer() airzone_mac = await airzone.validate() validate_end = timeit.default_timer() if airzone_mac is not None: print(f"Airzone WebServer: {airzone_mac}") print(f"Validate time: {validate_end - validate_start}") print("***") update_start = timeit.default_timer() await airzone.update() update_end = timeit.default_timer() print(json.dumps(airzone.data(), indent=4, sort_keys=True)) print(f"Update time: {update_end - update_start}") except (ClientConnectorError, InvalidHost) as err: print(f"Invalid host: {err}") if __name__ == "__main__": asyncio.run(main()) aioairzone-1.0.0/examples/loop.py000066400000000000000000000027421477442374000170320ustar00rootroot00000000000000"""Airzone loop example.""" import asyncio import timeit import _config import aiohttp from aiohttp.client_exceptions import ClientConnectorError from aioairzone.exceptions import InvalidHost from aioairzone.localapi import AirzoneLocalApi async def main() -> None: """Airzone loop example.""" async with aiohttp.ClientSession() as aiohttp_session: airzone = AirzoneLocalApi(aiohttp_session, _config.AIRZONE_OPTIONS) try: validate_start = timeit.default_timer() airzone_mac = await airzone.validate() validate_end = timeit.default_timer() if airzone_mac is not None: print(f"Airzone WebServer: {airzone_mac}") print(f"Validate time: {validate_end - validate_start}") print("***") while True: update_start = timeit.default_timer() tasks = [] tasks += [airzone.get_webserver()] tasks += [airzone.get_hvac_systems()] tasks += [airzone.get_hvac()] for task in tasks: task_data = await task if task_data is None: print(f"{task}: empty response") update_end = timeit.default_timer() print(f"Update time: {update_end - update_start}") except (ClientConnectorError, InvalidHost) as err: print(f"Invalid host: {err}") if __name__ == "__main__": asyncio.run(main()) aioairzone-1.0.0/examples/modify.py000066400000000000000000000041651477442374000173510ustar00rootroot00000000000000"""Airzone modify parameters example.""" import asyncio import json import time import timeit import _config import aiohttp from aiohttp.client_exceptions import ClientConnectorError from aioairzone.const import API_MODE, API_SYSTEM_ID, API_ZONE_ID from aioairzone.exceptions import InvalidHost from aioairzone.localapi import AirzoneLocalApi async def main() -> None: """Airzone modify parameters example.""" async with aiohttp.ClientSession() as aiohttp_session: airzone = AirzoneLocalApi(aiohttp_session, _config.AIRZONE_OPTIONS) try: validate_start = timeit.default_timer() airzone_mac = await airzone.validate() validate_end = timeit.default_timer() if airzone_mac is not None: print(f"Airzone WebServer: {airzone_mac}") print(f"Validate time: {validate_end - validate_start}") print("***") update_start = timeit.default_timer() await airzone.update() update_end = timeit.default_timer() print(json.dumps(airzone.data(), indent=4, sort_keys=True)) print(f"Update time: {update_end - update_start}") print("***") modify_start = timeit.default_timer() await airzone.set_hvac_parameters( { API_SYSTEM_ID: 1, API_ZONE_ID: 3, API_MODE: 1, } ) modify_end = timeit.default_timer() print(json.dumps(airzone.data(), indent=4, sort_keys=True)) print(f"Modify time: {modify_end - modify_start}") print("***") print("Sleeping...") time.sleep(3) print("***") update_start = timeit.default_timer() await airzone.update() update_end = timeit.default_timer() print(json.dumps(airzone.data(), indent=4, sort_keys=True)) print(f"Update time: {update_end - update_start}") except (ClientConnectorError, InvalidHost): print("Invalid host.") if __name__ == "__main__": asyncio.run(main()) aioairzone-1.0.0/examples/raw.py000066400000000000000000000027121477442374000166470ustar00rootroot00000000000000"""Airzone raw API data example.""" import asyncio import json import timeit import _config import aiohttp from aiohttp.client_exceptions import ClientConnectorError from aioairzone.exceptions import InvalidHost from aioairzone.localapi import AirzoneLocalApi async def main() -> None: """Airzone raw API data example.""" async with aiohttp.ClientSession() as aiohttp_session: airzone = AirzoneLocalApi(aiohttp_session, _config.AIRZONE_OPTIONS) try: validate_start = timeit.default_timer() airzone_mac = await airzone.validate() validate_end = timeit.default_timer() if airzone_mac is not None: print(f"Airzone WebServer: {airzone_mac}") print(f"Validate time: {validate_end - validate_start}") print("***") update_start = timeit.default_timer() await airzone.get_demo() await airzone.get_integration() await airzone.get_version() await airzone.update() update_end = timeit.default_timer() print(json.dumps(airzone.data(), indent=4, sort_keys=True)) print("***") print(json.dumps(airzone.raw_data(), indent=4, sort_keys=True)) print("***") print(f"Update time: {update_end - update_start}") except (ClientConnectorError, InvalidHost): print("Invalid host.") if __name__ == "__main__": asyncio.run(main()) aioairzone-1.0.0/pyproject.toml000066400000000000000000000043031477442374000166000ustar00rootroot00000000000000[project] name = "aioairzone" version = "1.0.0" description = "Library to control Airzone devices" readme = "README.md" requires-python = ">=3.12" license = "Apache-2.0" keywords = ["airzone", "hvac", "home"] authors = [ {name = "Álvaro Fernández Rojas", email = "noltari@gmail.com" } ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Home Automation", ] dependencies = [ "aiohttp" ] [project.urls] "Homepage" = "https://github.com/Noltari/aioairzone" "Bug Tracker" = "https://github.com/Noltari/aioairzone/issues" [tool.mypy] python_version = "3.12" [tool.pylint.MAIN] py-version = "3.12" [tool.pylint.BASIC] class-const-naming-style = "any" [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: # duplicate-code - unavoidable # invalid-name - not using snake case naming style # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* disable = [ "duplicate-code", "invalid-name", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-locals", "too-many-positional-arguments", "too-many-public-methods", "too-many-statements", ] [tool.pylint.REPORTS] score = false [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.ruff.lint] select = [ "D", # docstrings "E", # pycodestyle "G", # flake8-logging-format "I", # isort "W", # pycodestyle ] ignore = [ "D202", # No blank lines allowed aftee function docstring "D203", # 1 blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D401", # First line should be in imperative mood ] [tool.ruff.lint.isort] force-sort-within-sections = true combine-as-imports = true split-on-trailing-comma = false [tool.setuptools] packages = ["aioairzone"] platforms = ["any"] zip-safe = false include-package-data = true [build-system] requires = ["setuptools>=43.0.0", "wheel"] build-backend = "setuptools.build_meta" aioairzone-1.0.0/requirements.txt000066400000000000000000000001271477442374000171500ustar00rootroot00000000000000aiohttp>=3.9.0b0;python_version>='3.12' aiohttp<=3.8.5;python_version<'3.12' packaging aioairzone-1.0.0/requirements_dev.txt000066400000000000000000000000631477442374000200050ustar00rootroot00000000000000-r requirements.txt -r requirements_lint.txt -e . aioairzone-1.0.0/requirements_lint.txt000066400000000000000000000000331477442374000201720ustar00rootroot00000000000000black mypy pylint ruff aioairzone-1.0.0/sim/000077500000000000000000000000001477442374000144545ustar00rootroot00000000000000aioairzone-1.0.0/sim/airzone.py000066400000000000000000000060621477442374000165010ustar00rootroot00000000000000"""Airzone Local API simulation.""" from aiohttp import web from aiohttp.web_request import Request from aiohttp.web_response import Response from demo import AirzoneDemo from helpers import api_json_error from hvac import AirzoneACSStatus, AirzoneHVAC from integration import AirzoneIntegration from version import AirzoneVersion from webserver import AirzoneWebServer from aioairzone.common import TemperatureUnit from aioairzone.const import API_ERROR_METHOD_NOT_SUPPORTED, API_V1, DEFAULT_PORT # Airzone device simulation class Airzone: """Airzone Local API.""" def __init__(self) -> None: """Local API Version init.""" self.demo: AirzoneDemo = AirzoneDemo() self.hvac: AirzoneHVAC = AirzoneHVAC() self.integration: AirzoneIntegration = AirzoneIntegration("driver") self.version: AirzoneVersion = AirzoneVersion("1.64") self.webserver: AirzoneWebServer = AirzoneWebServer("11:22:33:44:55:66") airzone = Airzone() airzone.hvac.add_zone("Salón", 1, 1, TemperatureUnit.CELSIUS) airzone.hvac.acs.set_status(AirzoneACSStatus.ENABLED) # Airzone Local API simulation routes = web.RouteTableDef() @routes.post(f"/{API_V1}/demo") async def demo_post_handler(request: Request) -> Response: # pylint: disable=unused-argument """POST /demo.""" return await airzone.demo.post() @routes.post(f"/{API_V1}/hvac") async def hvac_post_handler(request: Request) -> Response: """POST /hvac.""" return await airzone.hvac.post(request) @routes.put(f"/{API_V1}/hvac") async def hvac_put_handler(request: Request) -> Response: """PUT /hvac.""" return await airzone.hvac.put(request) @routes.post(f"/{API_V1}/integration") async def integration_post_handler(request: Request) -> Response: """POST /integration.""" return await airzone.integration.post(request) @routes.put(f"/{API_V1}/integration") async def integration_put_handler(request: Request) -> Response: """PUT /integration.""" return await airzone.integration.put(request) @routes.post(f"/{API_V1}/version") async def version_post_handler(request: Request) -> Response: # pylint: disable=unused-argument """POST /version.""" return await airzone.version.post() @routes.post(f"/{API_V1}/webserver") async def webserver_post_handler(request: Request) -> Response: # pylint: disable=unused-argument """POST /webserver.""" return await airzone.webserver.post() @routes.get("/{tail:.*}") async def root_get_handler(request: Request) -> Response: """GET root.""" # pylint: disable=unused-argument raise web.HTTPInternalServerError() @routes.post("/{tail:.*}") async def root_post_handler(request: Request) -> Response: # pylint: disable=unused-argument """POST root.""" return api_json_error(API_ERROR_METHOD_NOT_SUPPORTED) @routes.put("/{tail:.*}") async def root_put_handler(request: Request) -> Response: # pylint: disable=unused-argument """PUT root.""" return api_json_error(API_ERROR_METHOD_NOT_SUPPORTED) app = web.Application() app.add_routes(routes) web.run_app(app, port=DEFAULT_PORT) aioairzone-1.0.0/sim/demo.py000066400000000000000000000066301477442374000157570ustar00rootroot00000000000000"""Airzone Local API Demo.""" from typing import Any from aiohttp.web_response import Response from helpers import api_json_response from aioairzone.const import API_DATA class AirzoneDemo: """Airzone Local API Demo.""" def __init__(self) -> None: """Local API Demo init.""" def data(self) -> dict[str, Any]: """Return Local API Demo data.""" return { API_DATA: [ { "systemID": 1, "zoneID": 1, "name": "zona01", "on": 1, "coolsetpoint": 25, "coolmaxtemp": 30, "coolmintemp": 18, "heatsetpoint": 22, "heatmaxtemp": 30, "heatmintemp": 15, "maxTemp": 30, "minTemp": 15, "setpoint": 26, "roomTemp": 20, "modes": [1, 2, 3, 4], "mode": 3, "speeds": 5, "speed": 2, "speed_values": [0, 1, 2, 3, 4, 5], "speed_type": 0, "coldStages": 3, "coldStage": 2, "heatStages": 3, "heatStage": 1, "humidity": 43, "units": 0, "errors": [ {"Zone": "Error 3"}, {"Zone": "Error 4"}, {"Zone": "Error 5"}, {"Zone": "Error 6"}, {"Zone": "Error 7"}, {"Zone": "Error 8"}, {"Zone": "Presence alarm"}, {"Zone": "Window alarm"}, {"Zone": "Anti-freezing alarm"}, {"Zone": "Zone without thermostat"}, {"Zone": "Low battery"}, {"Zone": "Active dew"}, {"Zone": "Active dew protection"}, {"Zone": "F05-H"}, {"Zone": "F06-H"}, {"Zone": "F05-C"}, {"Zone": "F06-C"}, {"system": "Error 9"}, {"system": "Error 13"}, {"system": "Error 11"}, {"system": "Error 15"}, {"system": "Error 16"}, {"system": "Error C09"}, {"system": "Error C11"}, {"system": "IU error IU4"}, {"system": "Error IAQ1"}, {"system": "Error IAQ4"}, {"Zone": "Error IAQ2"}, {"Zone": "Error IAQ3"}, ], "air_demand": 1, "floor_demand": 1, "aq_mode": 2, "aq_quality": 2, "aq_thrlow": 10, "aq_thrhigh": 40, "slats_vertical": 2, "slats_horizontal": 3, "slats_vswing": 1, "slats_hswing": 0, "antifreeze": 1, "eco_adapt": "manual", } ] } async def post(self) -> Response: """POST Local API Demo.""" return api_json_response(self.data()) aioairzone-1.0.0/sim/helpers.py000066400000000000000000000032471477442374000164760ustar00rootroot00000000000000"""Airzone Local API Helpers.""" from collections.abc import Mapping import json from typing import Any from aiohttp import web from aiohttp.web_response import Response from aioairzone.const import API_ERROR, API_ERRORS def api_error_dict(error: str) -> dict[str, Any]: """Local API error dict.""" return { API_ERRORS: [ { API_ERROR: error, }, ], } def api_filter_dict(data: Any, keep: list[str]) -> Any: """Local API dict filter.""" if not isinstance(data, (Mapping, list)): return data if isinstance(data, list): return [api_filter_dict(val, keep) for val in data] filtered = {**data} keys = list(filtered) for key in keys: if key not in keep: filtered.pop(key) elif isinstance(filtered[key], Mapping): filtered[key] = api_filter_dict(filtered[key], keep) elif isinstance(filtered[key], list): filtered[key] = [api_filter_dict(item, keep) for item in filtered[key]] return filtered def api_json_dumps(obj: Any) -> Any: """Local API JSON dumps.""" return json.dumps(obj, indent=4, sort_keys=True) def api_json_error(error: str) -> Response: """Local API error.""" return api_json_response(api_error_dict(error)) def api_json_response(data: dict[str, Any]) -> Response: """Return Local API error.""" return web.json_response(data, dumps=api_json_dumps) def celsius_to_fahrenheit(celsius: float | int) -> float | int: """Convert Celsius to Fahrenheit.""" fahrenheit = (celsius * 9 / 5) + 32 if isinstance(celsius, float): return fahrenheit return int(fahrenheit) aioairzone-1.0.0/sim/hvac.py000066400000000000000000000401271477442374000157530ustar00rootroot00000000000000"""Airzone Local API HVAC.""" from enum import IntEnum import random from typing import Any, cast from aiohttp import web from aiohttp.web_response import Response from helpers import ( api_filter_dict, api_json_error, api_json_response, celsius_to_fahrenheit, ) from aioairzone.common import ( OperationMode, SystemType, TemperatureUnit, get_system_zone_id, ) from aioairzone.const import ( API_ACS_MAX_TEMP, API_ACS_MIN_TEMP, API_ACS_ON, API_ACS_POWER_MODE, API_ACS_SET_POINT, API_ACS_TEMP, API_COOL_MAX_TEMP, API_COOL_MIN_TEMP, API_COOL_SET_POINT, API_DATA, API_DOUBLE_SET_POINT, API_ERROR_HOT_WATER_NOT_CONNECTED, API_ERROR_REQUEST_MALFORMED, API_ERROR_SYSTEM_ID_NOT_AVAILABLE, API_ERROR_SYSTEM_ID_NOT_PROVIDED, API_ERROR_SYSTEM_ID_OUT_RANGE, API_ERROR_ZONE_ID_NOT_AVAILABLE, API_ERRORS, API_HEAT_MAX_TEMP, API_HEAT_MIN_TEMP, API_HEAT_SET_POINT, API_HUMIDITY, API_MANUFACTURER, API_MAX_TEMP, API_MC_CONNECTED, API_MIN_TEMP, API_MODE, API_MODES, API_NAME, API_ON, API_POWER, API_ROOM_TEMP, API_SET_POINT, API_SYSTEM_FIRMWARE, API_SYSTEM_ID, API_SYSTEM_TYPE, API_SYSTEMS, API_UNITS, API_ZONE_ID, ) class AirzoneACSStatus(IntEnum): """Supported features of the Airzone Local API.""" ENABLED = 0 DISABLED = 1 BOGUS = 2 class AirzoneACS: """Airzone Local API ACS.""" def __init__(self) -> None: """Local API ACS init.""" self.id: int = 0 self.on: bool = True self.power: bool = False self.status: AirzoneACSStatus = AirzoneACSStatus.DISABLED self.temp: int = 40 self.temp_max: int = 65 self.temp_min: int = 25 self.temp_set: int = 45 def data(self) -> dict[str, Any]: """Return Local API ACS data.""" if self.status == AirzoneACSStatus.BOGUS: return { API_ACS_MAX_TEMP: 0, API_ACS_MIN_TEMP: 0, API_ACS_ON: 0, API_ACS_POWER_MODE: 0, API_ACS_SET_POINT: 0, API_ACS_TEMP: 0, API_SYSTEM_ID: 0, } return { API_ACS_MAX_TEMP: self.temp_max, API_ACS_MIN_TEMP: self.temp_min, API_ACS_ON: int(self.on), API_ACS_POWER_MODE: int(self.power), API_ACS_SET_POINT: self.temp_set, API_ACS_TEMP: self.temp, API_SYSTEM_ID: self.id, } def refresh(self) -> None: """Refresh Local API ACS.""" if self.status != AirzoneACSStatus.ENABLED: return temp = self.temp_set + (random.randrange(-100, 100, 1) / 10) if temp <= self.temp_min: temp = self.temp_min elif temp >= self.temp_max: temp = self.temp_max self.temp = int(temp) def set_status(self, status: AirzoneACSStatus) -> None: """Set Airzone ACS status.""" self.status = status async def post(self) -> Response: """POST Local API ACS.""" if self.status == AirzoneACSStatus.DISABLED: return api_json_error(API_ERROR_HOT_WATER_NOT_CONNECTED) self.refresh() return api_json_response(self.data()) async def put(self, data: dict[str, Any]) -> Response: """PUT Local API ACS.""" if self.status == AirzoneACSStatus.DISABLED: return api_json_error(API_ERROR_HOT_WATER_NOT_CONNECTED) keys = list(data) + [API_SYSTEM_ID] on = data.get(API_ACS_ON) if on is not None: self.on = bool(on) power = data.get(API_ACS_POWER_MODE) if power is not None: self.power = bool(power) temp_set = data.get(API_ACS_SET_POINT) if temp_set is not None: self.temp_set = int(temp_set) return api_json_response(api_filter_dict(self.data(), keys)) class AirzoneSystem: """Airzone Local API System.""" def __init__(self, system_id: int): """Local API System init.""" self.errors: list[dict[str, str]] = [] self.firmware: str = "3.36" self.id: int = system_id self.mc_connected: bool = False self.manufacturer: str = "Python" self.power: int = 0 self.type: SystemType = SystemType.C3 def data(self) -> dict[str, Any]: """Return Local API System data.""" return { API_ERRORS: self.errors, API_MANUFACTURER: self.manufacturer, API_MC_CONNECTED: int(self.mc_connected), API_POWER: self.power, API_SYSTEM_FIRMWARE: self.firmware, API_SYSTEM_ID: self.id, API_SYSTEM_TYPE: self.type.value, } def matches(self, system_id: int) -> bool: """Check if system matches param.""" matches = False if system_id in (0, 127, self.id): matches = True return matches def refresh(self) -> None: """Refresh Local API System.""" if self.mc_connected: self.power = random.randrange(0, 5, 1) def post(self) -> dict[str, Any]: """POST Local API System.""" self.refresh() return self.data() def put(self) -> dict[str, Any]: """PUT Local API System.""" return self.data() class AirzoneZone: """Airzone Local API Zone.""" def __init__(self, name: str, system_id: int, zone_id: int, units: TemperatureUnit): """Local API Zone init.""" self.cool_temp_set: float = 22 self.cool_temp_max: float = 30 self.cool_temp_min: float = 18 self.heat_temp_set: float = 21 self.heat_temp_max: float = 30 self.heat_temp_min: float = 15 self.errors: list[dict[str, str]] = [] self.humidity: int = 50 self.id: int = zone_id self.mode: OperationMode = OperationMode.HEATING self.modes: list[OperationMode] = [ OperationMode.COOLING, OperationMode.HEATING, OperationMode.AUTO, ] self.name: str = name self.on: bool = True self.system: int = system_id self.temp: float = 22.3 self.temp_max: float = 30 self.temp_min: float = 15 self.temp_set: float = 21 self.units: TemperatureUnit = units def conv_temp(self, temp: float | int) -> float | int: """Convert temperature to Fahrenheit if needed.""" if self.units == TemperatureUnit.FAHRENHEIT: return celsius_to_fahrenheit(temp) return temp def get_modes(self) -> list[int]: """Get list of modes as integers.""" modes: list[int] = [] for mode in self.modes: modes += [int(mode)] return modes def data(self) -> dict[str, Any]: """Return Local API Zone data.""" _data: dict[str, Any] = { API_COOL_SET_POINT: self.conv_temp(self.cool_temp_set), API_COOL_MAX_TEMP: self.conv_temp(self.cool_temp_max), API_COOL_MIN_TEMP: self.conv_temp(self.cool_temp_min), API_ERRORS: self.errors, API_HEAT_SET_POINT: self.conv_temp(self.heat_temp_set), API_HEAT_MAX_TEMP: self.conv_temp(self.heat_temp_max), API_HEAT_MIN_TEMP: self.conv_temp(self.heat_temp_min), API_HUMIDITY: self.humidity, API_MAX_TEMP: self.conv_temp(self.temp_max), API_MIN_TEMP: self.conv_temp(self.temp_min), API_MODE: self.mode, API_MODES: self.get_modes(), API_ON: int(self.on), API_ROOM_TEMP: self.conv_temp(self.temp), API_SET_POINT: self.conv_temp(self.temp_set), API_SYSTEM_ID: self.system, API_UNITS: int(self.units), API_ZONE_ID: self.id, } if len(self.name) > 0: _data[API_NAME] = self.name if OperationMode.AUTO in _data[API_MODES]: _data[API_DOUBLE_SET_POINT] = True return _data def matches(self, system_id: int, zone_id: int) -> bool: """Check if Zone matches params.""" matches = False if system_id == 0: if zone_id == 0: matches = True elif zone_id == self.id: matches = True elif zone_id == 0: if system_id == self.system: matches = True elif system_id == self.system and zone_id == self.id: matches = True return matches def refresh(self) -> None: """Refresh Local API Zone.""" self.humidity = 50 + random.randrange(-40, 40, 1) temp = self.temp_set + (random.randrange(-100, 100, 1) / 10) if temp <= self.temp_min: temp = self.temp_min elif temp >= self.temp_max: temp = self.temp_max self.temp = temp def post(self) -> dict[str, Any]: """POST Local API Zone.""" self.refresh() return self.data() def put(self, data: dict[str, Any]) -> dict[str, Any]: """PUT Local API Zone.""" keys = list(data) + [API_SYSTEM_ID, API_ZONE_ID] cool_temp_set = data.get(API_COOL_SET_POINT) if cool_temp_set is not None: self.cool_temp_set = self.conv_temp(float(cool_temp_set)) heat_temp_set = data.get(API_HEAT_SET_POINT) if heat_temp_set is not None: self.heat_temp_set = self.conv_temp(float(heat_temp_set)) mode = data.get(API_MODE) if mode is not None: self.mode = OperationMode(mode) on = data.get(API_ON) if on is not None: self.on = bool(on) temp_set = data.get(API_SET_POINT) if temp_set is not None: temp_set = float(temp_set) conv_temp_set = self.conv_temp(temp_set) self.temp_set = conv_temp_set if self.mode == OperationMode.AUTO: if temp_set >= self.temp: self.heat_temp_set = conv_temp_set else: self.cool_temp_set = conv_temp_set elif self.mode == OperationMode.COOLING: self.cool_temp_set = conv_temp_set elif self.mode == OperationMode.HEATING: self.heat_temp_set = conv_temp_set return cast(dict[str, Any], api_filter_dict(self.data(), keys)) class AirzoneHVAC: """Airzone Local API HVAC.""" def __init__(self) -> None: """Local API HVAC init.""" self.acs: AirzoneACS = AirzoneACS() self.systems: dict[int, AirzoneSystem] = {} self.zones: dict[str, AirzoneZone] = {} def add_zone( self, name: str, system_id: int, zone_id: int, units: TemperatureUnit ) -> bool: """Local API HVAC add Zone.""" system_zone_id = get_system_zone_id(system_id, zone_id) if system_zone_id not in self.zones: if system_id not in self.systems: system = AirzoneSystem(system_id) self.systems[system_id] = system zone = AirzoneZone(name, system_id, zone_id, units) self.zones[system_zone_id] = zone return True return False def check_system(self, system_id: int) -> bool: """Check if System exists.""" for zone in self.zones.values(): if system_id in (0, 127, zone.system): return True return False def check_zone(self, system_id: int, zone_id: int) -> bool: """Check if Zone exists.""" for zone in self.zones.values(): if system_id in (0, zone.system): if zone_id in (0, zone.id): return True return False def system_response( self, system_id: int, hvac_data: list[dict[str, Any]] ) -> Response: """Return HVAC System data.""" if len(hvac_data) == 0: if not self.check_system(system_id): return api_json_error(API_ERROR_SYSTEM_ID_NOT_AVAILABLE) return api_json_error(API_ERROR_REQUEST_MALFORMED) return api_json_response({API_DATA: hvac_data}) def post_system(self, system_id: int) -> Response: """POST Local API HVAC System.""" hvac_data: list[dict[str, Any]] = [] for system in self.systems.values(): if system.matches(system_id): hvac_data += [system.post()] return self.system_response(system_id, hvac_data) def put_system(self, system_id: int) -> Response: """PUT Local API HVAC Zone.""" hvac_data: list[dict[str, Any]] = [] for system in self.systems.values(): if system.matches(system_id): hvac_data += [system.put()] return self.system_response(system_id, hvac_data) def zone_response( self, system_id: int, zone_id: int, hvac_data: list[dict[str, Any]] ) -> Response: """Return HVAC Zone data.""" if len(hvac_data) == 0: if not self.check_system(system_id): return api_json_error(API_ERROR_SYSTEM_ID_NOT_AVAILABLE) if not self.check_zone(system_id, zone_id): return api_json_error(API_ERROR_ZONE_ID_NOT_AVAILABLE) return api_json_error(API_ERROR_REQUEST_MALFORMED) if system_id == 0: response = { API_SYSTEMS: [ { API_DATA: hvac_data, } ] } else: response = { API_DATA: hvac_data, } return api_json_response(response) def post_zone(self, system_id: int, zone_id: int) -> Response: """POST Local API HVAC Zone.""" hvac_data: list[dict[str, Any]] = [] for zone in self.zones.values(): if zone.matches(system_id, zone_id): hvac_data += [zone.post()] return self.zone_response(system_id, zone_id, hvac_data) def put_zone(self, system_id: int, zone_id: int, data: dict[str, Any]) -> Response: """PUT Local API HVAC Zone.""" hvac_data: list[dict[str, Any]] = [] for zone in self.zones.values(): if zone.matches(system_id, zone_id): hvac_data += [zone.put(data)] return self.zone_response(system_id, zone_id, hvac_data) async def post(self, request: web.Request) -> Response: # pylint: disable=too-many-return-statements """POST Local API HVAC.""" data = await request.json() if isinstance(data, dict): system = data.get(API_SYSTEM_ID) zone = data.get(API_ZONE_ID) else: system = None zone = None if system is None: return api_json_error(API_ERROR_SYSTEM_ID_NOT_PROVIDED) if system == 127: return self.post_system(system) if system == 0: if zone is None: return await self.acs.post() if 0 <= zone <= 32: return self.post_zone(system, zone) return api_json_error(API_ERROR_ZONE_ID_NOT_AVAILABLE) if 0 < system <= 32: if zone is None: return self.post_system(system) return self.post_zone(system, zone) return api_json_error(API_ERROR_SYSTEM_ID_OUT_RANGE) async def put(self, request: web.Request) -> Response: # pylint: disable=too-many-return-statements """PUT Local API HVAC.""" data = await request.json() if isinstance(data, dict): system = data.get(API_SYSTEM_ID) zone = data.get(API_ZONE_ID) else: system = None zone = None if system is None: return api_json_error(API_ERROR_SYSTEM_ID_NOT_PROVIDED) if system == 127: return self.put_system(system) if system == 0: if zone is None: return await self.acs.put(data) if 0 <= zone <= 32: return self.put_zone(system, zone, data) return api_json_error(API_ERROR_ZONE_ID_NOT_AVAILABLE) if 0 < system <= 32: if zone is None: return self.put_system(system) return self.put_zone(system, zone, data) return api_json_error(API_ERROR_SYSTEM_ID_OUT_RANGE) aioairzone-1.0.0/sim/integration.py000066400000000000000000000023271477442374000173550ustar00rootroot00000000000000"""Airzone Local API Integration.""" from typing import Any from aiohttp import web from aiohttp.web_response import Response from helpers import api_filter_dict, api_json_response from aioairzone.const import API_DRIVER, API_SYSTEM_ID class AirzoneIntegration: """Airzone Local API Integration.""" def __init__(self, driver: str) -> None: """Local API Integration init.""" self.driver: str = driver def data(self) -> dict[str, Any]: """Return Local API Integration data.""" return { API_DRIVER: self.driver, } async def post(self, request: web.Request) -> Response: # pylint: disable=unused-argument """POST Local API Integration.""" return api_json_response(self.data()) async def put(self, request: web.Request) -> Response: """PUT Local API Integration.""" data = await request.json() if isinstance(data, dict): keys = list(data) + [API_SYSTEM_ID] driver = data.get(API_DRIVER) if driver is not None: self.driver = data[API_DRIVER] return api_json_response(api_filter_dict(self.data(), keys)) return api_json_response(self.data()) aioairzone-1.0.0/sim/version.py000066400000000000000000000011731477442374000165150ustar00rootroot00000000000000"""Airzone Local API Version.""" from typing import Any from aiohttp.web_response import Response from helpers import api_json_response from aioairzone.const import API_VERSION class AirzoneVersion: """Airzone Local API Version.""" def __init__(self, version: str) -> None: """Local API Version init.""" self.version: str = version def data(self) -> dict[str, Any]: """Return Local API Version data.""" return { API_VERSION: self.version, } async def post(self) -> Response: """POST Local API version.""" return api_json_response(self.data()) aioairzone-1.0.0/sim/webserver.py000066400000000000000000000036261477442374000170410ustar00rootroot00000000000000"""Airzone Local API WebServer.""" import random from typing import Any from aiohttp.web_response import Response from helpers import api_json_response from aioairzone.const import ( API_INTERFACE, API_MAC, API_WIFI, API_WIFI_CHANNEL, API_WIFI_QUALITY, API_WIFI_RSSI, API_WS_AZ, API_WS_FIRMWARE, API_WS_TYPE, ) class AirzoneWebServer: """Airzone Local API WebServer.""" def __init__(self, mac: str) -> None: """Local API WebServer init.""" self.firmware: str = "3.44" self.interface: str = API_WIFI self.mac: str = mac self.type: str = API_WS_AZ self.wifi_channel: int = 6 self.wifi_rssi: int = -50 def wifi_quality(self) -> int: """Convert Wifi RSSI to Quality.""" quality: float if self.wifi_rssi <= -100: quality = 0 elif self.wifi_rssi >= -50: quality = 100 else: quality = 2 * (self.wifi_rssi + 100) quality = round(quality / 10, 0) * 10 return int(quality) def data(self) -> dict[str, Any]: """Return Local API Version data.""" return { API_INTERFACE: API_WIFI, API_MAC: self.mac, API_WIFI_CHANNEL: self.wifi_channel, API_WIFI_QUALITY: self.wifi_quality(), API_WIFI_RSSI: self.wifi_rssi, API_WS_FIRMWARE: self.firmware, API_WS_TYPE: self.type, } def refresh(self) -> None: """Refresh Local API WebServer.""" wifi_rssi = self.wifi_rssi + random.randrange(-10, 10, 1) if wifi_rssi <= -100: self.wifi_rssi = -100 elif wifi_rssi >= -50: self.wifi_rssi = -50 else: self.wifi_rssi = wifi_rssi async def post(self) -> Response: """POST Local API WebServer.""" self.refresh() return api_json_response(self.data())