From 89518880439c81eb828bccf7a3138b6fb864f90f Mon Sep 17 00:00:00 2001 From: runner Date: Sun, 28 Jul 2024 10:48:31 +0000 Subject: [PATCH] Generate 2.5.0 docs --- docs/2.5.0/CHANGELOG.md | 201 ++++ docs/2.5.0/README.md | 177 ++++ .../2.5.0/_images/interrupt_callbacks.dot.svg | 140 +++ docs/2.5.0/_images/interrupt_handling.dot.svg | 471 +++++++++ docs/2.5.0/_images/pimcp.png | Bin 0 -> 136557 bytes docs/2.5.0/_sidebar.md | 34 + docs/2.5.0/_sidebar.md.j2 | 27 + docs/2.5.0/config/ha_discovery.md | 83 ++ docs/2.5.0/config/interrupts.md | 113 ++ docs/2.5.0/config/reference.md.j2 | 59 ++ .../config/reference/digital_inputs/README.md | 333 ++++++ .../reference/digital_outputs/README.md | 254 +++++ .../config/reference/gpio_modules/README.md | 79 ++ docs/2.5.0/config/reference/logging/README.md | 14 + docs/2.5.0/config/reference/mqtt/README.md | 445 ++++++++ docs/2.5.0/config/reference/options/README.md | 29 + .../config/reference/reporting/README.md | 54 + .../config/reference/sensor_inputs/README.md | 176 +++ .../config/reference/sensor_modules/README.md | 80 ++ .../config/reference/stream_modules/README.md | 135 +++ docs/2.5.0/config/scenarios.md | 154 +++ docs/2.5.0/config/v2-changes.md | 48 + docs/2.5.0/deployment/docker.md | 31 + docs/2.5.0/deployment/supervisor.md | 46 + docs/2.5.0/dev/config_schema.md | 9 + docs/2.5.0/dev/modules/README.md | 238 +++++ docs/2.5.0/dev/modules/README.md.j2 | 166 +++ docs/2.5.0/generate_docs.py | 455 ++++++++ docs/2.5.0/index.html | 29 + docs/2.5.0/schema.json | 998 ++++++++++++++++++ docs/2.5.0/versions.md.j2 | 14 + docs/index.html | 6 +- docs/versions.md | 1 + 33 files changed, 5096 insertions(+), 3 deletions(-) create mode 100644 docs/2.5.0/CHANGELOG.md create mode 100644 docs/2.5.0/README.md create mode 100644 docs/2.5.0/_images/interrupt_callbacks.dot.svg create mode 100644 docs/2.5.0/_images/interrupt_handling.dot.svg create mode 100644 docs/2.5.0/_images/pimcp.png create mode 100644 docs/2.5.0/_sidebar.md create mode 100644 docs/2.5.0/_sidebar.md.j2 create mode 100644 docs/2.5.0/config/ha_discovery.md create mode 100644 docs/2.5.0/config/interrupts.md create mode 100644 docs/2.5.0/config/reference.md.j2 create mode 100644 docs/2.5.0/config/reference/digital_inputs/README.md create mode 100644 docs/2.5.0/config/reference/digital_outputs/README.md create mode 100644 docs/2.5.0/config/reference/gpio_modules/README.md create mode 100644 docs/2.5.0/config/reference/logging/README.md create mode 100644 docs/2.5.0/config/reference/mqtt/README.md create mode 100644 docs/2.5.0/config/reference/options/README.md create mode 100644 docs/2.5.0/config/reference/reporting/README.md create mode 100644 docs/2.5.0/config/reference/sensor_inputs/README.md create mode 100644 docs/2.5.0/config/reference/sensor_modules/README.md create mode 100644 docs/2.5.0/config/reference/stream_modules/README.md create mode 100644 docs/2.5.0/config/scenarios.md create mode 100644 docs/2.5.0/config/v2-changes.md create mode 100644 docs/2.5.0/deployment/docker.md create mode 100644 docs/2.5.0/deployment/supervisor.md create mode 100644 docs/2.5.0/dev/config_schema.md create mode 100644 docs/2.5.0/dev/modules/README.md create mode 100644 docs/2.5.0/dev/modules/README.md.j2 create mode 100644 docs/2.5.0/generate_docs.py create mode 100644 docs/2.5.0/index.html create mode 100644 docs/2.5.0/schema.json create mode 100644 docs/2.5.0/versions.md.j2 diff --git a/docs/2.5.0/CHANGELOG.md b/docs/2.5.0/CHANGELOG.md new file mode 100644 index 00000000..52b65484 --- /dev/null +++ b/docs/2.5.0/CHANGELOG.md @@ -0,0 +1,201 @@ +Unreleased +========== +- Nothing! + +.v2.4.0 - 2024-07-20 +==================== +- Bump tj-actions/branch-names from 2.2 to 7.0.7 in /.github/workflows by @dependabot in https://github.com/flyte/mqtt-io/pull/339 +- # Fix for poetry/docutils related bug by @BenjiU in https://github.com/flyte/mqtt-io/pull/367 +- upgrade DHT11/DHT22 backing library by @pansila in https://github.com/flyte/mqtt-io/pull/297 +- Install gcc for slim docker to build rpi.gpio on demand by @BenjiU in https://github.com/flyte/mqtt-io/pull/368 +- Remove lint warnings from bmp085.py by @BenjiU in https://github.com/flyte/mqtt-io/pull/375 +- Add support for YF-S201 flow rate sensor by @linucks in https://github.com/flyte/mqtt-io/pull/370 +- Support for ENS160 digital multi-gas sensor with multiple IAQ data (TVOC, eCO2, AQI) by @linucks in https://github.com/flyte/mqtt-io/pull/371 +- feat: add MH-Z19 sensor module by @kleest in https://github.com/flyte/mqtt-io/pull/365 +- Add Support for Sunxi Linux Boards by @fabys77 in https://github.com/flyte/mqtt-io/pull/100 + +.v2.3.0 - 2024-03-01 +==================== +- 324 pinned pyyaml version incompatible with latest cython 300 by @BenjiU in #325 +- fix pipeline for tagging by @BenjiU in #323 +- pin pyyaml to v6.0.1 by @BenjiU in #326 +- Add new module for sensor adxl345 by @birdie1 in #223 +- Sensor INA219: Use optional i2c_bus_num by @mschlenstedt in #328 +- Update ads1x15.py by @maxthebuch in #329 +- repeat subscribe when reconnected to MQTT broker by @JohannesHennecke in #337 +- Fix non-unique identifiers reporting to HA by @dolai1 in #345 +- docker: use a "slim" base image by @chatziko in #342 +- Fix applying mqtt.reconnect_count by reordering except clauses by @zzeekk in #331 +- Add PMS5003 Particulate Sensor by @johnwang16 in #346 +- gpiod: enable pullup/pulldown by @chatziko in #341 +- docker: slim image, use rustup, build deps only on armv7 by @chatziko in #352 + +.v2.2.9d - 2023-07-18 +==================== +- new sensors +- fix for reconnection problem + +.v2.2.8 - 2023-01-19 +==================== +- Fix for #280 by @rlehfeld in #281 +- Fix reconnects_remaining referenced before assignment by @SamLeatherdale in #274 +- Only create one instance of sensor_module for ADS1x15 by @shbatm in #286 +- PN532 NFC/RFID reader implementation by @vytautassurvila in #269 +- Update README.md by @OzGav in #264 +- FIX OrangePi module by @neatherweb in #285 +- New DockerPi 4 Channel Relay GPIO module by @claudegel in #246 +- Digital Output: fix initial state inconsistency by @hacker-cb in #238 +- Add module mcp3xxx by @koleo9am in #227 +- Always remove finished transient_tasks. by @gmsoft-tuxicoman in #301 + +.v2.2.7 - 2022-07-07 +==================== +- Fix some minor pylint issues and silence some others. +- Fix bug with changing reference to 'edge' in raspberrypi module. #268 @vytautassurvila +- Add INA219 sensor module. #221 @birdie1 +- Implement PinPUD.OFF for pcf8574/5. #217 @IlmLV +- Ensure HCSR04 distance cannot be None. #215 @joseffallman +- Add GPIOZero module. #212 @fipwmaqzufheoxq92ebc +- Render config with confp to allow dynamic configuration based on environment/redis/etcd vars. #210 @fipwmaqzufheoxq92ebc +- Log uncaught exceptions to configured logging handlers. #206 @fipwmaqzufheoxq92ebc + +v2.2.6 - 2021-04-23 +=================== +- Create docs in a tempdir to stop them from being clobbered when changing branches. + +v2.2.5 - 2021-04-23 +=================== +- Sort versions in docs. Use git pull properly. + +v2.2.4 - 2021-04-23 +=================== +- Generate docs versions and root index to strings and write them after switching branches + +v2.2.3 - 2021-04-23 +=================== +- Add docs root index to git separately + +v2.2.2 - 2021-04-23 +=================== +- Fix version regex for docs index generation + +v2.2.1 - 2021-04-23 +=================== +- Handle tags in generate docs script. + +v2.2.0 - 2021-04-23 +=================== +- Multi-versioned documentation. +- Auto-reconnect to MQTT server on disconnection. #207 @fipwmaqzufheoxq92ebc + +v2.1.8 - 2021-04-21 +=================== +- Fix broken hcsr04 sensor that I (@flyte) broke when rewriting for v2.x. #211 @r00tat +- Fix inversion not taken into account when publishing initial digital output value. #203 @r00tat +- Fix #198 where Future wasn't created from the right thread. #205 @fipwmaqzufheoxq92ebc + +v2.1.7 - 2021-04-01 +=================== +- Add install_requirements config option to skip installing missing module requirements. #199 + +v2.1.6 - 2021-04-01 +=================== +- Add ADS1x15 module. #200 @r00tat + +v2.1.5 - 2021-04-01 +=================== +- Update PyYAML version to 5.4 CVE-2020-14343 + +v2.1.4 - 2021-03-26 +=================== +- Add version to 'model' field of HA Discovery config payload. #196 @pbill2003 + +v2.1.3 - 2021-03-26 +=================== +- Add missing `spi_device` config schema entry for MCP3008 sensor module. #194 + +v2.1.2 - 2021-03-24 +=================== +- Remove config validation that checks usage of the same numbered pin used twice. #191 + +v2.1.1 - 2021-03-16 +=================== +- Fix bodged BH1750 sensor value reading code. #189 + +v2.1.0 - 2021-03-11 +=================== +- Add *OPT-IN* error reporting to sentry. Bumps minor version because it adds a config entry. + +v2.0.1 - 2021-03-11 +=================== +- Fix bug where sensor config was retrieved from the wrong place https://github.com/flyte/mqtt-io/issues/185 + +v2.0.0 - 2021-03-07 +=================== +- Rewrite core with asyncio +- Change MQTT client to asyncio-mqtt +- Add better validation for config +- [Move some config values around](https://flyte.github.io/mqtt-io/#/config/v2-changes), but mostly stay compatible with existing configs +- Add MCP23017 module +- [Rework interrupts](https://flyte.github.io/mqtt-io/#/config/interrupts) to allow for pins to be interrupts for other pins on other modules +- Enable extra values to be added to the [Home Assistant Discovery](https://flyte.github.io/mqtt-io/#/config/ha_discovery) config payloads +- Rename package from pi-mqtt-gpio to mqtt-io since it's not just for Raspberry Pi, and not just for GPIO +- Create generated documentation for the config file options ("Section Reference" section of [the documentation](https://flyte.github.io/mqtt-io/#/)) +- Tons more stuff, too varied to list here. It's safe to say that almost everything has been improved (hopefully) in some way + +v0.5.3 - 2020-10-17 +=================== +- Add PCF8575 support. #121 +- Add MCP3008 sensor support. #115 +- Add AHT20 sensor support. #122 +- Add BME280 sensor support. #132 +- Install requirements using current Python executable. #134 +- Add sensors to HASS discovery. #133 +- Add option to publish output value on startup. #125 + +v0.5.2 - 2020-10-17 +=================== +- Update PyYAML to a version that doesn't suffer from CVE-2020-1747 vulnerability. +- Add 'stream' IO. + +v0.3.1 - 2019-03-10 +=================== +- Pin safe version of PyYAML in requirements. + +v0.3.0 - 2019-03-10 +=================== +- Merge PR from @BenjiU which implements a new sensor interface. #52 + +v0.0.12 - 2017-07-26 +==================== +- Add cleanup function to modules which are called before program exit. #16 + +v0.0.11 - 2017-07-26 +==================== +- Decode received MQTT message payload as utf8 before trying to match with on/off payload values. #14 + +v0.0.10 - 2017-07-26 +==================== +- Fix bug with selection of pullup value in raspberrypi module when none set. #15 + +v0.0.9 - 2017-07-26 +=================== +- Successful fix for bug with loading config schema. #13 + +v0.0.8 - 2017-07-26 +=================== +- Failed fix for bug with loading config schema. #13 + +v0.0.7 - 2017-07-17 +=================== + +- Implement `set_on_ms` and `set_off_ms` topic suffixes. Closes #10 + +v0.0.6 - 2017-07-17 +=================== + +- Large refactor and tidyup. +- Implement config validation using cerberus. +- Enable configuration of MQTT protocol. Closes #11. +- Deploy Python Wheel as well as source package. +- Add some (not exhaustive) tests. diff --git a/docs/2.5.0/README.md b/docs/2.5.0/README.md new file mode 100644 index 00000000..ec6b4ca1 --- /dev/null +++ b/docs/2.5.0/README.md @@ -0,0 +1,177 @@ + + +# MQTT IO + +_Documentation version: `2.5.0`_ + +[![Discord](https://img.shields.io/discord/713749043662290974.svg?label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/gWyV9W4) + +Exposes general purpose inputs and outputs (GPIO), hardware sensors and serial devices to an MQTT server. Ideal for single-board computers such as the Raspberry Pi. + +## Supported Hardware + +Hardware support is provided by specific GPIO, Sensor and Stream modules. It's easy to add support for new hardware and the list is growing fast. + +### GPIO Modules + + - Beaglebone GPIO (`beaglebone`) + - DockerPi 4 Channel Relay GPIO (`dockerpi`) + - Linux Kernel 4.8+ libgpiod (`gpiod`) + - GPIO Zero (`gpiozero`) + - MCP23017 IO expander (`mcp23017`) + - Orange Pi GPIO (`orangepi`) + - PCF8574 IO expander (`pcf8574`) + - PCF8575 IO expander (`pcf8575`) + - PiFace Digital IO 2 (`piface2`) + - Raspberry Pi GPIO (`raspberrypi`) + - Sunxi Board (`sunxi`) + +### Sensors + + - ADS1x15 analog to digital converters (`ads1x15`) + - ADXL345 Digital Accelerometer Sensor + +Mandatory: +- chip_addr + +Optional: +- output_g (set True if output in g). default:m*s² + +Output: +- x (in m*s²) +- y (in m*s²) +- z (in m*s²) (`adxl345`) + - AHT20 temperature and humidity sensor (`aht20`) + - BH1750 light level sensor (`bh1750`) + - BME280 temperature, humidity and pressure sensor (`bme280`) + - BME680 temperature, humidity and pressure sensor (`bme680`) + - BMP085 temperature and pressure sensor (`bmp085`) + - DHT11/DHT22/AM2302 temperature and humidity sensors (`dht22`) + - DS18S20/DS1822/DS18B20/DS1825/DS28EA00/MAX31850K temperature sensors (`ds18b`) + - ENS160 Air Quality Sensor + +sensor_modules: + - name: ens160 + module: ens160 + chip_addr: 0x53 + temperature_compensation: 25 + humidity_compensation: 50 + +sensor_inputs: + - name: air_quality + module: ens160 + interval: 10 + digits: 0 + type: aqi + + - name: volatile_organic_compounds + module: ens160 + interval: 10 + digits: 0 + type: tvoc + + - name: eco2 + module: ens160 + interval: 10 + digits: 0 + type: eco2 (`ens160`) + - HCSR04 ultrasonic range sensor (connected to the Raspberry Pi on-board GPIO) (`hcsr04`) + - INA219 DC current sensor (`ina219`) + - LM75 temperature sensor (`lm75`) + - MCP3008 analog to digital converter (`mcp3008`) + - MCP3xxx analog to digital converter via GPIOZero (`mcp3xxx`) + - MH-Z19 NDIR CO2 sensor (`mhz19`) + - PMS5003 Particulate Matter Sensor (`pms5003`) + - SHT4x temperature and humidity sensor (`sht4x`) + - YF-S201 Flow Rate Sensor + +Example configuration: + +sensor_modules: + - name: yfs201 + module: yfs201 + +sensor_inputs: + - name: flow_rate1 + module: yfs201 + pin: 0 + digits: 0 + interval: 10 (`yfs201`) + +### Streams + + - PN532 NFC/RFID reader (`pn532`) + - Serial port (`serial`) + +## Installation + +_Requires Python 3.6+_ + +`pip3 install mqtt-io` + +## Execution + +`python3 -m mqtt_io config.yml` + +## Configuration Example + +Configuration is written in a YAML file which is passed as an argument to the server on startup. + + + +The following example will configure the software to do the following: + +- Publish MQTT messages on the `home/input/doorbell` topic when the doorbell is pushed and released. +- Subscribe to the MQTT topic `home/output/port_light/set` and change the output when messages are received on it. +- Periodically read the value of the LM75 sensor and publish it on the MQTT topic `home/sensor/porch_temperature`. +- Publish any data received on the `/dev/ttyUSB0` serial port to the MQTT topic `home/serial/alarm_system`. +- Subscribe to the MQTT topic `home/serial/alarm_system/send` and send any data received on that topic to the serial port. + +```yaml +mqtt: + host: localhost + topic_prefix: home + +# GPIO +gpio_modules: + # Use the Raspberry Pi built-in GPIO + - name: rpi + module: raspberrypi + +digital_inputs: + # Pin 0 is an input connected to a doorbell button + - name: doorbell + module: rpi + pin: 0 + +digital_outputs: + # Pin 1 is an output connected to a light + - name: porch_light + module: rpi + pin: 1 + +# Sensors +sensor_modules: + # An LM75 sensor attached to the I2C bus + - name: lm75_sensor + module: lm75 + i2c_bus_num: 1 + chip_addr: 0x48 + +sensor_inputs: + # The configuration of the specific sensor value to use (LM75 only has temperature) + - name: porch_temperature + module: lm75_sensor + +# Streams +stream_modules: + # A serial port to communicate with the house alarm system + - name: alarm_system + module: serial + device: /dev/ttyUSB0 + baud: 9600 +``` \ No newline at end of file diff --git a/docs/2.5.0/_images/interrupt_callbacks.dot.svg b/docs/2.5.0/_images/interrupt_callbacks.dot.svg new file mode 100644 index 00000000..f8bcc033 --- /dev/null +++ b/docs/2.5.0/_images/interrupt_callbacks.dot.svg @@ -0,0 +1,140 @@ + + + + + + +%3 + +Interrupt Initialisation + +cluster_init_digital_input + +initialise_digital_input() + + +cluster_module_setup_interrupt + +module.setup_interrupt() + + + +calls_setup_interrupt + +Call gpio module's setup_interrupt() method +for a specific pin, passing in callback function + + + +q_software_callback + +GPIO hardware's library supports software callbacks? + + + +calls_setup_interrupt->q_software_callback + + + + + +q_interrupt_pin + +GPIO hardware outputs on dedicated pin(s) upon interrupt? + + + +q_software_callback->q_interrupt_pin + + +No + + + +config_software_callback + +Add gpio_module.interrupt_handler() as +callback when configuring pin interrupt + + + +q_software_callback->config_software_callback + + +Yes + + + +q_set_triggers + +Can configure GPIO hardware to interrupt on specific pins only? + + + +q_interrupt_pin->q_set_triggers + + +Yes + + + +ints_not_supported + +Interrupts not supported + + + +q_interrupt_pin->ints_not_supported + + +No + + + +enable_int_all_pins + +Enable interrupts (if required) for all pins + + + +q_set_triggers->enable_int_all_pins + + +No + + + +enable_int_pin + +Enable interrupts for this pin + + + +q_set_triggers->enable_int_pin + + +Yes + + + +store_callback + +Store supplied callback for this pin in self.interrupt_callbacks[pin] + + + +enable_int_all_pins->store_callback + + + + + +enable_int_pin->store_callback + + + + + diff --git a/docs/2.5.0/_images/interrupt_handling.dot.svg b/docs/2.5.0/_images/interrupt_handling.dot.svg new file mode 100644 index 00000000..a0dc96be --- /dev/null +++ b/docs/2.5.0/_images/interrupt_handling.dot.svg @@ -0,0 +1,471 @@ + + + + + + +%3 + +Interrupt Handling +For GPIO libraries with software interrupt callbacks, +and how they handle being an interrupt for other modules + +cluster_main_int_callback + +MqttIo.interrupt_callback() + + +cluster_handle_remote_int + +MqttIo.handle_remote_interrupt() + + +cluster_handle_remote_int_closure_func + +handle_remote_interrupt_task() + + +cluster_await_remote_ints + +await_remote_interrupts() + + +cluster_get_int_pin_values + +GenericGPIO.get_interrupt_values_remote(pins) + + + +mcp_gpio_input + +MCP input pin logic level changes + + + +mcp_int_output + +MCP interrupt output pin logic level changes + + + +mcp_gpio_input->mcp_int_output + + + + + +pi_gpio_input + +Pi input pin logic level changes + + + +mcp_int_output->pi_gpio_input + + + + + +pi_int_callback + +Pi interrupt callback is triggered + + + +pi_gpio_input->pi_int_callback + + + + + +main_int_callback + +Gets called by module's software +callback with whichever args supplied + + + +pi_int_callback->main_int_callback + + + + + +q_is_remote_int + +Configured as interrupt for other module's pin(s)? + + + +get_all_interrupt_for_pins + +Get list of pin names that this pin_name is an interrupt for + + + +q_is_remote_int->get_all_interrupt_for_pins + + +Yes + + + +get_interrupt_value + +Get value of pin that caused the interrupt +using [module].GPIO.get_interrupt_value(pin) + + + +q_is_remote_int->get_interrupt_value + + +No + + + +q_acquire_interrupt_lock + +Acquire lock for this interrupt pin? + + + +q_acquire_interrupt_lock->q_is_remote_int + + +Acquired + + + +ignore_interrupt + + +Ignore this interrupt + + + +q_acquire_interrupt_lock->ignore_interrupt + + +Locked + + + +main_int_callback->q_acquire_interrupt_lock + + + + + +fire_digital_input_changed_event_main + + +Fire DigitalInputChangedEvent + + + +organise_into_modules + +Organise pins by module + + + +get_all_interrupt_for_pins->organise_into_modules + + + + + +get_interrupt_value->fire_digital_input_changed_event_main + + + + + +make_funcs_to_get_pin_vals_and_fire_event + +Make async closure functions to call get_interrupt_values_remote(pins) +on each module and fire events if/when the values are received + + + +organise_into_modules->make_funcs_to_get_pin_vals_and_fire_event + + + + + +add_the_functions_to_a_list + +Add these functions to a list + + + +make_funcs_to_get_pin_vals_and_fire_event->add_the_functions_to_a_list + + + + + +make_func_to_await_the_funcs + +Make async closure function to await all of the functions + + + +add_the_functions_to_a_list->make_func_to_await_the_funcs + + + + + +add_awaiting_func_to_task_list + +Add the awaiting function to the task list + + + +make_func_to_await_the_funcs->add_awaiting_func_to_task_list + + + + + +await_all_remote_int_tasks + +Await all remote interrupt tasks + + + +add_awaiting_func_to_task_list->await_all_remote_int_tasks + + + + + +call_modules_to_get_int_values + +Call get_interrupt_values_remote(pins) on the module + + + +q_can_identify_pin + +Can identify which pin caused the interrupt? +(InterruptSupport.FLAG_REGISTER) + + + +call_modules_to_get_int_values->q_can_identify_pin + + + + + +fire_digital_input_changed_event_closure + + +Fire DigitalInputChangedEvent + + + +release_int_lock + + +Release interrupt lock + + + +fire_digital_input_changed_event_closure->release_int_lock + + + + + +await_all_remote_int_tasks->call_modules_to_get_int_values + + + + + +check_all_possible_pins + +Check all pins on this module that +could have triggered the interrupt + + + +q_can_identify_pin->check_all_possible_pins + + +No + + + +get_flagged_pins + +gpio_module.get_int_pins() +Get flagged interrupt pins + + + +q_can_identify_pin->get_flagged_pins + + +Yes + + + +q_can_capture_pin_val + +Can get captured pin interrupt value? +(InterruptSupport.CAPTURE_REGISTER) + + + +q_which_edge + +Which edge(s) were these interrupt(s) configured for? + + + +q_can_capture_pin_val->q_which_edge + + +No + + + +capture_pin_value + +gpio_module.get_captured_int_pin_values(pins) +Get captured pin value at time of interrupt + + + +q_can_capture_pin_val->capture_pin_value + + +Yes + + + +poll_pin + +Poll the pin for its current value + + + +q_which_edge->poll_pin + + +Both + + + +pin_value_high + +Pin's value is 'high' + + + +q_which_edge->pin_value_high + + +Rising + + + +pin_value_low + +Pin's value is 'low' + + + +q_which_edge->pin_value_low + + +Falling + + + +q_any_pins_changed + +Pin(s) value changed? + + + +return_pin_values + + +Return pin numbers and their values + + + +q_any_pins_changed->return_pin_values + + +Yes + + + +do_nothing + + +Return empty list? +TODO: Make this behaviour configurable + + + +q_any_pins_changed->do_nothing + + +No + + + +check_all_possible_pins->q_can_capture_pin_val + + + + + +poll_pin->q_any_pins_changed + + + + + +get_flagged_pins->q_can_capture_pin_val + + + + + +capture_pin_value->return_pin_values + + + + + +pin_value_high->return_pin_values + + + + + +pin_value_low->return_pin_values + + + + + +return_pin_values->fire_digital_input_changed_event_closure + + + + + diff --git a/docs/2.5.0/_images/pimcp.png b/docs/2.5.0/_images/pimcp.png new file mode 100644 index 0000000000000000000000000000000000000000..bc7dda8185a299886acc6c1558bbc6257d9cd83a GIT binary patch literal 136557 zcmdq}c{r8t8b1uLNUda8NttCzWXv2wD^X_BXo`dkna50JPKr#GWU3G)q0BNRNyt2B ziZqx~lz2YtyN}=dzJI;X^Y^okWAEKA*1FeqpVxVQrt6L}(9@)+Wv3+&2=vE~sT&ap z)Q<@Ssv05{{$^QE(**ys&E?oxcLIT-f%2bCi6RUf1OgA?xcXsZpZnj2eNA;nHx%Zj zrCVhlUEH5&bm?#;?ZsX}lWj+ZE1OTy*fpepNHJrGS0JxZrtEh_IZAX zYV-dV>neSEqidOIsKLpTyA10l7QDXo)lMv|_y=leY)RGIbI{HD?Ag03R{#C90G2lL zYI1V&ty_#S8d*lU=gyrIIhCLD6(EP1pBrp}bA8&@tb`(XS;Pd-Bizo*T6- z>JJYOx3si;{P^)CZCP0v4GoQt?tw-kel++dm0zzKe)dM&bY5=mp`dkTVd3VUo>MvG z-e+ds-v=H&dPH;Kzv^xODAeBc-}@5;IDRftE(sfMtFXj>e~5L7{eSdV&zX;=#K*_G z|NFBnZdU#&#n;z2OCx4>r0Pl@je)7DsewUyLBXiKEpw_GkAm;w!+ZDGtDDDM5)u=? zfB)X>Ssk>o{;j{L%zgTL-{-0;zpqC}-_@}(Hy>%wID%q2dT@54>6WmtFgc7hd>1qG z`!v-(J@M4I!U5)EzJvEYzxJdabPqS>pvG4uiv~`?KY)V)x>mwbS)%AnE85gGRtHIG(ktiDX9Wdmckh0gogJtN3R*O$tCw>cXgDv4K63h*>G7= zyuTB&l~GGu`--2R6f0e&%i7AqKK%#QMg6Fv2QrsFV&QMZ#GDn~9e5sF#mML*DvD!a zjZ-zyzwfzaV`JmWXB3{8nAlM^-NC9W-Wi7U*RNmC(!9k$Pw!+vSY2IZU|=w-@T&4( zeUZRinN65E6S%&%ipw1u!j)aDROca82LAhldn_j>=hNI6&4Mp}y1@J?mL@K)5QSYG z@E3RaNls3rgbP8Dm9F@};7g|yhK4)#bE4H{>!n7v7TLC;Y)%L{FdcTa-2AE;>wl3Teva{ z3iPu(og5ueP3!^Y1U02F#;iY!Q`;%+oJ7VQ5YXDce?Q(Rsx>y26x#mGtm2%k0YOoY zwXc2mw#`&>DXlg|ON)yt0e>E5W-g95MvIxeB<(6S{c(+EyOgxFud=y~jbKRa^Wx&Q zrD?`$?>9Ht7`c__fBpKrwA#_pVPj*X&L~8lX-iWz-oK2yL3Nc!SUC5^i^b8PjYi2E z{$4aRVT+4yDru_jpWnyE#@>~-9j^%7Sa))Ew!qSJbAOm#@>PD2nmW8N(d7MODC%@h ze!e=fu>TwG-M`hPt!Z{t63U*lU2o&~(ThKJc49APWle-_XHno}2z;aD{o~BJbJtha zupuVe^aBIer&^NHZ$v~yw3S2Oy?ck1d80f>9Im<|FD`!heIEA~2cIqbJ0k$3~;2rEL&Yp{+wv`R~}&z7TnEYD>3Hik$9@bAz>1Qw&Gbn9JL4 zEYFwD?75+`ovf^-J_}Z%27Z2u)p;lux-B6?Q)(|SG%@UsKI-_)yo#N7a(Y@*yrZ{w zpPZSgDT!?R;eM?o-5!Z6e}6gopGIplGGdBvHm&wQXzup!k7sCXV&bmV8;pBY)QNZR z-d$Mr&y+lPkd5~z))O#*gCm;cx3=P1rT+mN>q$+xtgNhU>%GLp#IWAg>9nBY;^JAS zC*?CUc6jGQhpJw@c%i`=IQe!57gy?sRHcp8rR*IM)Pw z5ZtqAf?<&J$dMxl50;(C);nozoPV}RLraS?Jjn5lKSpbUMymsAf;OHNhN0#>fJ8?$^tbWH6oX?j|-aGrAwD;0{_*0 zbtyAlQ(d`HMy|iP@a7hmk_NX0HvMnIER9rUUpF*GR6<;Q{LV+or;i`s##?OL#%*S1 z7D|e!H#9c(@bJk0{o1c8@Sh+0kcNiFE$%~4CcZogdG+cQ076O0atwz&8dAs4SOS6R zF_u6-O{KrT9}Q&UO-zb{Yi21wRXX+6gjcN@onLU%KYaK=L+HWy=;*@!$PLw%C3AOz&z#wqgka(X-Ifh4$}Pyz=YYO)jO`2M?3{gHlM9 z@>I^8bLDSaTV)R%C@d`O=%&2B_dHE$*J6_$Q9M=E-r3pN*tl0kWgV05LetHQlB}NU z#~5(E2hg3CPwnPb283Y=D%d)SJ@zuMPGNl$SX40vUd|CmXFC zbpf3oP_po>kr7!%Kw4UQ|9;wf$GJ_rx_$fh{j7N~FkIoi^LNDFefu_67P4F!V`|U8zbn19 z#+@zADsx#@aWvr9xBfRV98o4)c98WSD4%^{9hIN|Ep*$?b38@_wF-HcPp?g0T|WG7 zd+-@G31<_{td0+9s?Ls%l|BpOEy)rSVW~>q>wlIVP*I|yqPK6~9;pt9Ch>i27dibb z|6B=>U{hb;3a$cG%&z1mdcaOpOf05;%+A3fLbKi?I`40|Vd3LVkx{&;V1GRI+{QA$WDg6BW_(>(3_pG6$ zlva$(sijqJZtj)f3k0>TSM{|*ZvEvG5fx?LpJ9|se~+|<(0+qw%E=wy597Ld@4&!7 zvbY)T_U-&yOiWBv)YS4$0~j}dhlYkwf-Udfjh_`J+cjR_g>P+9>SAeSm6@42j@ukg z&!NFNn0H#@;K75wzN?|3o4Id!f9-j?ZKq&t(<0EP%-+3w4<0;=4QgWY7o!24xM2hn zR@|c6|HJ)*X(|C&;MtC>t-gh$fcv`WvikbBID>xw7)Gb``}OVYzuIxL4#4Ua8!(iz zQR7IU5PVnsD;*u(tPj=Gr%$6?>oNU(g1kHd7f?3%Z=eLwAAz|##%Z@Qb>~;HdZ~(G z5fNKyXr5Zstb;zSMA6aHXJuwaHp&tRTSpDcx zG~g09G!a^jd$GGbHlrsD71`=9|JYi12R9FxAZTrcS41S5#IEQu^R>6YA@AGlEITKs zsNaNyc_n6M{`Xx!=H@J#2v&6UU0q%FCObUPvR2+Z5D2erisv3F;wfvT!8z^ zOfT!`+-SE}@=#1|n)p8W>X!f6ZG?8k^kkOER*XDU>qf6d(AcUtd8Q$1XSZ`Bzl=vx zIPZ~g)0)84J8HVbLXusK5VMw@ot=~S@-kY?;W$vkI~d4dg#rHlH?hev4nj#GwULpL zq<7vs$er2xs@^|LQS%Ku1PNSRKNr0eoE|27kJW|k)4QK;Ak@>-gATgOS&~3#_W%;Z z++YpAWM}ubxw-ocs1f1wfz4{7>B+C^*;E57F@j37)BsfQ%P!vDEEg5c2?V>pidZ~K zxqnrw-X2`4Ch>oGjpYC5Pyf#t&VN5;w>2CS(s$81gUo%a_mOro?&|4+4eW+Z)LYuW z{KSG(2K-H`)aD_fg$Rj=-O%qNdl?PQ-)7qxB zkN+D@esS~W2Oz|jmX_wkRsZ#jBN1KQ-CqOB_lt;t@1Q0AL2a_JvQAD-9SZo@v*3i05#PRjuk1SbLfb)$9;gJ^+uF(l)a9Kn=owTYd}pK(x?*398*8b%moHzQ zB+JXohk_zRybZ$uMjyrZfYwkanO|8s1l-xvlav!&a3pZHOE|M(WQ3cSnM4Y9K6UaW z5XQfMzem->P#?#RAE)1=Qu(H@)bv?cSlEjfZdzKjhAWFx+FL?09zIM+N(#5^L*?us zKX~w9k{VwL6l9^`g7QH5aGCp7US=*XGiM4X;0ygVH>#gN+>W$76h%uDRML4|ja z(G_1^9?mq%C1*eG`~qUTxUf(we^=)-P|Iv!(P*jb5%f!}^L7?-bt2~ZqwMVX6yQ7{ z1)pc4o9iW=o#z0K@r|(7q$vOpz!;#e`ebXc-kpF>ho5??5wlWu~65c7o#6N(JDVBn()pc^nfp;|(Fwc0z8RZND~ z_wT{++OZIii;KXD1>?&ATt3t03k&Mqhl((XeJ%GW4h`ICxV)r{OqI@mL6Q}1*+a4F zXxm4$Ou3XQo7Q`vBUlskNytc@b~0{U;>jsVNpct8F=t%J?JECahVMajHTeGQ$+k~t zMNd&+-JS{{%=Gkhyf^6ijxcOJbk_OBMLvnc%IGNC$>M;>$~L=pC4$2E`1qjlMc!Tn zEg7lsZW|r7NU&gGXJ;p7UiF(tVuA>9PvnUp zxYADcW4%Q510BK6(b1Z-_&)W15LB@JN=+|oXIC2U_niKadg8H;@6VZzQr8J|WmUJy zw*cL4sh~IjN6-}HbEeS0r>CJ?sn*<7#M@EmG;|ZE(P}QK3!@?Dpvhnv$VBYbD5lAvjeq;Z#136}Lua^uqG|w)>Mm$J z+A2hvs)&Sygil!#p@ByUJZwBuG)#LB9SV4tAe(vW%G~;PEMa5-Ol?ccFM~(Fe}nDX7!l+lx(bg(58SQxRnL(T2USZF_(P zr1S&2E%p8VXLG1VI&b{j*csTJ16dHwR$e~2ZF-N2e<|i8kYslripd$wl=2#77y0?E z-yPyjR8$nWOLSC}@9N^-5eS2Jr(RT6DqEdCa>##0Qd+vRzWzwVAMoFvrzYUPyu6m^ zv>=1udkgl&H$#mAPSn#QYUW->7`3wG}MV|9MJhU?xPkS6RRadT^Kkh^TwYa6=g3HPWhG}?LHcEf-e)lw&iW$e_vq-iS%iD`t)y}a{#2F29}n*WV_eFn`GF(V|Gl^g6hQI zz73wAC;xI5d$|MM8M9O7ry4(h7CIvfGqb2g_5GG9Y$&D*O!l8YfAanJ0#?NLD2L(~ z*+;Nrq0}4XzQd>nu#^!{Bxp|GzI_8ugP5D%$wFv;?ldR|8NjkGbhg6O%&fbs3r%(O zXAv_q^9^>{S&%OB>H%Aq*%T^S#<&bn2A(OUJWhavhL7gd~dSm6at8kTCO;CcR2pPVQMw4o^}N zPnLlYV7|D7gtOJ<-_RrR$w7aAi76}lQ`7;@#mVNKp|MY%?61Cb;R04?5quz%48;zM z#j)c(x&U+`u=xJS3)_6kp;iwiXb|jbfT>3@3HGQ4z8`b3AJTq6cyU;PQ!l1d|GJr> zq2UqRkBg>M{J_TH*fy@{p)b)QpO)a2Wqw3QLb!s?aa#D&S*d@5!I+^ zsH16Z`%Xv3#60S-10g{5A0+|=82@~edjCE|Fd<_Fzn^yCPd3D=!p&4yF^rELBZgao zen5Tg{yJ23g@rfa!2@$meykRjx?a)|{Rh2Nj75^b)v&rWy@L#eRLn04ji}&U$rHb_ z>(}35_lME;JN|;?x}bkdFdhP@);e82v{lqU1q4r{@jhALKqKE%Kw@M2^6i^Wb!#*^ zqqMZt|KFd)A%^Nu3G=HOS*>>tgsgp`s4OQ{X;SqHP%83^-v{N{V}9ZO$EEG3tC!@y z^~!JAe$nFszs8L_O5T@&^$el8tQS6iZf8(LD7KJ1e)ftdt{RhD=dRSny3lPI85x^4 zZQ8YK*Wts58zfe)U87P-vp981&?*GruA%j9HMeeTWK<5~iuwha(YV3Nn^30( zxbXOKk86~woZK(e2ssEB0*yj)yz zTv>b#i)Vi>FNb~X92nRmYj0fD{l2x;K{VSam+!-f!M_7G*4Cu7zHY#gv5k(8AM2W= zyWs4wf99v=+WN5a`$L9TE zQxkayhp*@^r!BOqA2qkMK`9oQ(Af^Pq5W36PGe}Xq@t{Gdz@ceP&vAZC$>k zl%)JM;O9(Sq;AKTpO67ULqpfr|0zjI8t{<(QC_e=*s^vFLnFj$%RBZNOpH)qnVfIz zjEeutJnn*^9L4tv_S*br@#X0^Z{7f$fz3a4#U;1{PP}sL=aZ1QC9xt-yE|GI_8q#y zzP)>eg@j}*p3|piWi3JcMa!72$T7ADY2f<(>OW(+i)b|BmH*P83^(B;2m1QnwzP-{33bUiHDaH=?JS}gTx#Qo--$FgC{c%; zvAc9BH!m;y;X`9%W1TdWsyB-0s16Nnl=VOp06~rVh~^DE3C+hDy8>QFgHf8bc9JOC zxbwe0%FpqLyZmL3RjaP97TxUtBQdk!)qi(>+m@z#?Yx%mYzs(Ux<$9O{FSfun#H3MKMn{#}Thg*Sf;O}7mip&%i;)6MA+_fvS;ucumRpUPuD*eb+Rfd)0_I9-7J(om4LJ%0f^TtM ziPZiA0_W%N->c5oQYog2|2Q*{1dajm7hnbdOi&v<=Ig}HzW@JZ0X#iC0CgL57U9=Yitpp`prE{vWTC#LfOpG%+rG zSfD!>QB@TX7+77JWf%QeHzg@0MdXJNfj}&YOJJE}ZSUwPeD#XOTa<}{?I|6+s2r?U zL%DfHz8~anff_bb4CzHt(ax4#I_PMju|V;G8)vPpCqXqh7GCt1y7Dg=W7G15qDzfU z^;XWG%*q&K#9c5U#3%e$t^YT!uhaN1*>;M7-RJlV<9)srl-?7I>Z-fCvht1M772`0 z+*-IOYXgjudtx8OobZE4z(0WgvW=ep-_I{Q$&oEne4%}gUPT&|z(=s$W+U_Ca$s@^ z3Cz$U{NtOg%S%gTig%!9I5;?f-2K_aoSd9MR$O%zobBxu9s4Aysj2rTUMHcSeIF=s z;2&^xcmE638OVuDte4ypVtMIO|NBBpg{8L!C!D^GTEI)DaL4{cibZM@mHh=f2x{WJ zkq_tzDI*leoG`GRwME3xM|tF3qDb5QgD1|M=}YjsJW^Q(qyN?()t6z7tCSxj+#?#9 zM;VgF`I1j$~kq>Amt)w&vQRA&&RaF%ur$Rdh42XMqS(zoP-1*nR z^3Eg0D--|i|F87}1l*I*c67f+uE>YSwY3Lpf>c#5P^ap>Dk`dPQkKbyLlbOoZ^t{J zIq-)!6ze*$!}>lR;BgH%99boQ2?==N?~-LJdHpxndlL z%M%-|a&vO-+_?k1@YJ-7i8s`nabg!lTUI&^P%V>RwH^ZhNl;H=6b)S^@W_dai=R4m ziqcksEm_gYZtG}j+D^Sogkm!pXn_r3H6UdudG|fU><6kr+;hLVWb8;PpAQ=vE}(OZ zu_#|9XMha6YofetVLQN(2ma)ezszZ92`)?I<-T>=TM3vBybiKNaY;#)&q5{!BAj9& zX4>lB_I3kZU3&eml%?XaJpX1(bA;s?D7=9e_wWkv^M~kv=r+J+z+l0PlOckf@%)S^ z9Iwv|FKsY4|Ni<;%m6#!3q`C&b0m5?7i@F+x|FD>W zwho1a1Z_Yx(6B8p1($LfD9(+sF1Kc;Jt_pB2WG|&USm^JQ9mVgQ1V{P6nJRlBV4!; zxv8nC1_`GvAy*5oF%ayEc%~AC01@Hw!>x9nkTScLnAnYbEU0_0Yqyk{U_7D`CYoV~ zuKe=&+SA(8^HMlwGYt)8Dd8)`++ZTKTVnoO;u=ur0xS$%ie1p05sGllOQ$BBW5=!w z(|ROs@-qL+ckCFZG?siZpS6|3$B%8ec9D-9fr*aQp3Rx~^5x*Aj}I01!%VBk$Dnln z_wMLBlb=8PUAc0L^#I?YL;oh5<8dcgh@0_|gGIKt(DaWTJ9a`5Urub(Nc?RHS=nx+ zbhNdj^|H~@18}o|d7SL{S;IjT_<4B`o0(Msli@Z4;p>*g=y*^(XJKJiFE4Hdm-8{y zjDEj=m^t$U5N~DVE-orck+dYo?9_#0cwK6mnvfo{ok)E9?j5+>A-74QxfJ|AFt_A{ zgolqGUmhx50N_DwJe7{%C#ZQyN))JD7WGpCMrB0A9xhp1gEc(D1wg7EbAhRZ*=pjg z1R#M4%UBp?*$2-HxfKv5Y&vL*fD%Ce$X#h4Vq)5#$j-`Y2buN!`R88W0eH^jkBPj7 zj|&e6x+{D?*dK7D%Rp}TX3`Ljy(}$Nz#hRjA??LQfHT8Lw#Cf-@ZeB|WB&^%=6I6> zTecB$7*JZkRM3#q9z3||EGQuG^J_0-{d34*Xc>PoC_oKQo<8jj6SpE2w;MP9YTtcH z%Q|!tWFUS*2g0gC7PW7^CkuKs^vc;{yULx7g@pxdB}!fkbT{trv$I8y50_!|1>i=e z-M?Rn@c{F7ysQ9VeX^Pmyai4%6yDwJuhJoACwv>OI1R>tmNFj6Krj$F=DW8a@Z*1m zOIg`L^E#_SM<*u^83{D(T3id@HIz3kX5a|E9lMns#61)PpsFLJwkPiI78;tnDJfsS zew7#c4bQs*p}4reSQ+wy8*XaYJ$LPEGhDAem`dleV1$)A`k-F_&pj_5`vVU3)VL}grpEsf0WE%7Xg)U?e|;g zumvw(80qWJp#)$aMKEyjwV5KwU^2IB4YLG&13d#-Ca)huY`n$z8qnnwQ*WMr&7zEU zW}(G#l$i=K}@@y3}Bs;(|7h{ zOOxm451+_l?#CR#M(irpNp8o0K;MhuKEy6%Mwvj=2zVBIB2j4X$B$s!U0H`yorC|O zk}I2qCG3&`s^({|e69FspW6XRT~5v@^%2^VjiA!EV-+zs|qT?G%Fx)%OsnA-7$&e3aFKw(cUz_y z*kvN`&+&!g``VzX3>E&zP1hj5eh+~=2T2C<&dv2#Z%z3Pa=WlWDQz6xd+**yXQ}@S z$PUs_bEs8xK(hfOL9M;*>Pj$02x|u!8h(QpNKX&qcOd8)i-=Uh+qBuXi*mJ2v=1xO z4`p=Nd3kw}!(ibbCISUSjeG#_3DZJo7&);AhyJ^qy4SC_wD)v;@qkFHxPN7J)e|@f zNi0nLEn6UIKxxl*J^UXr5DPIkZ|?~U51;+|wbu};#dg?zo}<+-;ZcAHcKwI?AeyjC zo>$|~`tJ*HY(b@ceZoj4SzW$t?#z!yikGJ`Qp|Wcow24|t=+!!=PB2M$oT(Ha+Vr- z_4ElXEvC#AC;NhE@q9Ft`Xq`H>dqW?t?ea2llU<7> z9Wp;hh_%*AYU&jGT1_+i>C<8CGFQz_YSiah1%Hs#l&C2ehu8oA5PNA-RX_}?bZsTB z+1e<#3%QAsnKIm+BsHFul?DEgS~VgF0S7B<>~9#{pP~J$Uavjx0ep~1b{zCk=1ysS zrjknruhY`HZjhnI1Dar9Xn5)A48d*`k_q%bA?EAI^N8g=GD1uggMf;P3gjUy`38y* zJQJ}8=L~5o!oXwj7tymX1cZg7gn)ZsJ0Cot8if9W5S_j9FKkxHcmnxtW8)Nx8!Jnm z!bTj1MYH?|GXgnV$HaDgC<827k1roTeL8>o^l8^7s&l8bA623aQ>Ze4t=r@=>}uqC z-0>YSLg1*iynS2TetZ+bsr{a;14f7(_6)>jm9)99U*TVO1NL3CwS~hNYJhuyd$CHQ ztEXttVq#B`J$jro7>k~Q@4dey+|>>e45rCmxdU{A*j2zb$fNaK-$d1J4n%wjIs=gx zf;Q#COmSfpjN4yCarG7oWVUaQ#(abm{M4+X2UcO29xcHx1i~OXpeiki45<|UIyy2! zS7(qIB_E?0OBQMV#~=tKOIQH9ju}m08**}T<{0l+5mkqc^BzgZs8;9;sz`5PL%1Az zD)z~(%wJ{Co`HQo&ICh-K5by|+<8<5t}TAIZZwDAF1N-N#(^5or=?Z(ZE0=U9d-~K z#XK}1&LFBB<aww^Sgu6|}x;i<}%ZG_>aHF)?WQNIa-`%ouD>-9czKSN2_mnT>a1|NW-15mwqf z8bkY>6cV%y5%}yoKuO(ZOOUsb(ST0?$NK3`j+|$inRVUW%RtMhDcBi^Fe06SZcWL{ zK$93A9)|H{c)|IQT>!?No{mleC{#zs4sX%)#A4%jUneG(@RM-GbMo@Qk5u{isOq1i zNdt?Ow6kp@e7~l~(+3&{lm$`U@B)RmAmCL|R#xJ6-YNFQ3n`UfStAL*Ve-NU#XBHC zVQ63gQyS>@bY25-y$xpS#P~P@JxJW3s5BCmR{i(w%f@F8Rr*3hW(`j*rhzO`KbMZo z69gs7mF;_n;=)#e#oKyb61y-zUzBV&-VnvfkRA4chzt#Kr#IN7p#=g4+H+?A#a6^{ zNEA6uwmlO^>F)^X7rIneL z)wqk7z-6kvyg1O`53&M0h*Jr>Bo3nXDt(vM|9maLu9qpKgt;4)8;{QDNxA){NbLYsS<~;t#KpZlJp8f4psCt_ zZ5W1P6Ws^{Byd8nqZV>6Qa-T2euJmAj;sKtA&`KThWG%914>ks8E`Ka7PJ`gaV4cI zc&C!q_eih1LKRh1ArBU$rz3eS4Bj=cpOu3H(e#X0))lc3q*z(sT1%k=*wDPGO?;zeISKc+NtDIgwvZ-`%^25`@jqDB0Fxd>?&^aW7BlX<5~nrycfQ=e&> z`S_>>MIJ^AjW_xYTXP%%WN2dtT)TpK0KT{R0o+D%`Kvy zmj%a5s_|hByqM_Y3qG=2f=5|kvb{nBXlURGg>7sxN6m}iondYlpbW*Bf1?_kmnWhU z)QL9GycM@pQ&Te_ARtp6Q9=Loj0{UBS1)TAp0FJON7T2iR-DC}pzT29$&^=c=n-A) z)XVCCGq3sW`)G}-Z^45Hln|o2Gx8*7c}S$a_&+vMchw!>ZlMzRe$jf0@`l3dMl>&q z768*j@gLgTWD#OtaUluu@i@tn!~Gt&aoz;z`Cdp!h|t1BEBNS^(f4oP&h!?fUAsnx zgys|h2LM_Zu-o;Ife{EJaHDbV5J4Y-^b}G7jS zyrHn~dJ{C%D%y>xIS{o76|@xst`!x1|NgzBRuRl*9Q;uAna9x$-iZ2x=-|kl6(%=L z@P;D$(l+J-qZ>!X5LtlY2v7J(7j#3?JuN=SD7Celcz;4IV2CI}=H<>ER&Q@_H?ARY zKDbw*B&I(z`zQ{9p5i_r3WxRquiEFyQQ3j>&@#D`pn9%dg z484zWybs+E7RznhjNa~Ul;os~0*Dl#JitYMR!v4d4troCPO4&NjH3_ZZ7`fMAhNQu zK#+v29D@EW_aeRYSm!SEus35ah@4d6GGVUoOl!nu#349=b$hsGV*B^sleu(TXent3 zZv>7AQJ1e;451?r1XUsCc6AvuL@YtrgC>Z4)6R&Q*;z&0?}GcNWLsNXpoAS+QJbj_ zBg&fGvX^0O!3X2uMszef-Gqw*#x>Q|*B?HJL!;z;4{nEqL2gDy-2MB?Rt=G$r6le5 zkwOO!xWdDCl=};{6s@4-B;30u_iI~Z8Qx0LY2zSKON+xBIcMlcNa7)(i39wpRcYg-%3pWIuPeMq&;(*k_yYHK5HAsFPv8w&Uc zc80!Q9r*8^RUr-q7zK>&BHVitW_0Pwl`Em`*LR7`x5B!?;D+afUN)O}^xG72+-!+j zo^EbWo>EoYd_g;pWU3Tl=Jh}xq;e9Vh?s%j$-Ig8MhE~SA?h}O8-FM)kK>n>4;(N= z;1W}Yn1LJF%Nh(JwD?lzY#_7s$jAurIs!eDb&xz!rNnM<&^L39IL^Wv{%`%C2?YP< z=BObWJD@9s1sum$FURQnXohdNA`2bKL{A?>&ml)j0Skf(jlr`l=Sj)a#ugPxuNLq$ zVA#HUcfAkh4nqGNA+DH*Hv2zU6lP_3;CPy=T*CBCz0;{>WMW2_xBRRxPB$1n>qS>Ql9 zWbuJtY^|;DD|*_)5Q@3%j_3_0p3ehbKGedGp@Uc}e`doNfc3}vY0vWVv=iRnKj;p( zNp$!2QD)+1U{v%bIQ@Zvfh|d5d4;L)58-mG{jxU<4GTk7mlEjMH5FoX_%N@KkR9|p zsO{0a#bA$+iGT$8H?L-J$T<=j7`(i_k$~Mlv`wHKR>HWdS*V|4UcIUdwt~o?c~d}5 zC8dhs;9wkk8>bNjZbg6OzJ+|0dhxfhvADZ;**H0e(p9J65>U>wR0|2Kqd1^*fVJ*Vh3swA0yPCno=%4(z5r~G0!OY>Z0}x( zut7Lf0J)b$YBxRbDJxhG#}I)N#(8nD0E>rZg9z~Z*Dt|$3m=A&rP)~SwSZPZ4!HV# z7^)cr)n`whoYPFjI6NA?yX4$4-C|HRY%0na@$?Tj?gavZN(jXrKfAI1Z<50Xn;lti ze@GqvpuN(Fv+dun#X~9{(h0+yZj`t`p3IMfQGdR4?gQlT_WnJfa{us^UL#?NL@jzo z#xzL5XuQCJz+pHlptEkXw_d^pw-eJdt_7&|&VO?D4l;HzGO*YSo$c**1_GFuMBc?P zR$iQ6L{K5;`HL5~TWxlVVe|#TgvSYvwVz*3IDH4qO4fSBRMhqjNCvuP$?(hxV`CY3 zh}zmM157isaF>f9KOmqn@1yMhIHkPT3PN9**Y|sO@18w#hIQl%gL+0@o`bDz#Kw;T zj&<2jpFWXx?-umy!AuxHjezEsRaEqz8AFH{nKo3{8;RZgp^|j4AcxV2?5}L?&Oqzg zGk+p$eB5fWod-EU;j(!sDF`5-XnC}77G`c@q6ncAP;+@M)Q2=&H3{ zZUU%Fh5>-eJ}q74Jx_CTUU!}#BgfOx`7+?I7c_Q6>6{&)<}?^O$_;=Yuh1AQJLUImKe*A=6SZVvplsK!OTTQJ+5j4vqkC1%%PQ!Z5WR2^!2t zyh-mJN)lPzys{5(-`$-k7Mp|9Wj1TNh!LN&lfaK26RCMU9o_^LSX zO!<b8yhDbNjfsgYK3d|y;w$Hg3vpA~ z0Z9}QtF$!dixm1!#P8aX3&t0Q3tGEBX$XM?|Crw}thD8~ch z5$#53cjlg%CvH>0LKbKJb0`+gZ{H%kb;H!$$mkl0Z*=!vYzS1`yLVb=&-TL;`xFN1 z2{nqZ`j`vsGjP>>F(#C>m2|$U& z){{o5$mro#xG4C%i`LhVvUmrH8p9!fGRBDqCo+^UClIN_2YvhgeQ?<5+Da3{!JWFg zx;T5RYi>T0c{DmQf)bAcPR*cs|5z_|f7iVN%1fef6@QJ^94E0DJ8hy)9CJRfW0W72 z^JKl#4DDN#-H(^O80JJJhQ+>TC9;OgY)o%anthIPx0>olDZsVApRLBCwX5qVdIFJG zdAsSW%UlmW(Qdwm{X)kfN4HURCI!cUt>~!7fYu;yXKoGp@^LQ?0NlA#3Wwxp&E#KT zXB|OzD1DRnP@2FizkdCSxpP-aKtN#s{&x_7+S(A>xm41crj_*If%s2Ds@cS}G>eJW z&$5axpMt?e!^py-qR3m9*VJI}Rek-zzKZ;r@4)6dpf6mBKRnqCaT)(`$~OMAZ7bOh z{vnPjsSyoxjmKlM0HVObk!c=F0Xc*k3gO9fP0^~TpPYPZz7GA8l)}ldH7F}C5plnu zUSjPPKW<8NZLGfBp+^MSz#ktz^Z?rRv`1GmUBEa7g*~RDV=c)Fl?pLG$Rtnx9-(W? zj=Hpr(45t!OHqtXlmrUsGH#?A5&aqkYF_T~1q<2m?l>G-znt&I6%`zBEctf{3J;*plOWN@4)r2peFxL{W5<_Eo zetrh$e4jpba#}`NA#We8@B095G#eY@f=VFG{t~Hrs<0t3-u7PVxC&vEfkW;_s|}hW zmx2qy;a=~ej$rlV$(@=ZXq~5PE@)tQ&uHlm+*b#A9nOehU^J_mYQlcD?-If(c$^f$p+lS;o1UxL*nQzXUtRWHrJ(i7 zh={H0M;@^S`6Jrn_2nbYhH9#wx0e?NGyiEo!c)e^%&yn>9rx3|w!8btCc|cr0^%0c zNN>SI@t5k_Qifp;IDpu6sKsthi0ObDz=TS8*vde-{Mz5S8ti6^68n=OilVhpGjQS% zM}IlI6;u}7okaB9%Z31A(G)YZcT&_Jj5J2Zg6|v`VSiyWvq{^~sT2vDmUK)>mJD0Y`3h1dF)nXP1|jD!~0AcW zNF9Xh3U(!0ep@FSh%^p0A1A6V@Pg6S`)l)3ONsU zzw(>m^<&5^Oh5)WdUWGf)e?il{x9q+0=&FG+w+)fuhB3gmlV5e`!)LQv$V7OtxSeX zcwd%mkEAhdxK`?L#8)h`@5~P7!gS(wK3XB3*!JVWpE+$qK((_U!-4^(JcD!e_l349Km$UnS;s-F_Y`HKZpsY@yowzArH0lo6rKUD z!HKk7%uHlV@ca#N@ta6VB*ovoTZWOUy1sHm@0`^C?xoHhNy~=F9j4-^Po7NF$f9KU z5(`2>XZ7w#)&gU7b$MX#gHo;|teV!=L~Tt)(9zWe!B~Z`wLH^7EpFtlz&ZZ-WbPxn z`{<2#L^u|*CZ?x;)~0{5wOQS5JDi=z_FM7l6k-O$DW|Iv2WjQ?BT}q3GhH4G-?akb_5q5KK>!V zMA5-8509EQo;rOR+Zwi9<6b-%gCcX_xh?tmlS`{OFF7jf(ZTrcxn=#Sr{|8|zm5DF z)&%|MVo^V)E=a0R=4V`r#_zkE2#8&#wb>S)0VG&nUZ%QAP)UM21EHd>8@A>d(NlPW zM@nlr@&-4#`1O{yZ@vbk4Ib_9$uWaX3jT2xS*sH#z~a?13}AOeEwEv)h;Rg6CB|r& z6k3I0hKhG|ck3QI#tJ@o@#5b2=KP!-0|NsEwAqA(DzH?kyL3APzJB@A-`5AYsT1h1 z!)yy(JtTa*aP%88zOOQOM7^u0XH0Z-{czk?I@a^{_UCXChbaoR%0?HC6HGrOk-EX% zvzF(9oUai$9=>NgYiWA!E^f}%0}e_Yz#rJXHic)9(!x1ab-ufI?tEhCCj_4@vI*!G z8Sd%XO)xMrqFU!5Za}n$GsOK&jW6z0J~kb*v7sRzu7)HP+?=Vc4>WnIHyPOyzmJWN zFXE6NY#V2D-5-ySMKScruG_qt=xRx z9h%h7pB^|xs%+bLOAk3-BqH~1+tVQkrJ#0dh%HDv-wg1E=WjM3m|u+cX5>Z_m^ z&^ItxybfJqxjJ(>AvqZW-dk74U>Q{cAJtVmlYx;*1R9SS8+YLn*$+_^kGiGaboFHZHOI5|1Fv*bv#JpoA=X99QXsee^JO#&J9RTBoZa%<#7@S z69GBA+XJs7Ec-BIF`O|20HgIDDC02K5Z9jw8#nY=5GKfkZyzAT1>LiVqIPCl;b}l% zis%~0e-4w28F6wL$RK{BogDPg%Ylikf-D*>J-w{7G!P>mJEU{`_+iU$6>?6MoUe8NQ`x+WTXTC5L4Wi+r8W9nH(fYQ%X16wTmX?f`XC~<JBR0Mkm2UH5c;~F+1nMUd^CpWkF?r0#yVau2w zoH!g`H!`x06SdIbW5TIWYNM~5(?UY1#nB>4ii;`xP)TklqHR!dG0`whYX1EigjX5& zeXtoP9kxT%vzn2$-h$0c9|oMx%6UWY4!TNUCsx`*aY5bZDRNb%#inBBW~ z!|8h4*2c)daM_qwKwxwIbD$8Wup{AgWe82$M&1rr;~9v5`}UY(DucTRO0u|E1}4bi zkS&zVBuWD4(FbZ*@Fr@+4iw8DalOYAZQxo_ax!b$U>y*#gCY}Nn$e+%ZEaadq}cd) zYNt_vJe*&^GYY^z=OLpYYM(V28QTPp5GkYc8oSh!AErv}K^7TLAh>gewP|F;oG*?i z9*yM`7V6t*O%28EeBy%V+oh)mTlOLC10R4vv+k+B{Q!L~o@gg+%WzlcvdpwtX25Hw zQCuGS8E%!}-o3haBoE{4!Q|UGfIhBaHFbVvc>1EE`3QOlY5K7|PSqI!Oga1YC8&3R zy(SMSH58kjfKhvK}U9&mvpI-;}-E(859$VRN7Y z?3~Ql$ka`^b2xDe86AzL6rUq`LjHY4^o%h+*&lU^73cjNdc@<JrL zUpcO;+lX@>3uwnXckX;dTLu*$TYcfAw7$MR79L$Os{^(H@x)$!9%w=cyDBVff>^sX zRWG~q3$OxA1l_QN%hzw*pj*qugF%sG5cu=RV2tZiq%X$+R zYb8Y$6nwxdA&uejRN~yJ3H4(bMPSF2H-&@)%@JF54(Mzz({XR2Ro^MKE0kw=A^EV* zz3>ph?Xk^wC>=F3^Xk=FK&^+|iMYSGbtiI-*<)<%?fvlVvhjX&*JEWSk?MQ^V9#bl z;*h2wkP}5fEeUGG9j2{&TYevBUQhfPQchQ0}>E#foJ)+H(5CE}aCu!}b91sL5 zOv3Ur#=nZ;-PP2FIc)Uw9P;o_0@Am&%bBYA7?0U+PKVD7b8f9W6iue+;IS#2ge>y0 zOGhvO@GUYSqqaNopK!rgz8sI&dPPcZY+(YLs(U()fYU{VhhPCxXH>o$xMZ4xOpwb< zX77T4b}P-w)^-K_8{!(l755M!@a4;wOZMS>WyvW_Oa#Y?WX{r|lM-nIR4Dw8tD|(} z4`dLuiVPp_IqSu3$O^*+5E9Ci!(;o#A|c6+E`=|J4g%voX+eGLA`nGFZeuqQ?hu5Q z@Nt}9L15IbF-P*KCc_Y9DYtW(9xwEoJV6!<6LNAvi?>u4)d&M~Ek#iTwQx#{x1QYA zXj!yy;TA(C-QU(L znB%((%KNF>j~}fQrvcr(NYeKOqxAlT;9Sk0-V7=ou}87Y&`b#~ueEjfl#?6%{LVUF ze^OEsXQc@**^%ZdnI!0UZ6tg^Aq(^%JXjDQ{Yv#f{U9Mwibb8X)#=WRRzdJ0Z@<4X zSu;B8F2Vnu5HUfmrOe{Cc3+rw^a^x-ZJ6+q7#rJ6^#pEt@d&p_qt^ zlf`Y!2w0lIX4<1wnSW1UXvvV#aRk!|USLikA^ABY$)X|=Nm=&&hSnwR3z&`2Ayq6L zd$ zA1!O}zkF-Q=)$TVJMO@bykr2?&Q_&2%+-#97s4e!UNjalZl0byKPyaL_@>lz=fx+l z(jXYcUi4XAKB-551l4VFFEv;>e6>u|Rq6u8+#*q7tso$9t-EZl9=Lu6*wFD$*}r#= z{%*bURz51vHEy-*2A4dn@}zM7H$^D>C-WH5qN^a|)nRDt3ba} zEN_ zO|~m#cECst6#v}?$$8?51A!HUb+x5CZ5iju34t>+y|&96#A#TG{|I&^Aw;T~l_tGk zJG&|#2)E6;RbAA66~A~v zqx9tC50UYcxiC5T#|ihy{7!$<#9V4rO%>5z&~tyRKG3M%C~tD<&}Fr*%A}L)L~@_b zwmsXKBq46OUlcuJ{ct&|!+|>|^s@=m9v0%F8GVl?q;WU>QQg}=4~Au4iO-MUy&FQ+ z5)5VV<8{y6Rk{G%3xtS~;76^SWL$7jDj)XwG2Wp|Z z6a={(L14Z)V~Rz)AdFi2M9SbYK?M@4J5RiNN>@8C$%u+Egjpln=~{(DPNlGoj_zuD z7U3pU-tOH`2t-s070*15n)g6|2PmY|>>u@1#=qoKPF^~6ddD%*) z`O8m0QHXY`<`!7Lp#xD&urVlQ$H#B%l*MKr~Wv z%IcrfwLctgB^NT~7pV(qOVjl9^+m-X1=HU@ofXt$@_BdO`sxt3@OgXu(I+W|+6eAL zUKhTr^T;I}`7UkK71saxv5d~!;Cwr3{WaKV=F^i02{l# zCWP6E=c`M{#~Zxvj~M_m3L}4+=Gj6Pk{7^QNr52|O_Y=XLKfs#acS{``Q1zGr~nZ=Eh7V;A{#4^m8mTC>Mml#6*pY4^XAyjvV_@Q8)Vnh zny*L{wbE|e9-k4vq1smB+F5G@i*bOYF4>a%1G|3!n`5B7ELn7Lj|G6YICT&L zA5_ESJ#@zvB7$Y;u?e{Gg_sNptNHx7i?W_z=HuM-Hid*p6Z~X*8K0wHzs90$W^q8=_XMDv z58Qf$5aU%mx)|m~QwX zGxlqmUjP2%b(P?)J;+SucavB_jkbl-Hh@(#ci$J2T737e)T2V{jzAxQYWFYh*We{( zwzZl(96T=f)hk{(RjK`;OI9-tjg1whcgi5@J8_~V9LGL~G_{&^Z&)J;zY?I$F$rL} zg{eC77-i|5cU*(o&TkwQY^AxLv^;)%Fe=v79oiQe(Z+X^k@V_$R)ze4UZnKvO<2;W zPE8^n+6-a(j#qdNk>~GRo$e*CUw2zGxbnNxS8Ln35nUGqzKFZYY!aE|ip3fp`hA2W zOs&Ygdp_6Hl*i=G=;Wpx(pgzA@Qy2M9!T%Xouy+sp10DDVbH4Dx9^aE4QGpHFKKF( znsF9^0M)#gt%;CTQi9c54i~KK;-b)JKa@jU7`cw>jX{>qcj=E9VPj|4@<};)I*foLLn7Zj8}8Z}Z)IoyiTQ3(KES!MvNRA4E_%fFG5ZgPHc z@MkD3Hn=zJ&kmB3g#I?hkwX?-HNP_8QcOByKx4l;^<8-zTU)Ll4B8hSZauT>)!}mR z?1$^=iHVW2ygK5pLoP{hap;@ATVNW@?R=wnwEMYy6Rgc%46rud@I!|xzw9rv$v@PV z;6IJW-ZP8juVQzD_F20B;~Dk8e;+nlv*i1V3%HLye7HxEn|go3+`Ty@g7OLdZ}z7t zX{wHOqDehwS4xRYZ~Gf-WA6PC78WZWYV{j-NiJK#D@xgp&)ZfYH&Vg?;&SLyI z=FRHi+fL z0;qbFW!$1QAx-l1V>UnQnjp=s!hh@2^TDPuWnguKkxW}!;tc0C8dwy)2&$2$<8;35 z4lA}30AE4vE}n}s#=Bbi?b)25jrCm<_HjjM`O@aTGFSuQL_6jH)RAg`q34|P4@~$x zU#4iNs|#_yT8hiy4+w##z;7anyms&}A6S%j|$hV0D z&@bRHEn2h)?X$D~g6K(-@3c1UD@>b5y|z-2(Jdn+nF zx!>Z(TDMFu51CoFWh{HD z-Kg1zJr6r4bYdUFqdC$|y!DAMB>u+N*VUPyuLyhpc;Jh@N7_W^?#i}oH~!d{j%$IW zK}bRBU)>)+V;)D3n;)h~0YPk|G-SS{Z=QjTg+)8LWi4L~o>JRa=2EoRy>TAW;nBoO zCD4CV=e6akHb%&wL{EBt>Rp<)dAXZf)5 z`)>W{Bt-h} z^G^L0^Lb%R*j(ZYXy^6g`+|U^&8G%wzB7tG%om>~^2e_lRvJ&BB_Rdy|a3w;CH z&`cum=-I_!y4X*)A#x!yAQ_eowmA6|C3J)5fy-GdI#*3-}DyL6~pP$fnn`5quGukTQF~4rg~UH!UA?OG!%bnCk6$k4H+4SZaaf{ z!C|51XCOs?s5;0lZd5+jfakNhYls}8CmrK+D+5_TfpO_kjoz?);vjnR1S)Bb^jHNa z1abpFesp!ekS>AM2Dr-a-<1Fz=Xvwvy!PYncYSlSz9~r2#FJ0MKL#&mSiTQ4GE2+$L!&2YUCPJ= zg~{nb9*El>yX|D=e6GWnc>w_dH=`atwClYyb@}r_MxG3?0#`o{%LW?ecFCpse$Rd%_Wu+9QqDc^LGm?+u04C^IqNlU8ap(4tyUTnn>E|Fj7ig_ z#MeB{UWNHK;|5jKp>x4T4IO^VC&np@i@Wj9+XrVw9Pzf{!0_H1Ydf?XxmO|D#_-Lv zXAK0WDuey|z0y`Zo|CM@$BcNgNPF_~{sq?KHKHfECFDWuWqumA9BbrtdYN#A99@`g z%t)R=WZ=nt`ug?l``^LtgW&?rQXW>9nXJig5p}zn3a?fVJ7A~1&T8^k54cFtGd<$9 zR3`Wgo@C$2cWFQuI2afdm%n@0FJNEK-Ru&1{8(KHKL?i7fbU8a^_Tgb=pieYljb;7 zwWx1TMpX0v8G|#=V?43{z=5tkxM3B#^vR~GU3y#dVW-8}>ppt;klDJ|(0zIDXe#ks zYMhB>3`ZNp16U_Eb8`Cm^HTucmOGNEcI35R{z)p_c+=Q6uxG4~Gcp#A8<$8*OK@4` zH(=Z^3AiuRq&QpyaGX9pL#Mm)zB8iK?$dLfNu&eK44>TWw+v2}9dv`JbskZV_kJ_^2klkZ%xBzDGsGi1iM9B+r2 zk=L&M=C8x`C^_v?X2+_M2m^w9wbd^MseGNea3M5dDwzhhmD{2rP{=Ra>o!JZwH|16 zPzbkT(tF=ukt+?HG%cZ*b_}e_{A%xtDCAzz`-imqwXq>R;h$T|&ORBiwx2xN-B>wv z@ZhZ#O%eiLbz{45y)LbG4s&iBeS#h*V$=Gr1&K2a^{>b@O}D6i%|0f@aIM9)7Ktr2 zPhllAteL5bl)4E{zg#cw ze^6anZ+;AydswVQy(do^&g|8%-U!Tj4H1A#PDsAgnU&p}3SHLRT+(N=KU9!vE8jUY zXMT2-=%z4aJm+9aJ6jbvz9&w6W>bKy?4{KgxHg}ETWDkq*3-6}xitQJZS$uoyO)f0 zma!V#RBuY*L8p2mFNdjkt~2AnJW*x-@j zA54>t;7{?&m&dcm4J)aY-)mEF|Nicn3q6WF4^Yg}t4-JIjNa9KDfi{2yZ@rZX)hlx zHpuNFJ!1Xqt?$on?sr3j&+=aBcy)!aUqrC2yzfwg-pZ9LK`+gRe@WN7SH(ZlAr{@5 zHfvU{@7o?dd%_^4r4tb!o%60erT(ty%D)Gh`^-d%3{i2oT>9#FQVo?~OT?+2x%h$& zK!W-Y88R7Xlcsq|3T8V0_gg39?oE?Lw}o@oFH9P1-m7Qy`~UxJO?QCh2Kx_i-1qUh z;!!oiDptbN$@0G!rN!hO@eX09y_B4N`nn;*zq+ELC!o|h_JDnch_D%gcX;dt`->32 ziTKXS`)CN2m0z->B6$!^C~=#!PX^+bz>2RL;Onsl(eRI_Q_b*=75OrHdMRVhOt{kL z|6Y|LDU6(YqR1T?D5D)w-DYi#dMa=!mNLwoLYKbXdVTFe)t1N7hF?Q9`U;$@Q|kjbSc1SPV@5TXl>G}wQc~)GMmLQuLqJNtIkbC{ z77P-AMDQha&CVkZK-agOJQ>qBdaM@zYk14{SL`@lv^Ew5Z;zVn|9yVjMj`XyOc0SF zVFYS;OpH)oT~|I1hWm-3xQ^Ch8o9tg7RHHH=>UdKLqp%`2pLM5S|>@~u4%`~?e1&zIHsLJ`;Rk4F@_`Y@*BWSafr!e8 zKOnQAYl4DXgQj>3gBsP2ykg?H%CUi`3>>_PNI(mYd{kCk*cH(F<=eL@H`noGXB_9D z@m0aZeXPjs44^1{wc)VodhjnTwm`vc8+AFkt*J180z59cWx)Z{^_QrSCSGe-r1igN zz21zN-d>;@3;6SKfDTSuE)2vNH}mIL_rYqeBs;qjP^%6%SDM0)pFi8~1us?I|MDzZ z5YsFc3~{+HX+zC~JGh3SnJ9PJWbk@!6ZjU8TIR>0ua%jrxHYU>5|zJ0j2DuX0W~1N zNND&7H*MaW=tijyDx;$3EU2=~6Z#az6qBUtSgzLiXT$aN@r8?dV#Gul6cEsUQ+?%l zqj4H;qA@na?e9OkzE!%ix%0nhv~3jBHgprDFiLdh3nct{@K5I1X|8v+0}n5}?tOgx zuc7UVHvR7lvWTO}nm2zwQ~%u$sx89pb)^JI>vQ%UjE~>3u?$HAu!A%dDM?8ci2Dg6 z&uiT1aqptU|NKD`JJ9Dxt_HI*h(5e}E6uLL5yWVxKp8XUKVx!N{%drQxfo=ty_sH? z55&2WS`Mh4OGNc7k!fXZ{gI%Xn|p_p@qb5nLJhW0bRS#`U@1->`5X5jg8%9caKJ`1 z4SsM3P#EI6mzfHNQTOhpT)RdqL5zCy2G|1`^YQaoKQXTL++Yy)O@74s|Kj6%gCw7fKk?@YEg?dKEst5l)sHAvB6_tZ@Rz>H`uSk13H^>HO(^;)}AhJ|W zoRC@&;DskV?Ou(~jnve2Pz8=TZXqGW4$#-1!_5D5IyN&O8DdT};NkcT10rKHTCw$r zdFmMkp5-s${(%ZpzNEkYIGU7TKI7j=&qT8U2%=!Hq93fC-%>d#e89*U z%9~=kE{IPp%eeclUewZbm!**p-&KTX2JWv?eRfAdO+fZel{MVCDINl5T485^_Je0nBA|W+* zidY}WV+}#K_7`&yue|A4WPpyF+?XC#YuhLOCgpJ<3|u=q2H>|~Z{gr|#1sruh&ps= zEVot!yY|2{I1Fu_My!0vaGuwk#^z>F52srjurLt;5AJr~x0?K|1>&rdeI(be`A#&D zEb_l{YuE+IAk5E*OnlmCn^RFqQiQASVuUVMO9MATC_!RGFoXlJa#;^#V0T>UrHm7> zw6`>!4f^2Zv`Y<~A6lgK0(fDIFZ(T0o?~x6DRJ7VS*IBwA1vYS*7$6gxpcVP;2}eL zgj+cck*;qKGa&NhUIq`^+ApiTuhWkT0<8^0P#h3vY5XIF;$88+cyM>w<3IS(!GpWW zvh${yf)?h;k{g zHjO>3!|qRTkH-GUGdxdA08i#^2ttYSaoB8Y{3NsEiGoVG!*5#`KWn8mCs>#1^K<;sC}x4ok2qi?@M%n zK3zky$r}dbIMvhh+#U-rFTHwwI@gkezaYXX3#`K^R;h*~I@8kfs8L+sNo&`R7StK5 z?5zg8+NnG|3DZ&=JijX1_T;;*^Kno}*uz_Z+{w24!AOlfTB|o!AocM8Qn5Y-hOmy~C$s8p$l2 z!{OI^19MAW1XzZjAG{(StaXg`+nejVDZ#yiiXj><^nlJuK=7e<0)7*1Z@BSq-oC}a zsQ%+8q-2nhjSUSU)W6Ekb|tU_f#vCaAt#o#UcDbUHHmQf$dU;o;RRhi`}4?x^yHu% ze?){XVLwC61W`z3VtSKoLQC=Ao|WL9ya>RbZ|~nr3D|P+k8#u&EYkWCD+V33Hm=9H z&*BQJ;^N;weo$7g`}|@EE4p86QRYGAZR0UgKiB^qG~P$=+_gAD%FSE1NDg7yH*cCQ z{>STc4>|OYn!xN(X-6UoSMu{S^z5y=tB^*(dcjv>x{hLaU7^=%Eo0-}v8-osu(zk+ zhFU$YqusB93NNn&E(9hLV2s(8@DOMeg)B$~;XR-al0EoKf;p_4S+7YlG@Ad>d;)rg zsf7^;+6e~}66!z9QF3s24OP256ssA|iFm@-+`>BKv8T44Ufr3W4NWc7v;Yo}O;v5Q z6hY>8z_DJx-st>Hf(KFpu@k#?py;H5@6q;_D!BEs&y}PXEbv0(xv5_ zjlv$;_3>~zT7-xHGU~jyDK3_bj+QU~#3U6VuY>>y`1tE{l4x0(8G8*$fRqQKo`{Tu z_=`3xUM+&{JBR3>9V*p#8f#ZlHlI9k!bWR17~}~b0OAqMZurI=O9*wUs^`z0n|xDi z?AX%#_dn4;bW+lFzakns;u9?1Q5O)YOEmX6tQ;cJ8e+jThhF1KW+U*_3$w zojSWBBk4}Rz-0qVx3)8Vp~Iu*0RN;ms!-@8r0}2d?iCq=-*2>pstMQ;i2fRMCMUWh zie_{Jv|e|;iu*O0jRlwt6~E$_1HRUYFS1V`rZvCOVl-VfxWm{VMj3yglhYWO!=>$?234wkZ0!Qi96|t8uuh|hN?YZ)ePZW01H2F4f zyCzJ@`+bVaUsN1%r0Js=N|>@O_2^aN4U$DV7?OTsVtt-kz>&v)%#5imXyXXpOrn;x z&gOnchJ~q*^`^w2X*a^~hZDiV6<)p2Z=Akp+9rNpBAC;zl=p06Xec%Y$FGTn@v&pi zp3UR_u{WVwLd4$Mc488aaQa|_q`+jF3NHV|utD(Ael!4xuQoi$L42o3&jw-TRl*6X zu=e7XG04{l+@2<#fm&ac+03=rbNDb0v)UUOVSMH)L51~b+cR5E%5u4ZY0~E+{fwI= zM6NP@A+P-3KFOc>TIzStAqMhsQqPj8%cofDgvxh>upAZlzH7LZ+lXFqHyJXu6ObaM zo^nwR8F%JJ(d3$_KYuo)xx~nX=G=~94_yTH0@vjqVHA!A9~qCC_`J1itCc2Rc#j!r zM9qENr2wiq?t!;%-O6|K-m#@4)#Ikn5kfhIHDQ4H2~P&*-*3Fa<3_AsZ^{VnyOg}j zB*&rR*boX(S>$}DCz<>@HGUGKfeUsv{)m}Gb>e#L| zhX#K>X2Q{LY;2_C;Z@59&v0_8EiYFeAS+AF6u`s+qBE{t4jWt$?j77|ED9P8Rvv_D ze{@{Oirpmvt>4v##_m{U;jTvNh4E0h?u>kq#+4UUdnal6`E|vdxVBeMB6Yf(I!e^W z;2E+|^TU&+#^34!bzz#mIS5|Y%a z`x+7g4%J*or-6yMQ%-Mue%lFePJ7*TYt|5AAwt4I5yhqi`KbJ6%cIAS16Dj|f?~Ah z{aL5XLpc?4s-}ie+Zn1h3))9<>(D!gI$QW%l9*u#a;2x%`6!D+nS$UpLVA5!%CgrFzK&zS77h@LU zT-`Dn`Mt>cC={%1o@Qrj;#Ef7Ty)+XNULq$YOt%&r?YlmiY_sheN`dRu4r}>_N4z5 z4{27qN_bvmPc}@e;}LUt@22-veqg7+;US|T7?99w>myN0^;z&d$kvkVA<9;9#Gz|1 zQaKY_u*9Gwe^YzCBcBZL@!C4a*JR|m(d%JYOMh^3*wymJ7Cc&9bfVYTK5CK{Db#D| za^g5Jo>be;PApQBaX$yU64;a_P+4P)Cu^Wv7?8Bh!evKlRL+@(Sko{6}DkD!U_)zF*b# z+bOF78XRvHkU;~q=7q6K&cE$zni;2C+TV7)XndoiOs=o=${tu*=JZE%x5a-mr^VaH zXPX5JINaPuu`AGz0SCi(Y)Euw#{rc=^#{21tvz)7_+p?MFhxp|-sg+~m$Vci387uI z@}JtN{^w86kFs^Xu51)4(;F7)ZZLmELuJ?7^K&R6kbhLy-T``ExM@=h?O)<(bL|)o zmeZ_RSCHdvIxiC!Ptpv2Bv-JlV9A3&MLh6Sci%|l{_wHF-W8GEy7rqeYO%4pzq)8` zHQG=O#M&EPx*a&%XXzbvLX6RTnHMbZh=!Vkk-dkO^d}>%9 zJ%xlQSP6uV875CNC2CT^MjK&dDpb?7rrAE`q9Q_sA>#vlrqfF_E>9GPKMo%|oMJ^} zA3Fy;WasEe_ui?!Sn|m8okPQK-1sxq>~HsO-TF-gY6X>km+^(t6^W=(3@P!{_@3RpvD#7 z4+ZhQP#u$%xH~9r=l$-OHeuP~#n1l?1z^20a#U|m`1^pySf^fQL?uw03+v{c3rL^m z;9xMobp6P|z40yh3=FO@Sorinh?#dUKP$pq-FPI|_xyzmc{49u)!?R!YUmbL{<%3-a%9^H z_vG(&4SP}7_wt3&YH4mCD=S4T%V;^k-aXd0voDf8dC`a|L~9RX->M>?v$}-1*Z6*2 zzWhC`Yrlw{I|<~QG43MKTo`7nwVSYQS^N+n<|XrgIQGL?ACoqH0mA~TByzyZ*RSaa z{0`P6_AzfM?#5|GgCbSbr6o6R_0IaRmk`aZq3cyW=0Q)%;i&ud3tzaA z1_rBGM}6;J+J6?2R1qb=A{E{yted>4NV4Ez20brr>y8UPS#t+&y^VEBd(+pCA3J+` zCcp+dxebsOkcDO`$%`VUaYrwyYQ}5Xg($bPjws~D8iZS6tdO_n2|3{2qe5EJ)W&pk zAMMufhugBOCs$@3veR}-U)Q-fXl#1!hXIC$%4Uc(U)F48^P`KW=U>MEh?;n6xCb)E zytj&T6u9wV_}bfVGDZ7dnseJJ9ieFdhk;nRELn2mnbp}@8pwJ4)*8BvFd8vphX2<1 z`xznTuZk7SWKoUZou~OLh+mf51N-Z`=T;FgoBgEQ*>c$fyK3My#?k<}e2ZC+@tulk z==>y=AABpJ_~J%zk^XHwxw$_KHyW0%?;3V~B0VP$cEh(fM*sN3wtd|H<+{yj4|lf zFX#F59zq}!rfi%s`PN*k6>qmuXv4zGnKncLBoI7H-a}EuAUWlj$+NsWR5@5QKDbx) zHT%l(d`A29&^RBzoMjKbp(rgaO&XXp;mLUQ8&YT|`jn(Ahmgc8#tosQOZq!-*f1Ok zb(PK-g|Aguur+nw1du|?oVKsm${IJd0^hjJ9blyL2ngyj90({`6$Oell{A~2f@vk2?ysxUFrmbt*)U#)-*cA`LiBWg{l}Wlv8*zZ0mn_a+ z8Llx-6z6G0K_I$E7M@Bk(!VT{w<(F>B~Y*I9KTSf?l>?XX7E`SL6Nr`6ux zpK4B^S5DBZ(Bn~zm6VjE=~;vxoC6{)I4iHCRmKSoLft1&$-Zxmd5b?`m5^_6s{Q@@ zcj&j97@JWqh?@-O&o5y!n(>XGu-fchOB+DB#UJEXF~qXd9yxq?S6_K#I}C@Q=t2V% z1S~mc&Wzc=KkP$;lt4HTK{-juN#iX#%gWv$s8Em)(cly`ld#;sqVN&r!2CuRsqr*L z^H@A9qKzX$bC?@)h&{dEg~@~0Y+xS9pGW$7Tsi>VU%w3Pi% zg9zbJ$p<`SyGcCz`xuY1`mmz#w7sr8(Xvw~c7Dud@dS0p2bREph9fm%^yt}6PHODh zvgaS*-#FWuvr?6EjJ)Jemm1y!1doU*QhQTUyo!>>10)Wp_W-ZnlcRW0eI6YNjc$B^ zi6N1OmzvYXTI&h1oek}>O9PlxoV3u4wwf>jinbF|2HG%yWH8FcSD2u(K_u((V+B=y zRKyFgQp{DPZf2h^LE9zM2Y>Kat=+^W%$x#t{kAo~m5;=4tUiUBng)f7&c&aoy=rc? zbf=a>2M?aTa6zqa-+~X{(W4Rq!Y`Uahha)Od)_<%XXreWwC?|NSXPTKxVsjJmR^Bz zP}m*t)28O;%db4btwhBat_UnzeO1z5F+4VQE{~XuF5aA8xW>Y2DF|>(8$n5R?+>*zS%Rhcx z589V>YlW-pB@E2uL;ER)qW~21JOW!=W?^hjcj?ENea9VEpTfn^`-`p8iCFNS|j)?(3brc z{5ySlJHwd8td*SJ*?g04N~%<3hiV-r@E3`ER{1mq5(4?bX$bFr-prYop%tj|(GAen zu3VaE-L-GuowhU-Y~0-$5y2oOu?kFC5adHGJ@kaE{Cq@ZF3P_u?{p%6NkF}vv;95()E=B*sk zF*utm&|cu9JtF=m34t1>;DRqW7+`{!Df+`X6atx~=j1RTqfW~XRbo>bxhY(Yy(2Xr zKSt+QaXl=pMHIfAK}gyoLI$?Be6cvj`2lb(MTZ4E)a5V1=*n+?nY+pbVx=I^xDPZ6 z&9R<3b$|`MeL+FNN1s1)AAITkKU0IF4ngc|-M?R^c@$d`iaf{pOaQS#5ad-Zn8NwO zYSHzg^4$M`%e*d3hKR{T&#+)Y$j-^B_`s0`Qo-Ye%G~yO_2FaS*)*g+J?=_&Wq)X|Yqy&rdz z8XtD7cAPv>0U`^e%G>=d5+6f*6!bt!mc-pesX>Wtkys}_jR=BUM#k8cwfCWK{S$@4 zys=upa^=ugSNG(6wpK&5gkT>mo^hsS089tZ9J6Cr2$2+XVyIx{iD&ZxKWt7utskxx z;$o2~BAs3xXYAvt;<1iZXmsPoW!q2Km7>~F48OvN@8;~syL2=)E6*%m*O^g#qlP%j z+c^PK9mp(`KTbM;h)*0ic9#v-(TL-}u*DNkn$D`G`%j^0C*nA`txSDV#ashzJ^vCE z$|uhFD?59OyQ=yPWzTmH;$vfZ0j6UQcPaY}z)@PNDmX7$vJR!UV)zegG}@%S#BP`B1Pn}DeX9JgD}O*1rY`=CWa;^(vp%_PIpg-oQv#V>iv)(aHL)JbeQzDEp2||=`BRxI-zyat? zR1~m(aP?vIqx7wVg^zW~U3DtEJ|o;@cICAfaEMph@fY|jy!M8zEg801Cns($k>;2PGL!`hZ9bU-wQPomhsV+QLPkLR!3^BXQ%|oZ zbl7QsXNJ}2XRWRPqQeGm3mk*Sk0<;RU2b>w+{re(J8mNu2k*Sm7lKzR*trO=Pq^uX zff+sAiPM4Y7ibcI2x;Gc{5V$R?qxU}Ghw${O@##6i}q;ns-T{oY`<*JKS1)&iANA3 zk+V^|vywJKN8F%|rxyfiK_I(w*}SZOH4i6d6Pi?weNIv5Qgksu!->oM*UI7(o^}fX zh-vDLWEqtQ9tzvpZs}Gued6FJsfCl47S717DPfqL6Ey( z)Ji78#T?F_SUc*Y?!%7jybui5D9av!5q60=<=O;?Esj4?HpyYUCWvRYW zXDrr#yye$}-v3~T#OLo1VaWAi6h$gk7W#+$OzO^5pb1*_Iz1Lm^TPO4nQJcpr1%d) zGaZ;ntf3Y>E0Tv<2lk5;5gK@oHZ#)N$qOaQ6T!_KK<%IGLdx6bw|M*i$eHG%ml-8-b0%KTCWkL z1V8FwlELdQQ{tAO|0FIh3*C+l2-pmtbf9Y4ISn&2HG!oE7Xkf}STxc=(^PkvP#G83 zyFOGDFkQAk%hZ#f%~IWldY^#zjHAb;J@8+q&v|J_a)BcA zOhKvsop)t!M*@CKTVCmbJjHhz$dOoGq%IiN(xpFJ>6D#rJbeU`iZ*t?%&67k@h|7l#PD;TSR=6`-cu; z4>hx%AQBZ6PzX_q<-|* z2~$G5v}cm|E&R#hMIayu`gw)#!j1$^W&O7sN(f>IztPO>u>IqSa2|o77kv8I*tD#OY&lkIY?-Qd0fTl%eX2r{p;7&lHrh}yl| zb@Ad3{pF;N0+lhVG&1VtyI#zB$nvDYt%+_VURkq6%6Q=PP*IUvKwgG4LM_03m+e92 zLO;uu$NA6{vl_KH*@U3O#t&3OjMWhpN0=`*!+DhF~I>SCLo%E-!k?^4&*ee+z*dCV?3-Xrj1C0hEGnE4TZ# z!{)%3>AlIyy0!h~+1}0)__-H6R;)lI`2J9F(DyM6U<|=md?T#X6*AQ*oe=_K2i56? zO*;XRUm3xYg)uL~KMqijoWQpnMJ`R7S-@wKCMhp4m3 zgB+-XFIRr0e)*~$x>gnbGx9*#@s16k4p~{|uDezuS&X#k&$kcN)dj5H>O|d<7H}Qd zAth|IH$arx9$-R>2(h}|<+J7Lt?NbB(@k0z~ZxP`N`I%ZHF9=ZLw4Rx>NWEKb34^P}ev=2s2p`&+iH%72J-MO!k zf4=ercy3=x^85YK=Y7|m4s82a*MPZkZmXqoh`90cQ+dymG|d=BzL9+wSRrGG1a|c3 z9yxmU@wN23H;3Sj`!20PLf}`WVQVM=<_2{t0fC!NyV^HsTurZ1aHV4OUHm)64{w;T@j*yj; zbF|-Iavh&;>*LgwsY&K7!7VP`^nhy-diGFMbk$P^upzgA=X-_73F;aeU6quUEL)bp zNYtlOT)UoPwU(ciRW1*KF%)GoN&<$?;TpR!1cG9ka>j7S9lGj3iJxq!+;7!IsH?BP z-N#=2kF@PRUA*tjH*C1U8r@(E%+U!y&{L}J=`roT3!o|vTA4#qp%aR3-qjMEnUL|7 z8_7cZ{pZ<}dJO>uWo8?2`pqGtc?3V@03b4GcgBwYNacj?Oar-?y^~3gpG_xj1Z03Ol89~75;XC5sBCrP?9LmNiWqu8QP0LZDAqO9jTN` zUZ<*;h#E3IHI}j*i(j85leB@@&)bfDmhic)~$Q>rg1{yJmm5yUfnEKryu;a7KJd-Iqv zMh|wl><8y#VXg1y6Tmb>Kp3Y@^CGulh5?TOJ`lY;c-t_ns>k(F2r~;~(&8=ID00>- zf0r}vK;EvQk;r?eT2`9sHu1;Y4Nt1M1QB6j$}CR+*fG*)B$RW>sqyq}0uP-uraS@X z-~A_mkjX8?7m7D7&YSR+=w4VC`*>tf=M+26{5cTP;juAcB!axz$IqN`m_0kVT_655 zBdC@slrGrcm;k3%)wYiL@lf|dNXT6(OUw$s)zm0sCy{>({D%5(nqr^-Q~_nyYz=19 zF+v6QAn(~T3luLs-Q@JGmzf`X#m=>|as!yDTQgumDdiI(8`4M0M7e*=b;?gIL{q@h zVn^*TpQO#`Y#GziTPu_+i;GM~o@He;g(cr1hMFGu{{4HM2`F8`pIKQ&D7wv}u4^lY z3>zjA8MfZ8@%*fixqV?x#)OkkUGyD1JkIXh_hM>99Umu0jTI3TU7~%T0W*l~)boO% zA3qs>vFi>NywhlMit_doQ9#ZC;OQD*FHD_yrVoS`026TXuPb+M-;TQuIt>tfFe~F`egx&Ut2;8LQe1GyZWmYx= zj8phWxa>#%J<-=sFfxuK%x0qt1NavjbKQPY#Z80=7F$9u~|d48Srs+*IN^I zUDF%u-;x#*bz^AfKGbd^h}(%n)+EjLra-S;R3H+y>)1Isoor{kb~LU;lzC z_p;#8Bl=zZwqAlduTFqX1GI(Nhb?6TSQQL?*dUB4=AL(K0{^3Rb8$SbMQH~(jos2A zw_8LY%*7CQ#zjZhi|n_aLowXGos+Zm_eW?%*tvlOz;Wv=2z<{MBQ($p% zBK@qVk`l{9@uRRzKZ@n4!KhJ`rIi8O=>oF5D0~U2I8;WZ43k$K`kD`2`1t5GOK>EQj68lF-7YMDSqw8(&;JWa3F%Il6zvbU5 zvm@Dj57`nPi1j8}7=c)0a!?%GVOG)dd}%DbbIW{`^TMg~=UboWjsMYm%r`6FQ4d~! zYZ-bh=JHLl$*e)Fzf0`bq2^QvY3X;0@XVJjz!lBVY-gK$#j#;prlu;gz%r{f4$fbA#l36GAIud7 z?KO7J0^RMWkRVae@N4s9-C35M(-c4&Dt0D-u(6_8@BicL-|^%qyi}rs;0R6@$ggckhlk zVAt5|_eBaAytUbua2aHg)+VzJi1}^P(FirOL+rGt#Y-p)w$sLPr=0+xZ~O#fRhX2@ z#y&bjIViUPPg{HBq<~Y9*Wf*L8xRqmwcg3K{0qlV5P{;peGRm=--Bz@aOw{q{&5m< zW{iJ}vhcX1M7BQ}n9fEyL_*+bQ_@5Z&fL_ryWb~E{AIumUbdY$1Bs1x&`6%xD)H}f zw=7?_?7CNlMP;*rm`qwNkQG7YNDFB5U%k5iQSD+kx7@A|9^59)`uO;S1#O}T2g)~F zkqRKN*2;5d&UEYEecOt9z!g-BkOP5#zwP$)T6{Py zqflbUoytt<|g5VnW+y*TVTLoi>ool zAG&iwyy0rii1ye*Fl4KEbnDwZ_!VU?GUjJDG@!$|-3?TT2{?nUW8A|9-8c7FWw zW!UNtnW(J|KLWFfE;XrV1OWw`gn)Y_M^Nz^>tAOxq>KH{nMSxVa(n^(YrWB1>RzOW z7&*V|ygT0wJlMNSyOCLljujQTp>=B}4#nKyd|+S})pgt7-%d(9#-DsDA#eogK>U^{ zNAp$?Ibni!AicIzkw4T8s7iwH&6v>HUO*QLAO>D8EBG;@qbtTR4|C27C!uaK9Qyw2 z7bXp&u#L|Tzo5_u@zK#hgyRnOf%MQXCmjPu96JAs4sH#EOX)7ERlic`bEP}?w%Q}td(^4swv%E>*0?69n*of5 zv}3$-T=1`77o`}Dy4FAv|9DdhtDW{AwiFGz{Z#dZBXpEVhZsu=`*WUR-?epo=a{J{y zg{&o)3m!f!w>pbmflpESE-%VXdJ#cjf$XzqvryWDG1e&`xa~~ywpxbKKw7YAmR^L- z@sQx)+a5+rJ4TaNDT}|g8_L~tQlry%Ek;{JTp^Hb0Q$uZ%-$fcz$fnEq0+PG zNqQpfm;@%9n>Paza#!IOU7r|Fxr#~=1{zH46gG*VypgBil#`(-&3Q1azOVZlauRpK=)F6hB})t9o;Mve7(IIRfCHdk zKHYO353LJU{sY5{Rl1C*MGOD7)`X*%xp#fIz-(kIE$V%rCNcc-vo}0IQOto!5VVBL zOA3trTl}1zodK2U`_uN4%2=9o8JSRz!E+Q=ba_WP%o|_wz+LK7S zpu1vm8EkR8?@IaqOlx^Y@7Gaq-5@7kJ$6ho^E$C)wc+i5C*urB1o|=lU?nhmT@KHJ zJ8{VSBh2oX*1oADsgc)}bs5*v`G-Z0%S!rVqQ#9HDz@JN$UvCuMh0780t}bZp7SvL zNCtT`{!AUmnkhFftzEtyf>%#+Vw%6sKZ=qz^1$>o!7AbfR1hcjMcleIX5y)pbAx($ zP{rK7O?i&z8;$pYxVUxQEwtznqM{xbxx>-LT4oF}h1ek>@cSVEERg;5kEzs_^Y-l+ z_@-~(@ZJv^Ud>Hcw_G~O*0$f!^LOvu!7e~?z^=c4|4I}NIxLer_3;45h5KhN_VAD! zylJh7wXnXMBRj{@F~Y?j@ez%sqvJs)WxkQ$K7USUoIv@E`Uz`)n2FRI%iZ0xLb?kA z?hnKV`#I35)r9hj3JnM&8X98aY#huIO8BUZO9)l0x*^PwM{Ns> z4;@|73h`Sr^e#+s0qzr_CNcF29&G=+#akpF=d38IZN286hv3sGdZXmK%OQZMRC zW*Zh3TWDDUc{luuPzW&EoL&9ZlsOC%mdJm*15hZSkefJBta-f7q@{^O{|YVliSXth zCYZ!F>Koyft{rooqs5uz5d%EXprAv0H@kf=xe2UIig>Yj!~viVT2I_|A)az)y-pEO z_%QfBD(FSZQ1`Tk4$Z*d@=dzfh43LEHCuo7y-dfBe?g;E&h@@d6~-^;vU_=vkI}Rt zr|Gs9#Cqw&{kPv%F$fg*gXS*3jh7+;35+>x#|}O`;t5S4@{kJ8f0)IeF))d^U{w5s z{yLWd8XDscx`@qWt*MF-$i^|!CB2^znD3_4qX%xHS=n)-bqTvvkMl7Ev@{L}(Z21x zXWzcwy?WgWXj^D*ULKp!t8&%0+y#Sw{{}n9H}ks3t4oCF->E|~w^8qMd?32OvMo*R z5Y#wXPHn6y9s`6y2pnHM9TdKP#|DA)ESR%^W#XU??TYel>&b;UkiY2&ex@3QnF+J} zntW4GvQg8*gZmE}5`mmmRb4uHGpVXzj32o8#D^Cto{fuKq{1u76a&kMa3#;{PK3S?T8h3}DmOc@j6%<3Vm{eh8No z48&5=_;y9{P(UpL_+S5-0LY>Wl$4ru3tJ)IrCSK{xlf%pl}9MvKBWPiI__M~!4Be$3H>f7Jc>Gp_Jg3}>;S<43h zQulAKvkz46#|V04u~kj;VjuO4Rj;31!@Yx%IcR+tAvFD>EE=jk(b3^p+1ri}imVZpPo2R^**erupqmv>~O&nkq&vS^SBm^C{eAyAtp6df7Pm5!b8r96Ed1y zJyJ5*K;NGih#SlWfc|!1xrd6IocAMDBh0Cjjp)^KODe>Lb3$)J{ZixU z&^n3Vx0GZ_TMq8z>>NdN&LbmF*=RB%&nqsTJ9)AvJ^w9FnabiVOV)S&dyq%Sj#!>K zLHT8eIEMja#%$(0$X%m)7vRI0k`k4+7Re~z@l{?`K_P4Sm~fYu^y?zB)15F-^717k zsYvDFl^olIE2f&7t}qXC|6GNBM9E&~h?-bLke|Umj$xN0F@vv}Gf@U|miiA$dG!T# zCb|2atT+y9a$vuH?agSjbI#Gm z-n}rpW5;m*G}6LfM{b9yQkZ+=I8E528K7OI|H(Z^ND?vmHZ{O}N)?rU|JCD_zmG?* z>_?aa2g4VE@l`pHxPQ?{udRy3t*Va$^pebSTrZ-8Lr=&ajoPZJQBa4_W34{hgqCnE zr<5~Ca6CF;=M9u1gfM1C=P`s|5SecJ2m>0il?b?__Yh;7w{H__yeKXy05-QY;*G;@ z>_Qraw{QQ3?nC#%K#GU?Zrnj$5WyBY!)Sf|b>7}63QMo8QoDHGjh!@1qcJ&%iFpcD zk=_tavll)DjzRfur2wNwMhEvxyZ1t_4T1m7FQfxgrxt?ItIaR% zPqqfM1UP(oIj40tpDGk91QFtXnIlKCUc5jUPr3QCTmyWR-(Y?6>FRub@hYrm5)vlQ zVt>RyRoCIzruYryFEKsHDWui)8eBggl--UER3Hw6rWZ>S8mVLHP=KVaT)fD1kE05H zpVI@n6+LM}tGJI7OwDLRgh(S~2WlxZ;VQ=k3$EWP#q)|3%BRw|ZvvGkq`tPur|81H zfDV;*TqQ4*t+TSa5cE^x?*AZ}^H%lQQnQL_u(D7G8rHbCU`bwH9+XGNpn@d`f+yM7 zFyJ<~#>$3>%tDa!{d9EvNMGyn*aq~xxHvDgWin2kVPk~CkF^C@=wF9 z{v=kDL6v&;9Hyg_)!)ni4@Ahi!8`8QOq!(rjYXv&tE#pI5Ox^)Nec(ZY*exVhw-vi zCrML^jbi!PuwkJmQ#Lhu3t2cw8h(LibYey_RixP!mArfRWP}t3$dTPaY-I!)Ik_(_ z`vZm`Bkk6s$KOO&J@@SP|7bezcrO3_|6h>^Wi+S=X(yva5<(hEX{zi7m5dgBC?u;v z8dndQim1Utf9z zDx$%yn;Gz77r-)n@Gz5N@Cxzo$s+{(+#b5d%-auN~}#%^JUQIh9mayEi& z6Te`4U`NxcthcY&$=S4|7q|$lq}mazfEH28&FL6sq4fpTuTzGM88498D3Sv4+g2~Q zF{&#nSOv?gSLMEY%U!wd^Kqk2bS?4ZZOn)XZVENWi@eFk!{Za@8lVjwQ{on@eqA(7 z55={7XPOrkFL!!HN(^fqkVn+X#72j?#-*n>W346H*uvo?pXwH(CC*)vximHPQKiY} z8lAjr#G%Cx^V1K(bnJyvNZ^^E8t`AFIU*k2?!Co|kSlc2$exe~tv|kodXMUw;4tn9 zK_L5f?dRQCJ}K7jzWwv}FQYIVSrflMB--Fg6{qTnTtv1(+cNm1|BbV&>vLcX*e2hP z?DZxHy05JsK$1@@KDW+{?AzbSs0p1JJMAC^&0Z$iq3x-EgK_~}YyYGz!R!*yfy%0? zsNW}0-$S;P0LDLloP26~ZGWzf={7c&Q7eNwf9e_fDHRfri?Kd3fk|?#Q*n?~a;v03 zkWK92+i96!tE%#qiURPyj5&xde7VDA%t~BD?{9BRPjb1&$p;=Jnn7(9EV=xXXaj%= zH-P6bEtEs6W_`_OtZ+eea@I*@%JjJ_Rvd_poirza-kojEJt-qVhf}8U4VzwJDdMUj z$;QB+>~Dp46csQ$1PO+!srMz51W{>kAxk{QPr`TXeR9yS7NDeVWVkEG4HY(vOEvsG z+#9bbGkXl-R8+1|p3DlP&Rn-<6td;Q(B6e#>0Kw`HdU>9*-OOm)BiZL5Em`#lM-D5 zD$YDruwOJ1T(JBEExMmmTI#}$(|yse*x}qH-tejhU$KeC;&M$mV+M*Hrxi;(SZX+h%t)RiPHc`(I;zi?Pq1vK92L}GbphS zhO70>-u0L|h8PCZQ6FCkg(pVNS0}DvP05D2xw_Ur zpOmeCp7?L}DJM8!e&xKU&z>o(s8lO5N+KgEE1ZpmZL9(2OaGbh1B4vuXTW$R0Ra~i zfVNc^qJk!ks)N9!TjQdo*z;Hl#W}3?GG|1=yijWMve_ZFr>NK5wiqijS_svH2)N%h z;>xChs5w1IBFy5?s>g4SY$MG{L8FBp;|S{f@r^!5&z%$9`1^NTyWJKFmou9Qdpes6 ziK)PsOalVaO$`lzCgu~9KMMww)bQw{urBM*IdfgTV?h$H-$t$c%O^I0GYicolAcwG z@|JvHtA6XM|CtUSzQgtuL3`yCFnlwFBxCM+@CO3&kHo}Wd>;zr2{6R`MI%aSK`XGHz za+HAw1i}6c`8-a9{|+Ss1A5^=e7x^=N$DFb2tJANh@&G3ujZc`2L&;Xc@bXrFRO+L zO?Gut+}{BGV9qbt%zUFyFka6EK@)#Fnss&961vTC!kv<#hkLX=p&wWGDCx55PyK&%=6BGfd3 zP^eTY4fa9M_rWkFkO-*K0BR55h!Ouwtcsmhdt|`g#TO}TMH`WpXDz@%w%s%pLD*)( z;IE|^9A>(CdOFOT*GMzVSF2b-Uk#j~7Vn<%dGMt~hKaU8uNHDWiYa^1%ozp_B8vIW zVxLd1kW_;ztuHyg{_f|uL$BO=%%u<%big*9+n=Ac?j3vYSYMtO1)H({e$`y)gx9Z` zId^9Oy1s)WmOg+*0TjKtSmD^I;4l?V^178T@7?nt*9A>B0j75xohXU8);Lan_FY9~ zK%L$+ABw!ithcq#hJXOeAoAc&%p!V$bq-f*y!b4wghu>%AgG#E-4rw0*KWU|Lsxiv z2Z5g=OSSk=z!A?TVI?#juizA>w+iA@k^|9FYzOq@Qk+9p9l{b+*F4Lqfd$M|D!LV5 zNN|K?59I8y4hEyUh$AN1hLoxa&_>sQ7{N4i(jMuX_xQ0FkIZ9hYAqNZB9xBK&L60&vfn+1+@LKM z?*VP?>wAajc7(F3z<-T zI*KOhI>08^v)^7LTmv;_WvHMJ=}s?ig?K$%RJ0Isgvs0t>Q45n_!Xte6%1|=KR#g0 z?QKK^A(B9ybGP(qRn3YA>g6$#PZm>}{#ic1Z|)^1(&*jlO1Cx>na<88Z>>oC#HUD- ztIGX4#i{OOAjh0`KF8-`I5xTtHz?4yG4m8_I@bYSCdLN}+g!7kE%!Ir!balMVTuwM zI`_JKE+q+u1mdv>(4KHQ)D`JxuW3@DbW+{q0wzrE{|r=!Nzz1Sh=2+pmncEr47bLv zWI`^^5fCHV0g(IKi)m~TRiUa+>$9$$35I6et^NB~QcKgg5sPxLaPc_i9>_PfYGEwI%G)-b zc7wA_mu>(4aZqH58|OHGnpKRG`9yodn#T6G+?wCM%!K`H3i6|zV7$cPH$$;E5U@pP z8vhIT^qU{cC>nVF#l0kCtWuk59_bZzSm$in(vR{X?RBZn@ZnL$H9gZe)|GZh&Z@bJ zI3sI1Ya`#xk`fZ)jR@5(Hj$6UYJoH4If`BNz&#%rQrzsoi%%P_b^X$6AD`0i2ds~y zdzm?Nr~e9SCPsBGe#mk{3$t$2J-!L&mc33-@0BUGYn?3{j1#`y=B6TTn{L|oKC$nJ z9HWf60hs8_?V{F90S3al-;&!E`bC2Gb$Kn%p=#T^3kvq=U zHw9jXSf*>&@>4gpw)`RV2gRY;w;_5sB~?`B17a}Q5wlK#sba)U!~wNmx-nh}(3JDp zstn)>>JB(x44V?CSktCg8H^r>UYR>Dam4S<6Y)ujMN5Vj$0$|{*By|Yqc}_)Iy83qI3AtLGz?N1 z=Ul{0JP#Ka;a#F7XFS*&>m?sH$jXY(L#$by{Me4iPHFzDbNkoDe$k>y*4Dag@cdiBY{3bo!N%uo=)2yOi&Q>Zoer>fA_2%GI?^=%K_y&O5kR;t5=WE=@RlcNO=RTrNdE*NNClvXl-ec03QFp;hcH%fNO0q5Vp`>45T`1sNvBUHE3wXJ{QOBh_v+zYJO{sRXp8xj;xojmGVaMMc6t0N2y zv`$|BWrOvE;9I8fMA|?Drt;i5)^jeh&$!EIl=HeZLP9c4ECuv5vp58dNW5y!zfst= zdpDQ)caEC+H-My2;~>%nOGY3eW+BJNzXjVOjIU$)6JVytFJJzmI0a6bi5k4>F%yS_doY?3Arq5tpi>NFc%cU0Lpfn(IhK2z!UN-^^GLm(|l2?-i& z?s3vCp@2CV32-<5vMc(NLd(`eY1Eyhc)RV_J@|+tAk5IsYi53@j>p=vliVadGuEQ{ zmCr4;cnVMyC40#+68Osd@~!;AQ zr`#m@y^4xNqYjdF{dE{@^Y@`0k+&y4Ipuij#0j8r#EIP&>0+w{8SrV)Arw1k5F(2K zyC$R2lNLrXChXcZ-;TBPzdc3uNYS{)U#^5YLXM*t?TJ;Kb%=xgFYnJB1bT_DPX+pHOVa20oGL zsKA&UsBaRFIJ**Gi)}DdPtKe>$6&~Y>+2$&Cn}_8us#K$Lx&K)G!>nV7GK^=w>9 z3Xb!_g*Va1H2vcDfQs>+()ME=nUTwD~!C6wZNlXpuM^KPFrJ0Ak{#fS{|ct|qzKSLaGOpsjyE0D);2-+!N7Qc}VnhD+q{B;%>P zFlXyz1g$U-EJh&<$|XNPENQ1yXtoh4GPG%R_PgLqp?fl3;+IyYbJDiL4?=M|D9F<=44cK%`U{ zxsO+oo3Ghe%qQf|{>WkQ-_tpH7fadySlpDAlnh3VI%MeGw|*Dpz+a~KA&hDojk=Mr z7qx3rz$B!Z;;z$|FSvjIEw?~@FBNt5e@LLdi+9+~^z;ht-#WZnG$p7#ZMe{arsFQR z2kYcU60$z7Qs-?DO&tpoQ9&nK|&*ovgA%`&Lvq6 zaWvfgoF+awrh3F;RUAGY1$q|EuFXIl{Qo23i*;KXXTNrAP`GxD<%mT0M5@gYI1u5Zp)zEJmLwj4&WZQN}a@A+{qS)kc{+ zJ7=Chzy0(`5K#ss_uIuKP?m|vt$1L{TThXmQcuw6-8M-dO?u0DZa&mx6j*kN^!q?% zPIKx8h!N^}a>3R~6pYF5v&sB23~cf_*5AOu&f0nzXt&w{!~)%A8Ov~S&$26Tzd?cV zc!?3A5aWC4%^4AoVULq$t2YwtQ!@wGl}F6KOu0^= z%TFMhLtBhawXC%5il&qI4 zH=oqy_sz6*J@uUA1(5Nt83khZXKH#IdA9!t1>k80@ZRpR%s?7Z>uz#Ak*uky)m7*5 z^8Yu?+1AbN7=<=EsZqaJ1h}w)gKyl3ulR&fz|=9IZmISRYA>8To=fBce-A z&Bk_1hye~3w-kN(bBq@`EM15wa3w-q34wf*iX3Fys7@=KITZv^3rdHj8lLj@8*UeO zchN=fAbqFt?c29BVN8HXNO<(*N&8`O4kAfGxODYu<*F8q4kjxJT_-$PK9}C#(ZQkA zSr8{4wx%O43v%eiUwr~jBiP4BTnYaJ@wRP$VatJH#WKDUF zXsFDXW|-3?cz`33(Ym@s6`%yb%Q7rkQtmhP=K5!=)s7YPA>RvV_r!^pz1jwjAFnQ4 zTJiF1a^Do^XK&uX#FEAEZlf};0=vQ8%eValkq`vXlZ0Lwdvt4I6a|43C~XPnnh*?H zq4<(2pM%Y)u_|pr*`>Fh++6g-84=old;6T-1l$I!f^UGwjfyXC&|XwEDKB1w&H&4C z1Vn}YAAdpw)}fe~851UieEPNe3L+G+!f3ytU2oR@&|PqO%it~7dNZf@{mVkdAXy6P zF@}cpAm9X+nO!8j(*eBdR{HxJ?XXU#)=b8SLC)v+|3qA4W7qHx1s z%QMNz@M^Ruo`l3s7A-w&`8wmg4OF_KRIWs*fNSHD&hVAMgQLTi;zlgl8dFJ$K&IS}lfOU40j8Wz0oH~g$$aPJ(==-J7 zP0M5Fz9-CaAX9AAJsrhh6L*} z{D^A-5ch6d03(eVVL^ZGfAF2cZZZ0kqUDBsVV$Oy>1apK0^`wu3fZDRui2C8}{UjIFkLBgZ7)_9? zWvrm{N_)tVx4Sk2dXjo<@QxsBrYlJ)Z02=RLSp0C92{&uX_W~@9v>r8$rhs3p@x)<-vmW$L^|}nJ zAR#&VJ2VzeOR?7|eM8SN0o0~oX@sYw`4G3ywyLu$+;9$*WEAsD=+~mVvwTGl-e_OQ zX;!PHd?QsH{0EtZwJp9;8JqqU6&Bh}nX;T9d%ohjW(`hYaGN_Xad(MnYQ!D_BZHPg zj2*oDt0fjuMIyPNMEpTqeEQ^JC;=6B=VfW1+Wo^9F^J0mryd9ki9$-X6EkI4^h5W3 z`S`JMO*)4-DWez3N1_wD)OYByiz%FW#t96jFFv8OhB~*ztc3B>0E%(;%eI6fIO1?r z9oF=fP;4~=w=L))I2_SGiZ8n7wqMk&c&=KXRrLFB*WoG7>8q=@5^TrrVnVBRlBoV< zh7t*PI|eyGeK72hLo@xx4JO@Zrli8}E3w(sNk=~%+PRZ8 z#-;i6r9{8A#_B@we6Q-0{E_3ww^*V#h%}1_k|r?AuXQ;NYnNgsZ)MH zbDSyV3mP+SpegeAZ{jreQR$U?si~<6su(Xtmk*+{|LzQQjmMLdqb=>{&FiG1rb#VB zQGf)Hf*o2IB!DUj;P6flaa=&3J~v`^@892PQa2S9_bYG3#>RITCO6)O>(j1+Ched| zVO_7JN25@1G=9EGwA_bgvskjLyZT^0bYKK0nzm}{dZ$}|Ih2u4@pqX8PWi4xbw=Ax zw$TH=5@6~2<<9cC-%DW?p(Q#9)5!V@XMp+E(pa+>J-gB6$-}j+o_+{tz8(mthiE z{`hHeF-apjXkbJ>c0aNL5hh9el9I+qnk7t0cWu=bhJOkV zI6?f|X3vI@dANvc>jKs`|92aj8;9c=nOX4YQJ68knds1`ld6v!-xV+p6}bvTMa{@2 zLgNRRn6wzG$rMidlw8nEt*UV|)U4yYg{oxkf`CV@#{9ug(*i=y87r8Ufu3s?72Lvw zbQ^M^tM%n2g>PU@$i>rovr-TMO?sIo2pqx1qji>#Ism3xJtHLF4cP@JS(nE}jB!>o z@mDaxyaKnUo-lA`+<6=zA$AhY<(mq)BsT z&MdtxdTb(BM$?MM?%w?$TZ0#BvXwK9Y+kA&hc3a1)AKac89Z?smjy3(M~c8!e1CbVO|@S}!0%+ya&bDI_YN@u&NS8{ zL-CeeD2Y~z)ov^;_6bLe32e^pFpxZs9CUk{-{s`CD~@H7v3z4F$SN%1^>OcUmYq9# z)ckzpZ%f=f9b(r6h$y97*<>r{By1v+C*Nb8ViS?Ki0Ha<66Nk3LO-rwx41}Tvo}NU zj~)$vw9E{a%+$1R%aapKvC0_~`g!e5YU$hlMY`~tsD;#WID>T8Z2~#U0fglGT8~DO zU&HtCH!jxoc5*6#b;ce18z<@7Q3v?f4j(SzZ$^TmqGPY9qVn5Pk`PspE506G8jB10 z|5A5%G2uS0V3wm}m!f95*!}w(Uw`*|YWbHs+caB4S^2VGt&?gl$w1cBL12b(R(5vv z&z-!Hr;*;ro|}K(-hfad{%K9|-}EhH+~a{Fk}F@SzffS!RaPPQip{g7S3DDA+jBXf zpSNZXK#YtoG+XU*G^|p^ZP8;E71D*bOsm(l?|fn+K_8Ty>j>>B>7Y-~F9aj#vHw|i zD&%#Iw~Xz-RV{5+TTK;ppEM`G>qQ~Q2N7qeh+J#y(d_JOSoToEpEOe}dAzp@J^xH- zP&zuBJ!r^PKe)53?2m?qJpG3`ITFs{&!{cz?6T*}h?PwL{zg*|N(#tvJ7{d+M5cEK zKYM$9RiD%XB2s;T>Zp1BmKTZ>3i;x-KYF!F09r}8rz>vU^SxvZ%ly%oF+Q?jqWfBd zLZsqKL!fFnPwIQ=jEHAwR)JdzVEEnoS)-ZCP|+r8>q=U6gh7iQT(b^oJfYvU7GmVpPzs-2oy5!{#8WHo#i6uw5HmhTME|`qk736Ce$}Z(k|J8sZ4^O*bb89Y(g^lmaB0&k z1P4KX2Xk8i)^MND5x~-~@$o?<@%IMW-rZuvKXs9p*=oQc4uQOVUumKU80Icl){qBs zfMLmO;?UgCoGt{jjX%XHFZc2qm}~<8wkG37_Hn}&zX(_R#f!BQg6BD3@=(clg~ z_~`q0$CG*;3WhXX@1a9V*1yrlLNLAyYTat}dFZmyM)Gw*w`E#whjiZ>5g{G3#5u2) zfpjito-XwtSqW~4)9G)2$`Y8Faj^Je#>GzCl5?! zKy_oI-siT^miMN%wKMRZ9$ka3a_H}%dKSw&J&+lqzv$RAu&%hrz>7Up&t+0hk==_9 zF}|4BM2qC1LoJj;BA4AbhYtzwzVFBpjTl+worqWPUU{&994~i}PL3}xYoid_vv)6( zRC?Q;Vu24BP!*B5Fi01X>3>aiTAoqQbU7n=xQN(8t5nlNnQO>Ui9th$$_~@;Tj$p) zGzHWTXvV%YUb5Z8#Hl|cu%$tUf30}AyIM$>8m5!h`{6>m1Cv_d+u_R4<{XEC<+1%> zF{-M1P&;~kkuaqCl8G$mizF2%)9MB*D%B zP0U&IivwB^sL5!|P!%oETp3KfsRZwGy4p0qER=1VD9N3rbbppp;+=Far zd_dzv09@!D6O%)emX7k`H!%SBuUHi7exz!%o;dLl5)Io^6*UHEy%snu@8@`oRulXK zs`|-}(53H7e@SkrFAu{VXYjk8a{%1ENvD=Vly{Xj{KL(4E4grEAN0{`ok_j(A47WR zlQQP4nB<}=IuFUH&Z@MV41dZ64(qyg)u-8D*rLE55g!Y*E<*mbW4f8fk;8|(?l~Kk zl{dQYp6=w?e(t8LcSxG(*8V)NIs)<`H?l~_w_)vwX}e|=LX(p%pK;HR8rLm@k3yZW-0I4 zd=6lmYR={#jg8-Ii*V(Wp@K>g}gy6d~ae5xG~qR;-}ZVzdKvlfR*jkcD^y1o=9K z2?%`-kAZuJ3>-+z*iEK87gu3-p`Ao8*>n%F>!q^>(oiM*S5zd(`tTrQw#RhjuV}-E zX6*HE=h6T3I)fdTIrYqONWmSs2< z3Fs5z=d7-QJE_Hxih(YX(ceW?UlYR;bHo6{6%M9cxw6vF2c%(+_b)v>R?SUKaR(0k zNPdT$6RpaF2e>|U>}0-*IkCcxjE%CmBX~e^Xe?Kg36O5&50bl9Q znd-9D{*&TrH0bCDxzHLT>t#9`w!4Tp6$08?$M3LSJOY4c0Z#&JQ_v8&;3CLQr;|q= z*)7$dKTmN;efj)(0P+de9#tdF-;^m)wCNo3z87VLquA9#2SO*rO*@HH3THv6aLu3ST%Hlz?bVA1%IbPZ$VfT;6R#5!oX3C)jSqZgOxHB@=1ns6N=n zi3M^3H&zk4c7w5u!f*!+V7II_cBB-dvpaGmh-xNJ64!0+d)6;qOIfv<@oXo)%D~X) zpVzWpxDlQ_!GIDoW3PlTFWI42_wKwf2WRIeEkWNB+#R~TeSO#4qIdtb9p`Lqh?bgp zN!WTmSZ0jNz8*d7_Fi*a&|Ku`kiR)uw%;)Onct-3CNGfcb2>OOZPuIv&n!Ei`I(XS z{+W7B`tFa5Z;Xte_PBWLZ@;EM3QDjcds9V;NmWrAYOOz90oC0m4g=C& zkA|$J$MB4yrEPgvwx+&%_G|=L0}8z41y`*9rWhF+F=K}bCh#+zoC5F;|wCk8*8UgVALSe~T0ZsQ%V*Q^9) z(@``bJo;O-@sAF=Xn$^6cAV9|o*gBGOH77F4Mx*wqqMBew$~_+r<4Kn&fACRHhb>} z_BOJfNzzHm6&1zEi*mXFVoE+bt&8u~^Zc^>#yn^qPs9Y*CRbVhtN^7*P$#rPK}`A> zuP07u#({P4SRV_g5cAT?$~lV`4O`~DYE@OeKjm@VfXk#H7Zj}V^|hNb2T&46K6_Q) z(6F|=@`|@ch`Gzkl^`ZlAMQ2R6oe%sHUM%!d9@a)|Jh!g8T0|l?IH8T2vMOTGEMp6 zk1j#Y(93~@mL(_5#RM14n-;&f|M+ji20Zb~!@=4>(>Q7F>=_s#-|K&An7Eo4M7!_s zVXwuDp)$*?&h?ewH?_xXX-iE_3a34Xatm__qE{@a?H*-?;iJmO$gSH94je9@H=gSO z^<>*ej~3oqYkAKmab{lSV&>%3)YMQ9YA{!6IC};?I~5i=G1${P3Rq|O6Cm@+djohv zAH=vgw;LI>@_NIE-$K=#@+o}g;0DHy#cvqj`ba_i9=A>2UsQfl{gY`Ld|f|Ie3rMz z_UPzN0-4r1{@AYFQh-*6`E|d{C#rEq%vKL!FoA3p7CM!A7MSbWZUW-L0Rp z*9P6|!y&nQ_v&wN433?Ch5GM*QE$K%SHbWG_3!`b>(`)}A4pnikY{R*lA4+c_z|`t z}mJeXsTd%MvvUh$X%=j{J=9=X?T$6MQsM!9-m$gR%g`Bn9+U zPj9K?QT?Dpq0Voh*#ocC+e008R;;rv3>sS_=8?OhxB272ua465kP8Nn<^9% zXQp8%O_Q~qO~7y*60+|x3V$K>`=Vs^gQkClLK5r)yoJLtE(1RquF&d}%|YI90u{Bj zte{cLpB+bNT<>opWRaU9jw^ik_APt?9vNN~kN|!HNYRZJBC7158(UB%+fgEAE`vC2 z%%4q=_31X`Wp4elFbmg7ro5<|q5bnlJ}rFv>E7dt`ZNb!ll$tcBK^{Tp1N5*!2ai- zK!4@qO_R;`ICjX?F}wb6y;)>*bXe1Zpl`QJ*A4sEA_!Lf3hz3VSOMHJ=e937T%aJd zB{e0tnx7Xs^#Q&2^agM`?FF_Vfx3jpiwtp1DV&pAdmB3-;|Js+74 zjwS}>%6CFE#X!o^b4)Z!uyt?IqrvO%wTS}^KWR$Z{(`Eaw{Ir|_1=N}(KaOk2#G}8 zvQ_+g_oYjxG!EC*jkZoQP?ZPx%u1|kh4L&+&=-l)E zw< zbP6pa>H0{ALmWoERaLWwtw6}b?i-M-4k`iC_-<7ow-0rTmE4gh3NkWH9F@K{XsK`e z87HunwO?(Tq&9B-!v_zxHc4?sG1BkOB6J143;7_edBcv_o?`N`vicT_L|FIwVUlle zt{YbJ5hd`uTm5OXX-l<+4J&Zb2LLmE9yA0ZAly0$^O0YQdi%*@EZ6up*zGDH*Ij*2 z=zXs7{>n<~%=Mii1kW6du%f%5Ie74=&{f!JC4_G^HHNE-y7%h!7MhfLG;r1Q>0Ja( z3OM*xDja535Rw+VefvY24WA>x@R9-@l~>u%KbJ}aK$!?ahCvZD0UZ1|RiRCVV0IzR zA?l%sd^ZEt?U+8@K6LGCGP&O_yo?CcNZx`{WS0rp&Atg>OW4}S4X(SB?LGQA~YK~3@Zj)K_=ACT!TdRg6 zLX#6ptE$4q3mI(5tAA3-~(DVU~_DIU?M>|VJBqX<7PuEw=CcHFpbi^LEgRsn?JGxsSj_r8{u z`L;Cq9&_xkjM%ELaES=1l<70?HHS^m(kpM43d@L$IYDv4)WlP#Q0H>eIubL0IdJ!hhyR>Rg>f(z1l>35kJZP|+ z78e@>d$=SXKklkKHC&9yp2z*)=efGsrVMV*+i#veH9z&7@`-h9>hEFYa!>Aq9m4_` z$pNy3Ef-6Jq=5E1DgaM+5B?`AhXDgR2zYWAK+HQiwc4dy21*RBoEVsSbf-#v%Gyvj z$@poot=M~<3HwIucXZd(6XN>4?GwS+wvK|Lx_Xyxy|A>Pm_9izUP1`I;tD`1HEFyr zi>f=-{fxu%!s!bah7TT0K;0R_b|$TxL9yHRPM0PH$B73~sX9GvsJ^CNft3u_Pvc)w&3>@G2MX?qPB!O20G*=;m{{HmaOHs`c_qdUZJX+1) zqy#Vr7^F8u!#?%;KY>9(ZP>c;zvdRq73X!5{GD`-R!W@kbIGNH{9P^3Fgm%qAt!z! zg`s>KlK+q;&d1NYnnFQ^exY8}G3&-QS04!?&;!=8nd6iMAtGSYa$$?!dR|AAW?xZv zqoAau~&+nt0;$X_>BxWu(b4CH^PKAK{I= zhpUk@fVhzlz5jfw3Hm=osnm#H5e3go^~X-{rsch1!;MNc9abL4qlQN9r%(6XQn(Jd z?l$BkUplXkgryDw+0_&t=1j^dIwn<0fk+ zP?n2%Ir=XrDH}CU$88v6)|$10>YED*K=5nJbGb_HE&dxldF{PK$WPUk7A#a;Gr@X5 zt*^7PTwumM$MjhU#6$GFRborqd#tkNTa7!$+#N@Z7Cb75G#ij(qu$?5AsO}pYCNos z!z}CW*xozF_}7)j7%lu?;c@bumY#K-t=O*|da&c#pnNf0<+BhpZqT$iAF+&I<1hXg zwU~_?w+Q6O=xUU=Zcpj+3$MxAZm;k$5)A+=R{UTl;{2z=F`@*aiI*{`8P3x4wR3-E zaW)0qiRg<6`o+G9pEj_tOSD?*xU5FjI5h`C7v+`7<@jf*Ra)5zEY`8G@+_+XcMmf2 zjOrtAJ13oW5|1T2A4oNB*Lk{FafIxPhSF?F@{HCi$o3nvy2Wv}z5TuFpIrOCa`l_H zyne|3I5THxU|Z{QnXnuRRFFpse&exYH$Fvq7sTyKxxwQ0cr6ZNM!k(FNz>FHB5843 zRp+&9b%bE5PRd}DX*&3)JUk%U?>al#;QL-cJn!=UUv>bYWdQM8B$^PhmX{}LCvdTG z@!rGkR{BeC(4bWKMd-~b*MWKa#4S(arN+FMh&mWb33h1A(9R7lZ|vz$smBd&4xM$~ z>%H;nSoSHY=J%KUX$u2@G+SrVH|DesiC_?UT?~mQl^>PkJPC%KhMphx2pU0WMkdod z&+0m1aQExyYTqTFyg0ePZ0F8n+Xp2^P$^Ash*eu(^S5dv}U_-G+;! zEG5gtTP8GnCV_f?6~MY+=fOY;c27*wW%%5bU5*Ax=9(M~44FygS;xi_U0g*;O18HA zmED2>t2A|QWQQ56(cq}mDs-2H&>c3*R!3}h6*Z2ZOUfX@h~vW1a|y4RThgs_m>Z}} z8Ne9dkdnDQb%q|7-k+mDE~c+bpb6&l(_^1b`SSIv=zdvtJ~?^+sI6AVIGrb2ThmUd z+Ggg0BHXY~`}w2FrY_S}b_onEi&P}AB$Q}upcI%Gg4p>nJ^$HoC2Aw3f+Rs~+L^zQ z(tihcZ&r}khubyZzngkIJxclc<o*o;KaFPzV&3$}2X65PCuN`~eQzTn62-!qt${d;P7ee%tl|4|L3FRoGRiv2+}B_L>2 zStQEohwg(@-DomawB2yj@~sETcH%z9G^(^_iP>agZ7W|wSj>31e&*b{r1gCxyxphZ z?A&wEqLGE`)3fwGa$M&h-)7cxMEtkX(x>}+(nkOSthk3bQ3H)kzBQw*oQa8xkLS#{ zxyl=r(37sBj|U{8@y_XG!fX3D`H=Ctt@&>2me!)^*gaMr8I0T8uCvM=MfYErz3exd zZx~b0RjZ=z09qmN8P1p|qm+w|A0H?5>Cxk5;nktQjsW*jy*PryT~+4{+e1h`S?B9O zIw?)wZngMce{s>m`=g%cZM%>Z0N z+_cdjg7i__!D5_*nPID~zMTY;&b(HbRX}~&ipg=EnY$XfCdae`=o%HylnQXAx`~Xo z{?lU*Ns6b^xQY`i5AaBtUf+7Mr3rsr@;dO{Wc9BkHtl z6F4_BBj2sB&mSd->~LVyPLJip9w#Rc{3v8SQ$WMP&+d9%p%m(#6Au}AZa|Mdea0dD zb?e$M<}P39043UzjiaXkaF12%diaVKoW?{HfAv|E`|uT1o~48R)0u#<$tQDsl=G%P zVULz=8^2CbYtMZycQ!m1mdc`Eoky?ZL8c-5fHe_2M335O83zv^z757gxUJHjm?%O4 zNW26(-1_$2qqtfVydD@4(lkeT*0JUDKCk2%&|_E_xPH%pa}#goy%LdDK1Mrj8E1ZGNrv1Pwq^st1oKX|ZPxpG}NawQFVasp@> z%Ro2Ja+z7FAlq3|#65V=5D9oS_z?65ay9G(3I+YszkdHV`>)}2RMdUK0hpx>^MAuZ z_k2az733|VK7fSg)55Ri%=8p{Gx!@IDUJ>v9JWk0FaxPIzV88s;{(o~ZFyo=Rb?gY zGORj)`QGy}EiHjY-e+#!TsXLUjyCwh=Lze56laiIQsOk?#~q^em+>WNC`2@Y(}{ICXpktW@0@tU{tAFD zz$8LlqB3>S=Aag4l9%agsz!AXBi15dBNr`7YD%SLioP(v5MNjWx5us@^{4BuZT_&O;zU zwj!V8?d_d1{_meZ)0uLvyMFJ82|9U-5DJ3^{pK+Ld^e}er~j{t$k73qvWhNzxB0e+ z4Hk9nAGU;t5Bs-WjDE-Z$s2=5dSGD)V{Nwi`)%2~IXBW9@*9Hoqdr!10#LRT|(3t$$*$`ngd?T zDYG`r8F6=4uBbvLi=+A{35|8PuDYJpcKZKXfa}-6J$L52i7LVOXZ~t@3rb(l9G6$G z_`*!Gx7XPpHC7)h|2(!>XqhYrxs2-{9D3v> zgxH4v{_D#?kdamTX%1d2ly8ILr-d7Lwu(@2YO686ihX|c=B%#$_r6aJ=G^1dTzOd3V=04$G^XC?fMs~(ck!sWU{F2&?G_(+@Zfu5|Le83 zI^da|yXhxQmPhtq?O9(`+RyL$n&*$SyjQQjjJOv_#rSXhMJCp#ES65N^;^Gw;H%j+ z*~}A#5#flo>6u~PQi=~Hs-TK6zN9Vlx2(m@-rmYx)hpeI?E+)TT4*K5Gs&CH8(dIU z6jcA@tU%KwX4;)~Cq_$Tm(Zl>6^{pcK4@8jfB_#@wDks-X|mI;_T23^*H;|`N#*Zk z5ckVl8=GFyyU}Ajo3u;GY#_B?95}VKl$VmI2Z6@B`hKS$1TVFV=qpgL1?C6zK(nlr zG7p1Y=pM}39Q&O9z(1g6%2DD;!d-{AW7|Jt2%5H2M>dIo{AP5~K0F-?i9SSW@zqtW6}*!|#lWM6Z? z=pTo%!(6!V=);H1_ty{!xQ~bjh4d+T!rcL91A+c}he#Pj9wuudm$H)<;rz*yuZoI> ze%5Df4p15>PImLqJ@*+GW&?Zs=+VFOgo&EB9r0&I_E=Bv%#KncL)-I>uyQ0{n{I_G zWTdJ*ut@hq4U=!K+4~#+1fMJTi8b4eIQFKYT_b+-xZsxq4>u#c!PY?3%ZLB_C}L&; zwK~V;HG}{L!^g_RPy4v~2CpY~I5|DR_ZQ1_v@nJ)N%NRXs3em-QQ&harza=GG>!Ru z-L3@Wo)eqiCpTYo=ENB+<|gy02YO&%TdkWR8&>}D<9h0H$l7)DHMo0^4dSdUM-@&b zNWpdeEBdBYu#J#&yawjFgLG1NrGby&zFB1K1Lv8tV)JeYaf+w){Z}HMht1?~H7;tQ zu@^U6{|U#hlAs698oDf~pDE^4#3U$!JE=erj5jgSm;d_w)DuLxnO+|hk3@ONZ6Kz7 z!iib+51g2DZr>FZB7+9-N$F1jT z)QQM#Z@N zjb7PP1Y4bN{kL!G38HlmTlIq#V1y|d{B9vfzyMIK27VjK7g#`;I9?JG=MVtkf2vdV zPz|S+I>U@Yt3wp=$OCy-N-vx7N{AL*6ie4Xpitoc1_}krIAXG47FU61CP$U+v}rF2 z3n?f;9>FVWPQIcdxv=2MM{G$o$@@CyUIKt%P{};z=q_YTY*ieYQ@mcjdeyUU-`j5ava(feX8)NU$#j3xm#xf! zqc|QADJJF#G6vI?u1bs|=mDlXjF8%yGqG`Z73_(yBaaMwH~jj|0hM9+(Q((r#m8sA z`^ZB9siL`wmb zZct1C$;rCsujMVf0#C;VJbotsnn%Fr8il`gX-Dy@!SG$VJoJIHfTx9wx|rt>_ac9^9u?xDBGkJY(i# z$#Mr45MbYstvr5A%Vst)&2gGADmZ_2kZPzSX+V+ntim;1Cj?4#43V@2g7!1dOGx{ zkOeeZ2KFv1FHiC(xePsjvd6RqQ9zksqS(74Yiy>TKB zB8K%(11~&N7jcxCAzS;rh^3|oPo1awnx(J2(PdefdIvt1+ZFxgyR%&&W4J6}G$1Ys zlPS)|sZ#RS2d*|3Sj^-w;XqeeP#pv+TMlzBDH>4YHF1%;tLZU;oBHiyI)O{#gAbdU zyEq>_azsJUH!uh}q9pH5X(S~PVn2@CqN5P`&t(Fb4P#Il^%p4n<wjy&=9^yGr;DEjovO%oJ0xJK}H28<^-zM~+{cX038 zF=S`Qu#ESUCu!A!ge}nNxyiYG1jldI0(WV`^6PuF^yX~w|Mz#a*Pp6bXF;5%oVFBJ z7u}NjHSjgU*ZFx^=5rKbvQ^qOPp5nO3kibenvpRv z$nQJdZu}6`Ce?0->}llYwsK`bMSWxqNa7^sxD*#VoZpBH#KU89JDtZXym4bJa(Wbe z4Ne<9@3DTzfsGg^Y-;?r;Pc_Zl{IGqZ|pQ2Fn~-y4tX@U%$R^}70j)zJH7h6Q{{^p zBlI%0rQ2y6aLIN{hQnU@4YY)L)|=6B5AME|b>_99;geGRtL?6DI9>BQO`s2!6X1Ei zEV(zVyDV9afNk5x;R}}#@*W>l<#_HSnAdNJoZO#`(Z0U-ZtT(WW>p9-Ts>~4qG_H0 zos4}V_i_K|&eT4i2a8uqnBK{{NewV(Lhze&iP25K8M&9(n-YRZbf}z&$fye+GX90R zR&DBij%`7pe5<`T0Uk}?6B1DfrbsKM$-ox@EG8}7d-7zEajx@>Ko#ugY-;o#&&D;{ zX^FCLWGD<*qf#*awB4fQ@PpU_Hzx5d;kg`qbe+4EjqHju`+yKR3xHtXs|gLYLdnr8tLA(>BE#SfF-oLNTH?;9I2%h^63!b zd)WD=zug6WHWN1prZ#h_-N`>u9HPF*%$g;?+D{^F?+QF0XqQJ3oyR4d-8Sx-v{hhC zVDe5sPQ$!Q@^0?#u?G&&?Bo!hBNw5`j7Z|ks z&n&WAXeWifhtQgyIKlP6cV_yFy`$qlspIJ`(-+fd_Hvh&mL^sQMREHBUBhJ95OXpF z>Qm--f9Sa7qLUVZP1N;NO~593J7e!wIA-|!lCF^VXye9>aK%vWH(&j0i?B|D+W>Q- z27T71(sIt~#4O(=t}G&_S~B-!es&I~o`q-FW|2tn$Jx`Tvue{hR~-V%a-@bRu(?Xs z5z^2G20^g)17IYQ%rY;H@!uw9N?b z#c}4Ge+?nBV||YG5ogm!o$g=J4omIbvu7l(b6%4ex)~IdGWaapgTl<)6(`nY-H+$4 zpy}cNGg^5`TQnz;JC~g}plbv+|23bUYC*`*idn*G_<%1X7*&p{{Q8yf*v}~8bGJq0 zmT{1xt93gF`6C2>mo>mzU(7L)qzddjMHy$HsGtH5s=W#KF#bw0` zH0djjB`^^O^=<1#6#U1JTd9?cS*5gwjC(tE%H79Dzo~XxD)*V2Hh%%0`B|04_;Hpv zYS0lzZ|(dZgA-A2uU<8Z1DMRL`*F^Q+Auo5vKRCFLEf+k(YVxoUXM5zEqVAK90$x- zZ%tPT>CO0m8-;f^s%bVF4GAe{Hwfj3;W)FP)6>@W$g=YrJ{)+DC_ z++BZELVR%-m@dvYG;usD{ru%lF+}6*zTs?U(@)yKz(hshF+1V{c8h+VvU?G|HI5`p zr=|h8Eppck1#jAOfA3+rLWUGOIOr^c1Kcxeb6fd)hTe!-0NuOUh~X$~Dkg(?_~jk2 zNTk@!$+}9K6TwhoEMrT(bcTA?EKAy^BQ3opN)EQdH}WiC!3g8VZyU6*{Y@}0qc(YS zmg8~$;7G5T!+xU?14sr8<_B9W?X8stdF34A;=r|u_gx6q&>J!02PKb5Up=S?%1jvj zw$^5Eu9TypyczP`$i#49Z+*?#rFB!9mGtAsYSzSPKfrkLJ7)$tlUJQM(?v!u=rH%$ z)=2?0!uOXbPa-0+f*nZb^Q3Aul1i>I`+FioSH7ui##~;wyD^PL0j-zcubJtgJdnVX zI}N#otomIWma33)^(yoESjPI5bWZ#fUtTIF9A(A?D~P|@%P)8Pc6lT`tBQaAxp}2p zbEXEz!H{1;q!3<*g=z>{W5K;J^!f!hI4+i!Ovn6A2K{^Vp)JF2Ht6HYa~0kzITK4de*?1B*UNLTMHf#eusf^AyvTQvA<4InmNM>5Zmx)TY)y z?#$5JrTCC*slzN!^R*q0zp1PF*?DUheYx2`7Vk@bqj?`4LeBSWMDP?^s5&j9m z+-uw3!Ylj!I=;R~pUAbNVmo~qBNN7$k9_8|l@G*fAOzzeAF{Ezqhm>}IDVSmCoz#| zxtF`1h(Wv`mjk^DSp-ZLQRt8sF`z)o$LJ6bQ4nbN2PsFHI#*|^-;TPeU6(U`#te-= znFJ!0t-1pTHIQ}!e#`Xsp+o!m6Y;TAPx??Dj!5iJ`ExOsH5rTzCh3gltx?? zoF-%tT*fcdG{-y-Ywx&meFQ;cDF}7x%0oOIc4CTgm})r7W&Fm!YXJrkO0-IJbIO#u z9knwRqJ?oCdj@4TIkO9ZwS7_%Z}g2J-)%(kW>b6&v^?MPLzTje876_l(h6b0~{xHnIr!yeb6K$7z-=pv(;FeU6bd`o~=Dmddhar zUD#AlzQLU!#{Lp?UU9Pvfld5OcpA?f?-L#m%iAW z4@-6A^G9FWIlNK)FvJxv!mD6a#Bqm&rHB zeOU=TE-g??4BRs%snDfC^tY$=jNYjHHD!uK^`Wx-H-xyHJv%~QpJZHadRR}Nt)I}y z)%&)hRikSiy6-G^$ds%XFS=i}p`3b_>g`OCA9q24P`csu-DN=eFFK235mI*dbYsYg z0|*8eA(^?>Np@%Epf5}5?TrT6(4=$PY1wW0|6y1Du8-i4h@o$1qoO(o`gUFy1|;WO zO=m43Fn^@193>3>9iA5M!VUsu)g>vq%llt;$!zI9(=9i3#-MI}fT_9)Frm4_P5M3o z9S4*usIu5bmq>Vg$$CU)G;@-#;q90M$W;hu#t;8M{z|9N34t~%mn^Y5^*m~L19Df` z{5Rc<636Q4U3_QT;aE4`6JH)gp4qF!e{DY?X1HFf`wX3XE$LBn!{;j$zy{ifnxflQ zQ}{7qnO~VWvs5?o*S?~vh^d~6NsPdoR|CG4w}U4l1cPRaBXr_No}!6BfVk!Isog{R z%?#fcx1d40?t;wYb-PjIvzCY?uc&|Hc|APbn);S7WZ^Ykp9q@WETVO2=8}k<)fu3K z25jNwC7rkBljIxpqcVy@A9k&-+J?NY;NW1SsN0rmGa;ekxgE31fPz>xFo;M*6C1bE zbDKs;cFki5yUoon4=7Egp@1j_#nHL&Uy0SknRBrluD1I55g+7Yclg4z$&^t38m7oH-w~9*bHwC z>gKa=tst;N0W3LF>)+*&;J|$xnX_ctz;32v!I8Rj!F18gj9H*P@}c&HB0xKi?Y$X} zy^(KdCD&bJK%*F%!xl-?G%hb7@--Z=-Y5~P39B$SD@zEfID6`pN}oQ%hI9|^rlh1b z_DsRMcjIVV&;#l+pH1C(EsDDxkCuJ?^a&_tN;@QpR5Lc2M3FLSLApN(HvQ#6_p z7(khA(CNIu~M|;7vDVbmTu_&>n%cCjsrwl4a9)mUhmva zYMTfChEDqSK7Iaqhi#0I=FGsi!{6uH`kjqev~LhWTuh(g1v{Tk|Ilf0Yt35xz5oI z6-#1EX|JDZUGF}A9DTQ}IlvGvz$ zXBr;S1pCmPN9aMA4j2RHI`jueKP3RDw(5i zG`i|^*-<^01U0VzI)39l7*4R{unr1!kM6gjtmF zDkEjJKnCfoFEe7W@o>-xxE^j%PGSI`Zrw&^YdtUoc^qMS@XIFrcP z-xyg-0Y|oyX%dqbm$UgGRMCtK4b5(v`kskI<9lN4MWna)r6G-lp<#sVNK{*U{VC`BceP$ZSCWT$9Q zNs(lf(X_up(IAncLW&BNtV;GMTcsf(g)$?fgd}MQp>aP>*Zn(=`#5fYT*vjhe$?l4 zp6~H`JzvjZZq)L#Yz_57>+6lIZ~ltO(eef+6C7x6_rl#M;?=_Qv244uQs(@^8;{?< zC9<$qe~c*6Nsq-Th{ekZ<^6$NmpleK*RNk6{&L9eWe~0T*#}Jmv#`ZU-wiR$+3~|x zIR;`T^E1QUN+DPbcS`?ITib2-p$AK$hjbL+d2sk+W)XMj&_pykd=*>#4EaHlNo9ld z^nCsNu)Jd{uY8es@#5o0kH{QrS)Sk9e=H2UmaD#p?+N;p}QKjUiH_nMIYC~h8e6A5Z;{i z`@p)YwvL0wYGf_BzVI8@s@rY-!3w@>b_fUfLBqch0%c=U#H8XtSb<@}3$b1?aa>sG|&G7LVQ<69! zEtNLky3XW$>6RlMP*p(4?bU0lHxF14TA2Nq4-hARxSgcVS|-zQN+Z9s`LgA!SBqAb z{q|AqQr9$Gp&{O%X+qf!2@^9%A%|oP!&fDs#odJKcbkgviQqCJGHt)r>={zzb3?#gOJmqcs1 zGj+dxPHT!BtZUH1SK_$s+bMnn#Ix`3FA@{>zkSFv>>x&B!l17%+6;Dpoe7`l-p93xv`?lT z4q@OBd6grDx34{ab9<|wum${m;kNFJVNZTTo+?qFy;TpQy_>%3^|?zjP}{@HSCEP; z>(uWfizyJkF$eNdyREA5a6p4BlSPixI4CqR1s@>BS7(@dcOQK}yjcUE9L|jFmPVT? za&~uy5WRJcaTM}0K0R+-aUv%$eJK`uixwq24w%q)4FZbUgO!CngwMoBZaS-1_b-AQ zX6l5#7o3g^*Utj-_)u91KNxAoNw8jslCWH^iy1fsSbtrf#{+>=J^1#}2|W*;^qwv?|OVchTArwq7)s}Z1w zIK6W5;YcL=1~7~TAby5L?foT!aws+teKMkl_OJ8y9sV#8yLJfz&K+n%w8HXG>D5qK zuvWXMGd?SO@T*mlZ|8adOZqEx%-rcHOcz4j+Pha$(ACp>%d!N$e|vuHZvalC-s%JG zot@v}GWqSB+&f+N5X0MB|6}NjVUvIQQ-ct<*Z|CD|6z0X61#i6knJ=W9H+)#x@DN9;eVTD( zPXGU)R47w>qPY-Br=^3I)c2jV_bfi~N1s$f9x)^#6(MBbMt4;eRaG)AvVnfF0eIA1 z)aL5w=tzih-Tc{ch8a%3iT;v8DHcmeV?;gTw$J77-$w&n4142v)gvB3BBt9_Mezd$ z4wMp3o;|zx(o4^CXU^0>*G2@5xJT>SgZuY+{H^7B<@&#&*dcXHTtZ$B?`ik$-S5a0 z%osIev+lnqX~l)ijMrg)DNg{VfT7Er#I!UoP8ql_^4+@SN3coh(58BOH{JcV2`Cm# zH?=_Ry$X&LDqHtEBE4t!{Hfa}Rp3Mivbn=hkrNlqq2qID$P%(b9K2n|v|0(#QBnOP zd(cXX-ccYLlM-p~3xwX(a2qIAn1VQuIUeJAV`1}X8>N#xKXNK(VUXI;Fz?!G^@InZ zE@TPJreHv7ru7)OBmG*t0zfw;g}8M?J8vF>H|9$`Pd+@^JZ|vVg_-LKgCy)G|9T3a zu~~9A=OR2^(s1(jU>ik|Krs-PkN_WE+&%2bBdN0hQK2sw7#Sh4xiG-uYCkC@5J-9dz=Nxl>o>>31GyB-o#k>n_XQJeK_INk%Z4Tc^;1zW9cjoN zq$V6`?-K`x< z3iZE@%w&T;@8Qyy-`N?Aq))G26$M$yoG62jT+QGVM><5UV)s>D^I(cu=650^>qL89 z%%~Z?CeFkGoX0+6Jj2%$CCj2tfajU%AE z&#kka4epDV3GGGEs?VBiwJ>{mmqVRoRiGMNNWKDk&Cvq6hP)zcIOrb^G0-kyNXNPX zBl6VYFx=fipx1%;fwlIL;uI(;4;Z^cJB>{M{bll{_ic+CC)#}UL0vb%|3Ov)1k1xd zos^_Vh%b%3ZeJ;Z(ygU@+K4RlXE!pCc@7ib^qsEhEiwC>P%DFQS=6!>_L6 zt}fVcJ}K!vzldM=<-?XCYPE6r5^yRTzSEw{;-PJKm`|&H{iRJ0lfbaU2XVZ`2F;?+)7U#KVCn9*jRKFySWuYjVn2V`Xozhz_Z33V@?;Syy*p)2PT(Ie3_P*8QxVD;O+lllskG=nT%zI4 zET9a?eUN_RPrkb8s;Z)>cv3vF#}PIbqi=>J+9(*QO7`*+c_hBF?P zm7;6K}6q)2MQKHhVi_r;juqAKek2H?#}mPKV($vN;G_A3R~f6z>}(9B}Ycj z6=*3N{ggO<_UuVT%E@H$YEA0m0!6v70s9L(4LRepB%5M_cNTKv76!qj71K3-$LRy~ z3YJTB%y@42h9@&j#!nTF^e`NMc?(_~X5}82=*Op+n=91nyIqTkU@n5S0@kAw{_N(l z^y!m;F>pYrUZ50Kv)8fyqIm$UN!3)3B+H4mmvb$CnjDPFxpRR%10b&q?6DzH!JLwJ z!Xuy;F_$1(Z*ORT|&;7_=U_GoD{fbS_Z*|)gZ+jJZsh6Lh zUz*<%9=oViyU`6E(N_7lsWOY0urj4W=*n_=vNeAJDAp2p_u$yALA&}+{KGH>%Vu%f zLP{-yQ1N{#NM|ix`+_2zas&GVoO#eeVKoM`HAS~QV2XI=?+Jye21?Ia&d{$8>Ui9b z83&IZKYFyDarEQI_V4HN;q*V;gwSK0fF*GXZa{i#4b)UAtj1#Ok0J|JN%f=q%rE&R zMzm8$9&uYT^OOiG=h(TP#}iK})U*E=G}?V%zs_=Maonb^Ia~OK%a$(1^Ai~nQ(u?B z);S&vf=u;6I%pH8u3!+~G=ZXnd#bgi1#y#^W>j;4afIb4ef^wu@z^d`8w=~u0x#`yu^zhR2*7^6(;i7n)|DcS~_it;T1uc z(Cv&92eJ9w35{B^kMMX%mh0wVA3xMZEj-ORPDcEYu+}#`bg20D3NB6}y(BNMOf!#q;cD6Vu zkFdncR6!<$oA}%Ln}JT^4HYnk$=IaHlOfc7tF3Ll^0#q%J)Jkh_uQ$uJk^pI@q1HO zcOB6|;CYhuJ#xV`7ngS6cn+ZsqGdW@TjqZ=IDL?VOsSPuGf;RtA>~ENukYU}-Pke- z7cLAJwoKERb!v8i@n#kAIP~P*9&KArAvfS zLFNn5ExC}CM2u{22GOhQI!ZJ+)wACDgj01M{+tjU6Vr|-4(GI9>;jmz@IDKADRb$; z5~V63r?_SCArY2Tjj~cwP!I&14&Zgg>$^#$owlVZ0^Pc;BPq^033A{0^xG8CK@esx ztsxdLOg6fz8}4-sQmkU630ou8TVE(=*RSuM!Q|8$$QQQ--WNM3(|G0-)L0WWG&Jye zou-OC}uveo9 zFcWL}k1K3u=Eh|NxYv@33G<)k=71?roJ=Cb6e}$)%xxUMUrJ4dv+(BbU8&e{Vf>F2 z_6vst*lMJQQwsAhSm2P4s67#1y@NgS?DQNKNN5LgKkC>d+e`Yy7K)M0N?PCaC?WV; z-u;*c9`rJs7M;cN+}pK>?(Xlo>wCgyKlb4?>2dtIoMC--Q}uxR?|Q%K)kr@{`6_A- zmoDxoCOY7W@Owbl<^2^7ow$1!LJ3I?BBc`J4LPlY#+&^42vmcwFRzG+jFkJPf)?<< zr(~+0yYONbOKueO{L9y_5$wLTt_I2hM^#L8G-J@tW0r9|i;)4ZnOBz}8*^~L9`*Yf zpZOBKdd2UySDU=OWMXk0eA&x#|?7o zEY+~7WMXnAg#^@{jX3%s<$HB#eH$5rx$S9whdWGP{3b7Zs`OOpo4+Ehlx2@P>ZYqG z$x5GD+0#2M`TUY4wcD-IhK!v%S7A_6jeF1hi#vBp1Us5ari4T*%%3zVdV|U9_J>9@ zBszq2np^d22Of+Mesz$EY91W6@R6eznqss??D=zV4 zCCi~f0F<*te7(GKO%gr-^aqWbZetUZv_->e)25qP>P(q44nHHjwW;~+cg>tJ15@^8 zP8Z64KOoY6l9nQ9s?@mdl8|D)|NLpWZVZN*ImDCsZSdk_YfDPxQvIO@y8VHDY7o$j zcuP)JHYO@cj5AO7?^=~@J6wCv)|aNxe(?B$#so!;KqQnImP9)Yo@-#xDup=CTQkea zTyrMVU1n9gru~(VGSyiB{ZjBD$#B|Ab-Ej&4d6GsJTf#TrW$?zu@DWD=TBU6gi9M z51g-|fMKGhbP`%Bqp{`X-iKhZa%-hsIM>{{pr{D_&|_@Zo<~sgJt-;aW#sG8uQ$i| z_d{gOiX$jI@0n}l^IkqcY{S|2AvssHRriN*XtZWmAz{_tPeNAy=4oAJ4$Sim&y_5r8t&G@0T zp^}DP1M1p;!&|N|{O0f)U}3<5w^^EUuc+uUeK_vg4>!T(>*>_fHt;xEIrsOnNe1&B zkvGqUw4c6`ENIGW+jbEzPYuI?9SO#QdGj_9sG7Prz=15Mmvkqo@{Ic6eVy6Ae?QwO zGV&qZskA?IMwD$Koec~PVSr=0=Blh9*`;gOp}M*XMkQ2M6j}gJ;%pY3_W2RO()dE+ zhjx;m0CJMCzKi?98*A|dTSPMlr_16}y@s?Xr`5PeUPf`hRF*XyC!}f<`qtGv4B5K% z$yOy5mB`*>?gNpsG^nCVjZ=xxa&Yj&YOB5ilWqhvp-VjS%RLPk_Hil1yuPxkY%IdI zru<=Z+pkJ7F!;s{;>~CdM*E}jG>nB4Z3H1BUcsW^5JL=rjJTK=w=msiZ^Vo-TFWy1ce6n5^-<$@_kAHJ!kmb+j7oTxVSZF9|2zZ5b%DO&;m@?J$9I%l+^J=Ry-+D;o(riI<3~=hXxokZNY*Bcn?$m zkzwUd3sT-N(qn39fgJhnmFFB!5W%T z(52bNUFWFcZ3!87XiA94xX%GSRFDKi<=~K%NPO~LO!%=(<`*YL(|Kb}=9Dfuj!TzM zn)YZf$+lc~>GND%82NM+P%nfj#^tq8A)d6QA_W3QUk*4Ctic1Yyoy&rvSD`X&pv`H zdhGh8dc%gzfwCSI1tlf2$>!fcM$`sqMoG){1q|uB$@18uHEa5*s+KBGKllyfj1Df6 zytqxPWNl3tlu&4h$l>M7rUjH^h)^R2=Xe#fBxVEEPSaAP&w1%F_Ub9bgD|3jJYC=I zVjRNGdh(=)GIs8HJ^h%CDCHHudj}mXVv~AYX(PsNZ||o&?H+sOPUcG(LtbyH&r+mu2+Z0f*^1;ezP)r{L!MV`pK`_ zGO2OePR@phzP*1R?4wSRx6k^J@g~E;D;TdDz2I691XTG!2@*j#2e};px`+^pwraYo@NRL*-Cx-gj@gfPXwOo zD`<=w)#J?T;$n^G<8zEg;YGaD&EQz8*jP7ln{44RZ!AAUB!+JV^=`z15rH_(KNGGwjMv2#x_@97e9i_<%K zoFiQWvo3;S4 z5x@fY>%5rTWaNM6BLd08Xh&iTJBjcVtH>;j@wWV?CzyIK<$}ZqiE0@e0OtQLj1U6O zpM-A-_)_vLq;z_xZRqT9l^ERet1jIdm*J{rw1LAe6#baXP!1kdOzl$o& zaL?aKjll((WZaLo2K1NNCG)+k&tDm3-7hK{4S~R_zln)UV+qjp-SkXQLFh$X6$m4~ zZ|l;7+l_n>%$N?|6~t7G4zZZ2~Th`s5Uju^?je*-z zOUepVyV6Ge*dD5p(gNig`^fX-F4BZqKZi{MBh^3W|8GWVjHcMgD5LKU8R&RVWA_BZ z!Qw{p|FFxTHRfA-CJeLsVWsM>tHz?mtF{FB_n@EY^2(<3zjA~YffWpMS-kBLw=o5cvZ9l7mme+&o0lOrIjXW*od$Zs zgv?|GX*IQjf$|hH9z|fH-+%ldTAdGx^zAobMXt$@JdU#Mmi;d}p2g5o;eIK6UHBhMcBueIhH_xib~vwF~ERs`v0Zz_;~1t{GA8 zA~dtWp&THelNLC|?REcP4Xzm_84UAuA%uU)QHg}_KfYWmgdPaEZf6(%T7KhRv!mI6 z!LRf0+_}17oa~h!cX^7RRtItJ!sG=S%;s2nFL@CQ;t-2 zk8#&?$$E{06~&b2I?<`Z;llHh5>dhp6MOHSiaC&&d@qGV&U2DKd?z=z+6?yC;RzeC zaVdT|_1R}&ss zp1F*FDY#d`;8->?bV8=2rx#D0M1KtnYT z{51G%cL(G2+DBFO=<4O1V60G74S8Q*h&BW}b2_u`+H!_xx6srPk|x!o!CqI%VB?BK zi=O-#2ji?7+6@KF+S!8O;n8vT-36(xj9E~$&7M-DX6O#i1PbKhE&cG<)Zxl%`{lg* z;ee1ocz=KC;Le5gobY>aQCrD1!L>MX{ZH<_G?`V1S?*Tu*mq`2+c&BpI@<8R8SlDK ziZEm5k%L?}r>hVW%H7xJAHPFJn|>S2v6LEl3%wT&gLuAd+<2|*A&n^TMk=aH$KUHf3u_ z2M>GK2s;`cBvU$UO1aDt;1gud6~^iK?ckex_rOD|xkxA*831!NkQ5BPI^%vDqXa90mM{2%&ftMaX zZ7J)*)m7v4Rt+l<(eHs~%LYQ*hIIt61E12Klik5Vs^>_f@#7PRZ8wKYp#sc+zwNOB zHhfBk0X<1Iqo88uh}8#3El}+0JI-L3fL+YlvtO5YJFi7I z35bj{S!BCDa_co~>X7F|Sv7HTvc8!w;heI&&p5eSi?ZpPwr_nh?2l%zR0F4>7C{>K zcyC(6J3J_Thli|&0(HAiN(i4Qx?u6;|9S^&AbjZ-WxgXa(oz|=eOYTYiFiZ5FgbmB zUgD4~L2>Q(`*k`Is6irIjXiU}*@l}! zgM!U*N$~{G5A4-Aqvyc6P7>kJe(1~N^e2)~TTh&Ift_BH(!6=|?9cvr{AJ}kWoB;D zGBRIQ4*8?rs+pQtL-ODO>lCn4tQGWPsR2aCFDP8s9cIoP!67~U(hEI^a2P4v(ecDP z0yLV?MlHpuNNBEKr&bbCqLaalQMo|D6mF}@(Z#bqge@^Kvv2_CCpu2p3LFLK`0GON z6Y|`7G)=uw@DUBd^+~c?l-2D|4(+&YV?d{IHA^O8`V6KNG^%vD2q!66H57)hGOZb@r!-^4aMfr;+r%f|lQZ$qDp+hiudv|bS_s3_Q|uwfYt7OI zKsJHGY>W>sTl(GM>>;7VS3GP9rO|7jIer{Yz+zBbcX!SB9!gSGe{<b(rOe1Chec^hC>`ZG(QHu>cH(LkBka;>i=r zi$1*-c9G z9*wE2IQmA?dAZ~Jg~yA6x~a)k!7M>YBKoGJ%c$BQ zx8(}Jw3?Aedi>jdQc_hWw>dc!B+V}E6V(y}28d-FqB$6yvci@}Rh;DvrNNxO@?F-? zU&)nuIKQxvK&l9H&D?KGuusA`mYMqNRJV>-bN*6*pPGFdE#JF&GWeg3yEH>Wz!whB z4D6Yf8qFU3W5NA%Z%3W4;+zO_LUe`C@E0^ zZ_x7zXme6hu&D;@$bxU#_DQn-0LtkMuQ3ZR&vr{zgNfo z(^9r3n;ahy9Qj@&7gKOFdy1I;J?gLSl0n+jItY+wKrhLo`Kmj4AnU_+cb=<0bEJsX z6x;9FAgM_s|D`VhdvFD8eRk1bikC*a^ewgmCY!yTokHQ^-^y`uao(3gHe1k>F*aMC z_|(_wEpA}oi|miA#6&ZW=DU@2?*HMc&2fMcS55am;)tdPml}8)Ep}qfPfNH6?VDNH zM-GUifSaCtIGQzzyxBBwVnS%1yh+Xj5RWLUr-e2z`L{?h*BoxhNQ#w_>#3q5A%IqB z4j8~j1Nh{rGzV-JC$B5t@Vl}m@?73ktlyN0@ASS|R zU-4RHWn`%D(7y!*JZ^sq3KLkmy7Ej^N=OS1vY&kWPB-LUXF?aUWaCU~ z5qeg5)BG1iV%s7lY1Cj|O_tR_CV{|z=d*VHZ7%~)jjMlXGw)U+5yzY-3q$F|anj;qI&O%M|8K37z95J*)2e7OPQ zW}5#kC_`YVgXJ5#dswA&1SA6z=x9( zM;>8136-w&606E>NC>nR z>nc$gpVcQ-PS_GKHNTSXi(ZEqB>XN(kj2PFvd^-P-~E9x&ZETO-d=y;hzI0ECH z&ilBQQ`Sz`937UnzBgQ)qmT|6)>3qFbesCXqJr|v{xaU&!jTE`st@NgH8qt}WpXFV zVif7SDi~%J&!_ns-g3&*U$r?<_DKYhU*=4Y-?qu2mEhmXXT z00@MA@KcOBTt>%vJ2(An z)bHvOM<@}l&_VK#@T85h`oIMC_xEM9t6-aP1t1#)yuks46k!vScO@ka;m?(NDk+hP z9-Qya03Tx4xf&^o`KIwKa_rB^t@A+G3Hc8e5%qP+&3bJtDLZdG{6Dh}u9nrMsb*&n zu?G})pz;b)*3r@$-9DVRHZaNxKGDL9i>JyvhgfO7VyF*S1p&gLxH#SY?M6B}mPNZ5 z9tx&jUL(}{<^ERr>@_g`&CIt14q<~1T@zO!U97je2{{BynC;PvY!sbhvB%xG(eD_x zQBnd$nx!^x#qx{!lqr+D6d6UR|FH+3c5omH%R-xnEhW%;Nv&>N53ul)?kHJ?`AVoI z(kr{iySVtGt&SZDZ0luY9QMTFsEtd>M!{?kNA9Ew7*;bYXgTTHq}u6eIcM2bl(y(QqOqopun*nP8t_dR}**zH!vwIRuwd>DN-;MR-b7e0aIUYgOjTi@Nk z=+vN-1DiXfWgG;{cR3xqt@wJk(7)S{LCl1_8BXtY`%Ge55RkFnfAxkfA$>S z@sZEUlxV2hL^0r$%q4kwDS=U(cvDGAxG|dvB(4s|ClCvg2Dmn&@%QHt@7AHr-`Q*~ zdzUf169G)_XzPgsN5;2+OW|&u7=2{(pB2;`%eU`lQ&F_1g(>W~4Y)bpz`(ShgpHe9 z1&e^qjbmf*l!l-ELgmzZD=S|CopMy@KXT++CdQs~mSlJus7ac^j?vLBkj*?rA)0oJ z{svzf$Wx*qLD)ih!S@7_AN@r`UteEO?;AI?`mjO^g%1$MR?%ncZTSQk_C=Z|A-ydcN z!;fOd%sA^iOY9`$i@bYm16CU#wjj`;dw%EIm_68s4z+ZP43(<0rLNLL_}9$ese_zM zc3No>=E37}a|wo?gWWX8;DYO(6$CKsAMT>J!4=;CW_3GvT2rm|{NeMM37hBIwSPF2 zX!bBX0Gmo*iF6tO>GP=`K4S14ZI#Vsb{t#z%L?LG@xg z;%b5H;eS?ltOx!n(*s!itBBihxwbYr@bM_UHJr=%N*-Lq7?we+mX@U8>gu{h&z{)b zfco;FurSfHi(xtcnjDlaqk80aqzD;yb?iw7UF5xFZ^j|W&JO$r0Oe##$`6aHx$~R` z&HDy{n(udvdt>T~pWqCFFe1=OLxHi@E&VB-x4OBy)>|BZ-?9-^_YxiqFQVb+&&`la zDPy_#vZzNRn;3p$RfeJo=q(wO*znw8~jEeTa{>xX3vxFF`j_ZrgcG_um- z)qVghz+>(;A8?+!b%B^%Q;nz3pC`T4W1Ws<(}$YLF?nUYKT#0O6gc{%P5{Lt85(xg zU!DeT+y>kq5$mv2ZtrjRm05Tx(h^?>CJl6#9OCYa7IDG9^4Z|Da3M|*Z1s)LANbP* zv5!$C(|(i;GTu&;GDYtKAPS2Cbb4d4Vl`MQI-5tjtd4voYBNre&CS710@b&ASx*l|fJllcZlgtVftgs`= z){J)k7;c&TCUe8O$H%6E-1GkDk_P{z+C4-N5`4$zFP*R4Mx@8^#<3c&6WoG@ah95sd5^L=h!fW%uwFfe*qc9yEa#e|O_}2gh zNgfqIH>Hy0O>Idt-mS>wO0=W0!BRWdRbT6g2J;LuqL}cp7AVdRANb(K-rniI6cIPi zWTLxiPl)bx;Gp%95fO(*Eb9E{J+y*)Od~mgX$MZ=;K|H)^r#u*enS+*_7nmO!9j-C z*$N^HmpdLqP^asj9sD-5o3IMsWDh)A2v&Ju@7`_u*#7*<&F(IVXMSE}73Nz<)6*f} z^X<`AZVkLshOY%>kM(T2Yv#4i)=PD%hQ!gJ2&^1aU;x7{LVy!s#cf}TOAsv}YLBZ7 zov4Pz&SPNTZv#DWiNkvdy1}%mQ(tBW2A&AH!1@yPx^zHC-`TM1NU>*J=-I>8z$)~D zDw#5NKEf7;6d!hgpZ*TW`p~PpBnt%njeCiarvX_+D*Kqdi>cm^s)(W;ePJEkkegKTN60NfwtjoF%=JOn6H0) z=1{+GL|)n0Y`w5lFl9`S+w8$FoLbHpCT65{14D;vU=q0be*qty`A)qS*Dkqxc>qQX zJIud4JXud&iy}>A*w@In>-in)X8JVKrJl(yw;5^RJPIXAc(6}DLAm@4FD8nu4~NI?TKg4 zswpXXGv%N^gGOaN-pKaC2%j&r@%804hFzG#~TgR-z3!nb7-n1X7)wCJ-%=Qzhjmljsbh2XYX`;8&kP>ow9VT+8`Lwc32n`BgQ+}4F z+w6`*J&vC`RSnN{rbkD?m0O>Ji2wW_vv<}BOJXf~9$P1RUfC3J{`_j3P=ZXMBpoD> zvDEo`xfv@V;pFs1QB(ep3veOUIKY^Uq;CftXMy2hX0WV~&Rb_()4q!VzBg&%;EvoxSskrGVoq=2k{11? zabtGQNyE_CGiAa;)5$t98S$!SJhO!J9q@GSDx053xr|{Eg_n2!yG|}~YgAA8(GZt# zTxIB+KzW)*0U>Zd4dlguG`uO<;Qfg&n812O$Vjq|5;tfPncvA54 z*W6cTYGBZZo`}tbO3O16Od4L@Z(HELdA^B@vs*jt6?8a!R3+N`PV^rgVL3VP zF9B&lxkim!(8JGokPCsjX{1R@J+Xac_66d==WaJJgE)}2yMBkGLI*0}@bhc@rAwpsroQ}2(>$n`Eiy2QfQwjJ zJl#cO78=^Qz_E;2FWSB}gL1nuH0c~NiDZf?o}TqA0Pd5B`{UJYU%bnN-a$VlrVHFT z0i7zRG!)c=`-+j^*Z%suRg2~~w}PCMS5~egG72{(&e+MzI-2cayays{13#VS4#dI=?;<%_S;qEHasI2K0D_Ul z@UyR(!gX`elS+O1aqlQvZ){q~BfQNpKPN|H+_-BPL!$-uISaEI_7GKJLPDSUUfqPF zyzuVb$;)$`HLJ3=7IhDNV!0t{V5>`)EFrJ6ZNdC6%{SA~x3Jb#!U>xsN&{NjPh1rF zs@`57Xcb|qtLQDfzUi+oGjpC{qFr1}3_mWy)+YB^3=Dph_=`F2&>x`**k_%eewBU5 zKm_nKI@R&PKHn?vgD^d|}L`Eym4qp!tG+BytV`L^(b=@A<u?Gy&$zTbym zv7upOH|84-iMy6~^(s5#CQ?@{V4$Rp9X0BI5q&U5eb%mJSN-X!cd{KWC#VvzL*@JT zJVmS;xUj{D8XFs4{DujK@#g1TJ@&L$vpwH|jSJDAOfR**eZS!ggzag|?{yTENl7EQ ziqlHs|Gm>%LuN%l;_jDTN(6zl#an5KK!XmHOnMsfkuiX4|oV zii)=nt_d;~WtcOU@IoQe*jO4fSqM0j;~mELW5$Z*nw6IAfze$e9^%mh@Vw{Z`9O&G zd@ZFOV(H0VVeeBMsF4x9a13*#!jEftl6Q9ZlP{EO-`Mk+e18RLz8(=M_i>pog$k?dTU!shfq3x4f?M1?gd~y+EY{U6h+qOP2F?+F- zEwITV{&3qg+7eC`U{eM~-6cz&KlkMrNlUBF-RbQ}gF}K#Q4zjLq@aLBItF5NTEEGXmBRD6x%83|Bp^@imJb7|G zj=>1KI0$D~F?=PiS7Gbn!+j6?Rxd~z*s%}C08s66xGC|DTEt{B8h!R`%!CQA3ktdj z;DeoN%qypU^771*5*>aA`fBz2&5gobYE@}!hx5(oo#9Jc&zKQAEiQty{C>pppCW4y6AT5_!Z+yfBz5*w1^{HU5wkJ)y3Kz_oa|N`I_Nk=0iI-_}elAK) z{a_8g>!fwP@d{k;@2{h+U2(*YLFicCHKtr#i?l7N9~Qf_ijW_&w~Mp} zu20;1dli#Y11EDrKNg%}A+YVCCN~1z+DQ~Hrb77@71TZl-X1do_3Hd*3Xl+EO!_PgMT-_@cLvkvk#@{V$zH1q`*Pz%I+ktiKCXd=d1CO+qhw;O;T3i#G zt+*lP%-P3q5ZDnid!n7Rkb31xzD$Lc{`@mtq=0GeB8^K`R1t*z&&KHMZwc`w^m1t5 zZ37O$z6DFUe(|E>^oBuQ4%t>UtnX*C*~5>LTP)Opqx{{3%gYuzGgyVb-|UCy__14j z=*WwP}!S_h3mUze^L&*6SBk_L<~ zEKcXkm!dt1vSLnTk)e(}UyKpr;CuTvtFGv49epKuS|xB0vNtDegJS!(HB?T4=mrZ7 zaL(EB`QwdK@4GGu1F`YY?E3m0zw=?x6Jy{(OfDy%>A|<|D6oY8J{~FVo4wT65LM;E z@E`A_o%$h;a2=q&<)h~#1*cF-PD;Wz4(_X`OUeLg$y+Si4uWUry6{Zr&vnrSA1KBc zOavC1cSR(Gz7DdWDEwj8#kn0|Oyt=0^z`$NX%aX25uRC-X~D6-#tQ@Jj>r@cW||_7 z7P1F!Yl@?6f{#ov8VB3d*3yEIs3b#aiYy!HKuPo(C<5VdV=!23b*c_H^rGxT6%MI9SyDLGZpo50u?73CDw!-fxUe|*f3%K_X$Nrt4F!`3P8J(3`8Krl8uBzid! z^bO11+n-_a&w7Y2RJY*|l5Q!#XXMPiy?EqrD8B9kSFK#xhpLrZn;{~94dGA5%s~JH zRKJK@UA!#}-jHRri0aXi>;*Uzdxlp01(T%%f)G41-ecv;jV$j1(Mn zhFmkU$a7Ca_YIe^Y$7pDs^eBK%y_d_zL8BpY7e;W8%A&<8S0>^2J{?v8#UB`>({Rb zOKm&zGoX*@1wZD>RGY?YKUkgkv*~@EgdjYAV*bAoup2Pf4s*pGw;oCSf9JnmpLfl7 zbR6oEH9<@3HsV4KfzQtCS5jP$n;`xP0CwWV@%3q3&3#=4=|m8`{xXXqp)1!R2#!V@gtEn6cQ52^Xj=F0-03XTGeT_IgMgDf zoe3@d$|v4naN=~78*!0 zf74Xsb5n1(>Z2T1NzqKy1yjhbO&xy+7$35|Mg`EMWbj6E*l!VSgLu{C!mv^hKrInR z#IxoO*@lhjt$~kl*rx7Z574*czj*NA!quzKI~uWhI|)Sx#+-R`3CT$Z(cm~^xpHss zguQ#8=1->jb#rzuc;S};&u+er&6)6sN>T|7ie+QGeaxA-vC@EHD3tskjI|nmBa=&6 zQXu6tIhxfAXJBOfYoFNIM^#{ENCuN0c8e_p7P3%wofA$OG24FO!n5q@6#J58Gai2w zX-I;`(4k#dqs(MbP1F?FmsyvX0ubk&@D=4Z{BG1%YoKq*oo_()0>qALhoC2wImh~O z#7-9H`oM@-Q#oD%%x6x?)sQ#c_z1Z$?9`BF-1LF~;Z9~BSj3__m8r1~491X?e94KD z=Sn=={zi`}#vD}o*pnns#d6v(!{N-Z(^TA1oZX+fTB-a5Zl^VGKz)-Yjfh2`O9mr< zg;3bZ1z?=j-iGNr*8ZD!Qu%02=P=sZWqEmwa_`>S>!r9Cim? z3eFTIm%Q)Cre`u`Vn0Dc6Pa-vY1OzALMQ&EcjTqR0_<6vYnERAL#3xEyG5bVl$9Oc zbhNOr!joo#KO6v}?O40znw3#jk`CvvQ|24eOEu3m!m2!2!^wMtn|dWr$>*3xb)%-V z#HigI0k@|AdzQI{g|Zuw+wuhKzkiQ$F9J>ZCqB#Vt4+i=lB<93i=Y^4IBvr0FJHf& zCdOltNiBJr`vf~{PK8vdNp-aphbJWkk-I#oN^#u=s`Yz|8$k{2Ldfpeab-{eRZj;Y zyS>e5FtXjRPiqy{!LRGS!<;V<$Z}G$rg5jaCMPJ-R_GIEcToA8-!^ig-ngMdcOlt6 zHM?otZHbsPH4xc-5LctXZ*D9c^juR_T(5a@9YQ46VtA2S?9cWI*YDGECXEn780(({u zibYCw=1mjLSNi91J-+-%0*MB^f>e28IYZF(HGzcNY;0*BF+uI2HP)`PclN4H&!o>1 zwg6mShh&W>3Zv*9V+fq#ALxEuh>%AR7G?$X+Lt4v=V{Q(zfY;x6r>hZy|`Kl0;jTq zsG#pCkav1^5&Tc+#1#FhQZgZve0+QubaWlqhx92t7G{jn$np3pE_|OY_+@7TN=7+e@CKxC1>>WCmyCC&=oaM6#4nZ zPN?HT&wp-rCz`r|j~*`PTRsq`vC-UES!btZi*+vjhR#4>%R;?on`%&7dI4mBLl<689nguL zUJwjcKiK|sDnfEa#U%0~$S$58w}1aqCcHZJ8!@N@XiWZXC|7?gZT& zGY-~3Qqw^Y11UC*X5K`_-d#!Q>BquW6L?vFv7k~Nqt#_vbEle(i%67&zt}1L27o*) zaPIi#-@kVZE8I$S>k694(lzq|(#Ma#L1Q4&6@)E_)ZKbk?7#wBL*6dxkCTE+`@Nv%ynsZLx+x z_y^M7iX$l`y-4Fv2rB6wD8hrsEQfhh(d02>gn!d#)Z4x`F5Oi1g;uQbp8$fwv*CAg z)*|y4t(4>sG_-2O=T-23;;uf_Cmte>_gZB;A0Vdj~G)Y17 zC5#DvmnfwBq9MKZ$Co+SuS`godIVn}vE-!Z<={j@EV8#OA0EZV@>bcju9U%NGp@3S zIejrn!vk{X-Advpb@cTWGD01?a@IMXmI_Z!9uQOD;b6i58BGVBK0x-#RK)g$A8+Zq z51K8iOqCO}npr}H_wQ2@fc^^L+8wr2{<1_7O~zOK!i-q8)LRSIqse8I2r1>bb6{4{`frN^n#LUU61Q z#kB*t%~2cgy_8q+?Ywki07w|b0=k}{myP|YGAPhRl0f{t@-!4GU{pYSPA%%5>07h2 z+c_mj3xTlIgv4d%jZF2{$=Hruy^RgW67Ch`J6y4)9zR*I0OPJ6(_Vlh)gC6Zb%t1c zp>{0&)j!iIEiC7*%+el7_J`2Xl}v2>^=pma=yShba_WMvK1Orf)9!*F zwG=QSci9(i9`AP_59zxF?;Za@Z!KK<&Ra`Ljw;=L^QKi4ex;;*Hm#!*=2GBTE53ZH z*yQcy6+L`Mrp$_)cDz)}KwyAvNJ_=D?Hc#+?jvqBor)9dC09RDW-JI*XJ?XI2a+k% zv7cAlDpn)5m^6T6Ir)HD({56>F1z$n1c66=;%&LlSp}P-oO3D%*oPW{f(IJV%3sG= zMN!_{@k5iF)x(uC0Pv3{X>ZXys8^=FBlgF}N>xhcnCzYdGwr|s2JgO0v)WN$3{YI% z#9?gn>cW-|cVywag+GX(TD$^EoYq}1g^=c`=`3j`YOEQtRZHD+^Wpr}v)th=8D(WY zWiLgge-A)+JR;B{N%=zw;fWwSLhQaPbX%_N7%i>E%>HY~QbHma{LjtJiy46~I|CaI z#&J3^1@}x=XuCyEQDUf3R5Z<_dDaxrn^!MiZfdposkZ#Oj+~_bO#n=4g1UA8UWq!V z)~~Hj@ECe^wZzF;Y;qd$Bz}5CPFlJYS_Ykkw;l+|!nD1I56dM7X2IT4_86*T4!h>- zu<$<_fTq#?^C1p$z)mnc8m@5RR>nU64%@=Y$djkeIZqs$I zJF0Z@y@y-?%pkzH-=U`y2?a1S%VoZ;6{~qU9N%2Xr!R)foEXOzW~NlTa!A{5=fMU; z)m6I}E*|MNzsFw6NG4Cs*SLj<2;m~V3#A>O?7kB=+Sr7bu2+sNTyp6phx|!Nvpqnn zU;$TaXp?o7?(-K@{|H;)WhDKqY?7dgL%I8=I+)fQayti!Y)zx3g#{P|5K?w>XX<-y z3(h7Z`+Dlxm;ydg$SFdI7#RZfD%ZS340??jI#W}h(MCkH&2@5m;HsZGBI}JgR%9@G zm;*RoT8gY_f)c1-J&)3TozmlY=ngnEp}#!3o0m= zsW*I=CklOWq?~e+Crv`nXsBwDF zvJ~f3t{e_%em{4dR?VLzO4z~s4jewbnI@4hdVS|Z3`)x8p&|tUsj04>Qz7f~e_Vjv zls>hbukV|i*R!#Ci!>6~Z*>jL@m*&20Ssi>g9D^_r2Y47Fj^hrO*j|9FTdVIA^4q1 znP9qV)iAt_uU$(?(f$&^jd=&|B13&@W^gx(l`N_u(nX6^-3t^nFv^+C<~}AhR2=xO zLp{FQ)s--16}?y+ofuT2R$BJhoE!*EnZ)?OhV#EFj93H#f*--FT2t*1b>ztK@fwKn!w2I>Q-rCu{{XHMfbtw`-l4`P@AGio z&(y>2ocgxZy~`FHRDR}dvC{IL?<}|C!k^x@fgK;=0-_9VcRO7grr1Aj3imSg8jo0A z{XAX6^kP}`eEXxKx)<(~12QiJC^5S7Wr&PiWkA*C?8}+4xLtkawNb<$+ub;c@h-zM zJ6$DdNpMB1qjW5TAg*42eqz9$!28E`6reeKP}%U=+n-$wYy!&fy9U|{HaeyKy$#im z_gmeQcR}d-!6i2ZwYr|`zEU2uH}kh~TjL*|S3yS`chGid<1s25wg-~-w_Qf}X7q+@ zne3e9zuKTE!r@0@0H<$KBKTTThBNN}>`Pt`#4jKJ{_K5jya|2f(?8*WpD@avLLp>Q z_3vJN|R*!{xzo;=%H!@a1WNzbxlmt&`dn$J&i`UdQZ!bn*85 zJ6fA2LEOK2bD}t2p0<7J7%h{2BL2RHm>8Ziz->xO$fJ)R+dqZOnC-mLVF5<~v0VFX zbvyrYx$>L%*pacZ<_cNiw?kLwmdS^O1>(Yb)K2%%)#}TpN_QNNeV~^2^Xn0PA^FLX z^e1;y4jky+FJ!5(1rBLl)5r;joK2hxEO7YVuzo${Xx};i4_|K@mgCyKabG1Wic(00 zA{9xQl0t)`49S?G*%WGJh>9plX(ENPNP}@LQz27x2`w`V8JdVfG#DzS-rv=F{$Jkh z-QGSt+j>yl_kEq`G3@((?1z4E5CNKTO^3BW#iyPW_?i43Mn@_$&RFKKg7$RZ?$J|Q zQ?HDal`VeQy?w^fe(m06cJG`fFMaGWcJi7UEiIR5!zpFZF-+t!<;`>w&``ld6f9wd z6MVlOq1JbDO5{of#f`GH*=`T#5to>Vx&G&`)2rJ9sIO_ZoUjD8P&i!^K1lHA%3P?J z!eJ&$-%0Ap_O>RbqMff_MXg^JzEcQ-(?U2iImtvzD_4WAz%_l5T(@J4{=nj!MTP2M z6EbwDHF$5Q+0IWjQ`;wzGD9b&A}1;>9njH}eY2>E^X57c{Q_IRjQ6}E=6Q5KU+4&f zs~clu+RPC_klk~BrasKu;6EF8Q~T*O2y8r6e|}LfduqV8pDrStj!&kuskZ0syC7M) z*Ou~Vs&1cHdOeHYgVcSioQdW_^s1~6%Pl0nSNyQ$9?%!xkJD&5DYI(yChQTiHO;jja@8GJ>@2}zKYDz zVPjKaCFl~03%*1Y`M=ebCD|S5lU3EBv@qgH#JYLdT6w|s(o4DqM9nomr+sIYM!Mx4)}_0>6{1z`$UGV<+tk+ z@!8^&5rP0wpwKi9V=4)+D2BwCTgwd$iucZV*hdYfV9$%H$7n1Z9CR`_bv9Y+iW&0QUKqSHq1q(-r=6t#=&?uKXET^xjS^4=hyFlM;?b(#NT>FUGO+B9= zWUSoOx(Vi%mW|sat7Qse@A!c!VgdY*t06t*kD``WxsnL$zSh)G?1jQ<2X(&$(URZcQuq3cKphf=gbjfEQ30O7>9-^WcXUb8*L*0H{k@- z1Fi8IsY)^Q!aPljQb}ja7s{Zs2QwahJQu@ZZ!8b&pz0rP@kwTgnOO%U00TxE1_r%j zU82EjBf`TQ7N)?^&&MQ$fyc^K`c#j+r2 z|Ni}S6krfNM~dP~VI7=21RFZmMcgmR_tRR;BT+nZ1c5l&YR9rGvSn0Fi#bp{mETEyx zp$oDr1%iq1U9>5nKbS@O_jyVG!y*aM)bOhJ>_n4iI$yUr;5WDV%vEs+96d25@s~;Q zHa6QOxen00+Kfg(?CJAD#;WRb2&vF%Z|Q!=k8c83;s$Q}{HaOwvWFAIB}ZI5=>c}N zW7J3q)CyG1J+8eL*KOlaOQ(cNx$mDBj6(*AuNylqq`-Lg1UsCP6IeF7@;*K+2Yw*Z zZl4>7bE{c@3d|pPc{enCy!;i@T}z}#Oo6a#oZ19bDknS4Y@}`kWyi~sk|@=2e_z<9 zsoAm}lP^|1otpa>yj{ZFO!m8Dwg{2LC^0YeKlMzF&o9c&U(a9H#W7sSbRL%J-y`+e z5V!?KiIQFRS~bKHc&L&|)UI7IGv?&^jfYYbIUpa4akd-b(7&1&^2%d0Y z8Z4`ecPQ@j;W0FnlJy)4Mfl&!4qw7FjXgy`mgz&@(m((FX}o@L879;eM|&M?tmjY& zSJ68m{(FsDxTH{jyStX3uHa;Q1;*REgR^ywk38$LGRHO~$d>PEmctVeFk^9(+sVX!DG zlY7~&&tMP3>8t_ikGI4H)8__v2-`5iv=sNt?)H5RB!D*6Z%`=@mZS0P#f$27#&p;8 z!PpecG-xq@qLcR06MKAqGw+sc!=ZLNqaY>j!-f3`_)ayWDbrg)A#dvdjBOARa72}S zWRb7Lk67#ur0Ipms_%$J8a4-xe3g9Xq-@O)NW+2W5>Ga*w1EPn%M<*wI9GKQaX$PKxfqN z;qK!)qMmzUpM|)`xeSA5X>Lhj9R23aJUuL9diRxhf>o=B_bbbb zV-1cD2uUsxb~@mY#gLbmS64yYxRST*_jl&m%<&WoJj}&rn-k1QzVB=OqEOs*1VOG} zJpc-y6=pT!i1t-r#QFu*n^5h&8znCT_83GqGEYWa~fi6_ni<<3@oqlBLZ1J{~Bhf{m zh?U(eF{KO&y4y}RUM9xG2>R4skerLU7)z)JCgmB3E5+P)Sg%{hcCYkw#kF1Z?jm90 zb{x~)!tkBSzfj3?rsD_#^iMO#eQYWZlomwSuFMl+i^O|vZ;z9S2P_hT*basT$7Og| zuUD642Wb}!oCO|2b!jqiJnr zWtTMh{`(Eyr8l|?i||6~ml*a!v_+^K9PpL^BvOseD`}Mv8+jGnmgf=UgI?l}zyeR1 zo%uGiW)(%rb$dtJC`o0bOq+~c&z@7~kl{`QcC_OHV`_T1;vw}>W#W?qg&0^%3Wh3C zaM*zjwHnPa@qt@y57d zTlVHJOSK3O>Xd@gPOwvF2tAJ2gH z&OrJDs=EBIr-*NEUTHD$ci(dY;G}O{zC1rB#uwQY_(FE&m35ux0T(sZXzoDovb#(2 zVthdu=MO)SToW2LzRG%z@`qW>S1XKc49y~FG@o}@ww|Dlt=%W;`-;KA8a#Zd1NN*| z5b{V;m4o{?Zr{o;lM%4rdixZ@#+nhfz+%E9o*F9UQ}bf3KJ~-ulA{KMe^2t8KOIkt zAFF&u;C%AoL+FIfXwd|rW6vndJCUg#lt(o@&Rn^wBiv@QW{UM{-Y<>(*Dwf>Vc~($7&-8jE6pu&$FZ zzI^#o`pWC7Wf^?{HiCixrd$6L>2yKha9!QqmMQm+PRd;RASNK`UXmGdAsMz@l!=godk_r z&~&=O&);7R><`bbGQJgk6^Ka6f*(*Zc$cN<(eI{*kh`M7b#Bh_UOW7sv?;AZXSQ2I zX7UM!$^Sv-m3bQ?aI=pOlOxgL;XD4k7{d$Yq*Y$11X#tsW2%)^-u8LU_R8e2Q9JYV zs&9Zw^gQ|8=dK<)9Dpk0`8NsOTevJ#cnz}oNKM!P!8R>rquCP-4cCzo@O8YEnbkUh z`7vlCxCX`sZftFm5QwEXk|VZawN(9t?-aJcue=o8`y5C5f_%= z&^EeM75^T@!Ei4QOW9n%RtYNj1~YMWFe}3;+}$?M@&+7gqa-FM;#^N;B`La}aaE7H zU0S*g9>;0e{Ma^g8XBHaDJ^Q!&*3u8&dwynBD1ZO2<+iu3tBTd8|1KU*(HSWnXPqw z`5UCxQxtB#e0j)lpDQ;mZT1!hg`i@@K?nwR=s7Q4yod=S4ww@Xljnh;Be_G|u0ZAcB3{4kRhY}82P!cp(21543b zn|@|Qcj$x({*bDP=XVV$&AMbmW!Q{Myl48cW6N03KvaK)yNu3MMMv#}X&uy|%LgO-gs znSdqIJ2(v~cLxMQc`4Re&`4kB_Uee61gI zqW{KK=Drqdp0qS9iD(CUl&?++$wJ#(UwSJHl7Vn@?o5W^HMDb@Go#tY#{CTq?7(`1 z=MlWSj;(b)osX&a)WNovUYC)5V#xbiTpuk#mu#r9E0tUb*^R&^wJ7=sp!1J0mEsVaxc}E9lUA^Zj zv=fsiOy9aS6QG9icW7l}tRAMHWzacx}+FN3?Y zE+D2CQQGivQb-V8t42y7cCcGorR93`*a-O$AMd+yqbX9qfPespJ>$lXotbE|fF%GF zR@t9=7mu&N9R{Uj@O&eGHrkE2^6K&9eTNQRU;APtkF_?PMGd$-!g`=0rH{Z52)Uf8 zYaagU5viOY<{xO(09zT|W+H;)3V)Z6XYkEOj}%t!+pY+WUUJB?(OOg*b0ufYxQkJO zI3gh>MI(l_Y3`Yy`~)WLac)5a&3`%zXC zSY^EhU0Vli9c}CkXuNna1If;gj?8U_Jo}g*6CYnaARAU|e{AgAQg)&^p%aUZ?S1c! zxC)@6qxDOL*RwKlr-HafCYq6LKElCOC1O`$5dA`jdW?zKhUbaQp?Y|F;{4T?s?|TR zLS2}d_(i9JTCHH4?zV9*wdaEJ&W7gYnF~R@2bwg^s}jdjQW!S@(E}shKYP4k@!nw6 z$-q}eLecvTkQFRCaagp-*t=|ELPC!)_U;MvAI7r>NnTO~Md&K91#UFC&14H;{^9$i z!>B!bLZz47@cH=P5GX{|i@yT2c7XJbm|wG$A+BG~RQH&vlk50FGs{Y@nCpCUX^-#8zo6$b8Q=D={B)B(>-Gj}dS$+_B(v7H|!3R0wMLg9Fd(T}WMC zE(0H~t{OiblOKD)KsjDoIg5p9U_dQN{fZrqojN5-Uq!`6rwnX@sVS2!rf{O|AM8gP znwqwr$?Oj4k)jpMOc3NRf@w&rdQnLLf+Mq(2u0c6`1K3Z#8hBV3n9AwdroJ4AYsdik- z+`qUxX7lbGj*V?h{J!Sf2cJ#AaYJx5i@SkWP_&nAgonMdg^g&{|Afx^~fy3xDVZ#7du%ziO&H zHLiimaeHJAgqrzQ>hJV?{B5_d&xzcjQ*syIb|TiG-V5E5!XZ2f-Zbum zM!AQqt6o1wMXoFWmkx{OS2ceJUe&Ac7*~8%YT>$?@?0~=kPK^FrLF@q4gAWoW+%K^ z5G0^!rZ5Z{#)@zQg9|}HLG)zC)%^rEA?$-ErQ~Y+KyipD8q>{YCfZF)*?hE~dlIbC zJl%8BO`bSor$|`ELOqJ$2q+2o(C7IaK_H(k`a{+rcz?BK%TauO)_8d(Qhz>QADLQ1 z0b73^p^ZHC5bY_ruV$-npf*l1$ao94NQ4Sso^DYVLK<{C7^LvJY$MnI2(=+E4 zz!-OuRwvEKCCV4+4nPBAK~anGSdwp#JFd}UWZqQ>_%j0W2RDaeJx%HuaSAS%!(C{uunMsKw^sX$ao|-NSPa2W(or94-vF8ICJrgy4oTKaj#0= zscg$1qrT6OH49Jyz69`Qjus$HQlB~<*??bo_)*HS!{9t+M}FW#i{vWidaaoWjggPZ zZ$9Sc3I7sF7X8sB$`=^|IlI`eNoVKOW4l5^Kp4Ep%;-9UrYc(lDEe=_af6P4G3hUa zz~6zD0{lUYBWQ2Z-igPF^M~@Dkl*7v|T=RsMTQq|kHHzpl zPo0wfX7_F99IwYtJ;w9rLj<;&QW_@$)StI2^tNEVfrQDCpK0-EWd0E*}^a#H9loLedrX4IgHe? zWy>w`Hp?3F^DX#%9$DXBK6=z$P98Vqnj`z1M~~oX1q+RPo$!sALI2+;pMbef!@xu~{ZCHZ0qWfVH09zt`He zYuha7Yg7c)nPE|iIoMJl70%LHCG>AveCtN|YmH=I-gdL(7B zXvA}VQ`+Z8Shz+O1~5OeLutAOhN8TD@(!Q`s^T3_C&|Y0JGD8TQUa6`T(ogliPo-y zKqY-^>O}sCR|+r!@y_!ITxGMpCyJ_Y0Cvj2*|#P3vvdBfkwIFV5D-YV|N$1 zgSADgj*Ce4oMQQ8qpnA<&{?~7?c7;N$$?t73BBi!3Hy)i_yWsdnA<(1Q6AkACG^zPl>=9--a*`q)e zLY*@aQpG@}j<$ub00$YjEyjoM8?m+Stq6lGS)$srh|da0r{T&z40%_qwcWlDM=H@* zpwGPbHa0STTep5I*5Z11mzV!io|{Ww$9hk=#heq~T3Y31x)FRod0wP0hYjQ1Mh!4E z!m4oXx_7tap-0Pft*LyMO*{|4B z!9ccQVPR;1lKNHgDU@B? z^5bvp8g}Lbk7h9YLNM_2yT$|#F1&OL5&|;?tizb3`Ro!KKj^gJ7;A@Iunmb)uyh`N zp!=#_%Qzb3fO~iD$o##ii@-FT-`dm7&CRSy;Ct`0LZJE&@ndWY5ZfK>GB|Wj!v->B z=ZJ$DP!86~izIA_VH1&K^ZuA|dB)83LFy3H_M)FJt-l~}(93LZiQ+|O^v|8NYuq__ zZ0Xkjd21LQ-iCQ=mZ9VhegKC9vOD3(vl*~;Pli#%0ru@@;FeiU*=O4{NyW%KQDL1t zb4IT}PuZs{%{wzTeUYT84;a7;WNq?|SI5)R6xRGKjQB^(Ov17!GZfrD1lFO;qyv_S z#69TR-Mcfc_3V6iW06>}xos4qk`@*zPdf#y3ZlG`j0=!MVF>VsC(f^1+abxeleiJYIdGj^`WrA-J(gK&2+idBj z*(9zRWE;+?apS~YJ;W$c>ukq8q{afBeyhjKAo1 zf-fFf0sN=3HS;1jroT3ya~Yub?t2gruH?4+AE_i`WTt|LMo-P$)@rnH@}oCz+IWwI z@%M4Zo1an6+t}FnJPW+%o=#=ux=Dyl+Dp$&$W)Cc<=QUd(jcK=FT*vel)>xz;POk*7!&~0`^Y}CT8N>KrdibNW4 z-P0uT4==A$wH4#w|0$zneoa(y9O=MvGMBH1t4^gbD*yE9JUhGd@Ee3VP{bBiB3-$H zS35q+^&hfP$klD#jT3r;2sU#^BJ?``vSLbZyyxc#u)kp_QG=C(J7W+HNGn;8kX(a7>!f~4^0i9K{mfsF5hPANgr1%swh9e#-qHv zYuEhP&rrtP{yf2)VX!h8({9@t6ls@^h>q?m)FP1eu3BeYQcimAoM^iDu`f3EB3t)#b%P>hg29v#!xXTyZV5#{77gDKUbXF@RzVqtImwj+ zfQUlgix=@7iwWI>^X{EMoBlmxh#B!;olT-|Gamjqu7{Ae`l1%cMc#lWPAfDTbb})%`_WXG}-Bm@0 z4@6FzJGT*LP$n3LcGI)a;^inqU`PtByYCte#m40%0eS2@cn~S|(R1hYuY?B&7hqgq zto{HgX7UQIHzF&(sA$?F<_+QARi4Ps{>@7O8f$B78N=qQmO&iAw0yKnVE1Al#AYo1 zIy59C&u)jE>60xr;xaVNWNPt~Cyhh*5a%rK z5;~fepI0nyic&av0g*YnKDt0OVc58;r~=h1_3q6A)CT?Qj$g$}HqmriLOJnRLPWXX z6$)Q7dB~?t_+n&}f5A&r%khOahPTc|sd_dR+>G#;HQbj;0sd;}(5~Lq8?GZ` z4b9@L@EfrSE!`kz{TSvQ%U_0;PM$G>6V7rMT|_7noxBQGUSih)Lr>$kxA}Rjra~ix zkVZdvhoBsN6ctyq*^&M9 zG&qkY$@b{+^u-IHI}q)SzP_k(3g z?F_%bg}I5zgrT>T*vT|xIN262Ldw+?n)mhdc% z(N)3$FhRoWpiSQ!hmDxscq}d{=>o!mz#XVY3*xVFw*tNu$nJ;V;H(Qlppj&2J}h#H z>w=4%DCoX{Dk?Gpn^Q$m3;qDLHC{Z`KmGtQ6iXv!*vUTNaWG0>-oD*{e8M8NjD&yw zt$ft^s#~mlth?f5J2s{mU_7g_<4*H?5HBD9R*x@qDo~&8w|X>GjS@ z$U8mf7>dk+?Ci)rs)?e{;0`*m;*ON>*_fR@W#d~HWw;j3n9>F{o9zpE!}ZgTfeP}x z44vFC9Kz3`n~q)MQ`#)38cNpjo+UwEesnyYD>_k6@HJ{w{sy&*5o6`$qbUpW zLyqv|K&q%-N%KAC%DIbmvh8A5khu#slA0|mzmKgd#f`w>PP8KPhcwyMW{O@Q;@~X= zD)y2I!tevC{rXWSU1TlN*?v-?hj}>7S}6uk9z6=0ogM~}_Dwb2Z#;7tgQ#BE1<~^9 znO<8{4T(gwtT;2s3gKwvB?54>`sa;w_ELw|XCm7r3WGxAhuDmahg)A7@JE;8hY|P; z<0vPNSsLOO7`1org|lZJGdJr$_wHU^irWykv&Kg!?Y0A2hq!_G4FaZ(j4Yy{4^kA_ z)vl)DnuG?rS^loiOBi#gmmr}cQf9pUaFy~`B9m{=A&jt zyMi^8s*r^eKlO0#9C}n!#`4%>T!@QP8_XtwWxZx@pPMsEHo!Pc^xX}1n8x7fJU6!# z%wgM>OA2VIIq$?45C_X12PJn*%X`5KpOPD=7ExtdohfrSdCPTj#*>a?7cSuVZB;TA z2A-opi5%G8qO*Oelm)fI>^;sdE@-RvzMZ~O994q^s;YAB+ukrzsNfV3p8a~vS8V^Z zUxQm4TPJba=s--2ef=P0xu8cqX}Qg0Ilay&ZB+LxvMT>Dhs)($pxG$8>HSTLyc~9UTMs$kHS_H!)TLVu^qSaf--h zo{7*4>pAtO?EHo7+YgZ^3T0s#$y-0BEcQI! zMf(A1Y2~U_g&6?Z;=s49ZRMpf^7Em?hxxoJqUj$CPF0RpiK4yr^uk5rRG}V=9{654a zE+VLZ7b0bHxR}GPWL|PgNXO#t5>uT7;&({JKt(@?9rX6y0y{W8J<0G)z+r@ zn^P59_)cKpk(9J_2YS8`=i@-lB-)-~tkR56O!V^bDABK6&Jore+~(-KXe_o0(%zg5 z(c0ea=wjl7PythD*|dXKX_}IjhRpo1tJR$E2vT`@K(ddJ!=d$0V7iFL6e$6y+Gu&@ z+4OEK^u`0p&q1652T*EGx|^3LxcrEydR%n|V>u3a)7g*bI{MvyWfAFo0u1zdJ(ag-%)Q_wEJv zH(}|ER4b>Qtodx6SY>?l0Y|uOdL4L8%dER${EG(wZ#?iUA#37SSfAfgT2!4Y{j&xqM zX!l}40O|kr;r#C-K6>%vhYeee+q@u9Ht>qUZMyGDJ#j)r&@u>3dr*ntH#BF?j=}l- zc&_Obh2!HN?yP#PdGBT@PYD)b_nF{4R{YJhK=M9RRQk=EiH0|PhTChNW`0(D2T|eraJbl`Oe-46@*gYO+Ha;JA zQLA?Jz|6Yhb*rspmm!|}?!KRll!wVJGH3#x^xa4Lm=CrBqM$LOy>|h7;L$EvumX`I z7Dos6GU|SV0yVBUi^7?Bhd_s<#j7AmRqP&HJUeKeWf{I7%sBR9|2e@HBG_gBUbc!< z(`n$ZCR;$>(=r=RFadspq`tXo>;QTv+5=FXut=~Ruv*QqLVpy!gRZw^$0Q~Kb<@l) zU^3EfinH#Z^DJ1q?=(a+Ha@-y{Wg6~UJ>A(sLM4~=wkgCF7)Rv-&_-_S&<&bJj@Y1 z6sedhzX#?nWd-Icx)Ict1)kEw58R-qMDB@V?Ar&kn6_jYohk^vDOiuP^Mk;fHaNry z{{=RHHX$M{-0+&$pDTJ~6B^l~*h^OS@|{lC|cT^_>^!l0RcnJ$Va zm)~jKw(iTTaZ(nTL!w<{jo~#k9t`21>zQJ&+lg^X+wP{t0FKVfMX@R&5GzYwaXSey z41FTqn!J9mRSho=9~9(>ysgs=V09b>K?aalP*TDcCIlrBZbJ6zWZ9N>*fD)w-R8EI zOPn48fx~@VYC&V!07b=+b7E_`CJG+*kbF?mV!cyg`UZY6N0C~blm{{M-PAs*?U~`2 zF=Y1vVTB-fV9SBb>LLu4=`c+=Rpu}-w&FT}ORXnLyZb>qz#bw~GC%cHbcEN^3YF{6 z$Or8d3_|O_^LnQ=oU{Ii4w;EvmcCy&b&9$qE+(cvdVp9N=xH-h0>TAyCmNxuj7oiK zq#J?P`e*}#heRW}o;i0oDY!18uDWzd4bGK=bosImO#v_1CG)2qdiHv1J|_^ic)KCRuLVTQ%9EVsDYwj|44m`vVUDExPT?CHGuX{6kyL~%X zuFDbOFtL5aSPb?g3~=6NwN{{vCpw~EO?$d;oiphV_!);1g;&fui9RVH5iUgUt3T_c zt1wT8&PGdEFoNJf3?<4TfL+SN5n;He4KzbiUkg!LRi4gP7( zHx7ho%-$ZYafc!fF`BFwWdut2MI-u_l^{6sdTj((Kv&By0+|{w9KoPm-KI%yegtaz z@$+YKksEV8K%dP&Vemx4Y4axb?K-0ONqdM-K##+dcH-~8iZcr)$gy<}cWi5K7%k*KX4u*Jlt5<=Umom&GMA1mXIJ&sY@;$;t}QW)IbIYxZ-` zP$JgZad+9%GTl!GUi3)QmkF$^v)I|KVe*cxJ)?XnIeaqW(sNRY^bn<523eXy30Xv^0pclws375C` zZB<62=K8Y|Ehuh=O^!U&1t>yE*GG(vqdNlBJ(~||!}L|z$15_y$adX&OK|GYRPvBC z#&R^5asaS|S;x@Z&nd{$mV|m;(V7eQ`BW?-w z;vD5{idmRh;Meq7NCGInSUL#rQCga)FpiECFjOXRKQvr_H8r+B!TEH0F_x2CzWrh; ztS?GIK9$Dv(`U^(Kzd+t297|FJqT$u3ZUXH6Q@mP2tyQ_EWx|)Ur>6e{{o$nI*L+C zN^xa(CPOSSQD5{S>zxQ!Jb%8OJOpoof(~FJWc&9)!Tidy_WdM1T3tT5Y<#ghmbC1s z=a;>>P!43PAjLkK1XJIUlUFfcXNF(Y_3Qm7|0xK>YVtRgY{+jA(F(#)h}}Gs#0RZE z*=672%f9$xl*a}DdejCoEC+!;YgyDR%Mjh%h9?o;MmW^)F}gAl{)RevrL!H50u zp{Z;G94@%BImX532Nnpa_PVg4`F$k>J|~L#M{_~ERaEY*yp%rXknNcN(*n$bzNLLh zXS4Uz(@o)tDAWHpH}Mv8lWqcTygPD*_`AFMUj~I5+B~LHunef?@5GP}h>+-yj$~F2G?F$dP0}mgxiGQKq#}>1K`|hDuuLO7C|Vd-dDn+_o}MFCLMAE;=Cfz-Epo5)%|HgC z9rxFnGp$8$d-8Nd&+PKE{C<1oZLE)lX`+)uRB~$C@y8z2p$WT++&PT(x5fZZmg`nc z@~MeoGW3#e7 zOuw(PvclKwPM@@OKnCZl8(J=Xni;P9Ej7;4EmzUQtwASvj;Njk7J4B zC2AjvIm9(bKqw*R%RY$K&r@`A#dV0Rm~6N{-d~*AsPt29?}B3(=HR#P-n08rcy2?;x(Z89bP9cwiI_KKP*g;(@L&TYIc2v{S^Th!J z?QNg5?3P^f(aeArOegV9By{q3)Ye^u-I0;iA3lhm2|0S@x1elos8rZcomqe0yd6z} z?QwJZVV?U;Z4m?uae`R9pD=3Y!*nR@7$yiPJ1Q?p_f9EPL(W9H-0frp zlg%9fP122^XJ<6ZZ9oigdb3K;&@5b*i6T23X5dJVHUUXagQmefx7~a9-Zo+ir7w1< z42C+`fg&HGux0sI<+zAcQUxpDtIJR1i@#+JYdHG+#jI1mUsHG6*()PuUpRmM&Ax7Z zcN{-1?$t1r_cyh+uy7;O)O$-=_|?I*iT*Mr2YO`OAnZ_|&YU$X<#gYBl6z7D0dJi1< z2W#Otwrzh?48YXBGb?d5nB37TH)$``r<}U6f;Wxh@VMfelfT$2cNJ)~-XG5V`6F*) zI+Bxy)lXO7>CqaRzIT+eHDW1g!SDC4jHsx4lt#_9;zC%uO{b!hD#2k{q;~fEKxB|y zuKW_UnL*?93=HDq;*LH2NHqsWA_!Cq1=424y`tA`@IN&317;>*IDCnfLTznDIqi^g z_pX8={z-WHf!s0q;!%Ei^*k@Ha||~N!qnmTgD6y5z{d?7jKeUB{Zc7RZK%3) zUX{*TJI>ZHV)t&Kn}(NOjNT>M4MGQUYAu++>D1JVuKB^@+=b)O$=jBYWX3EI#BCC^ zb$i3ZB?LZOUTVQ#M&v*GGKOUadrqbz1z-1*CC_oeIcQzVMM1kV(+i)yY-!mz2T!E3 znDHIYL(tW=nK@H#iM?`kWaME+V6zrW0WFLeI<%;uAoXbm7?mLK&w@*D-jqtZ%d(k1 zDk>~*y}PxaPZ1yn7e38}ru=Br1DFYani?S}-<6b^h{UB+hRWi$e35**b0<%GdKY0T zS|IFAdG;IC5y!RF<-1VrWH6nhbo30TgKQZl4on4e}$=t^n5@*2g z%DN(bPJ4*H89eFH$m7Q*s6U|f5N#(k7pCG! z%xNucj_nAnU*SlQA_7u3l7;9<*rx^uz`ft~zME~*5cPednG~npsP{gWoGh6A z_TLeog9eL9y853uIeC^fz+R9Y3A(v8xF`5Rk94X_i4TnDP!;q0zW$vJg^ad4UzcYx zExL~DLzp;ZNG;EGLj?pr^9h%UNFMoAmi zuK~os=MtZ!j&Ex`Jm`qO@dzv3c@VVRj4aGN{I2V*$VFb7ZMOMQ%kQ8nX&DqRv~6a@ zmv(Ij20*3^7VyLrmxz{tCr>h!%*OYjGY@rL#kUU(Nl6y#Dg$x>B3(I~t>MR$hoI8T1^qgRXxcN~2-;bMm$uz+*F~eJCm~ zXP_xcAtx?i4AZU>8_%W`+(L)b&_^EoZ&9yCzY947uTD96_3G8zH*eDGHf-K4DZrX> zo*6S~f!G0^4>Ocr%;vVH!l{(H`L6X}|HkTp%{~MXsryoX3kuhdmB~7X%?C;i_=uir zH2X9>t-pR17!mVyBfys5jj#1$ik*SV{}w9!JcBZp>s+?G@1Ei}nZ9f(Ow*CZ@Dw~4 z_qlI7=~R^z6dt{BW2cs9Hvwn^7)Ie-SXwf(SRZm zB%4}UStVB`;kBM4ACXG7*;ZBYLPJk4GCD(&-EX2d9MW5S*+pcYE=p2Jma}H*b$~YN zEY3YGB0OuC*M{2KCmCtB+a!9b?b}Bi@_B64Z*1L2T`k||Hf(_6IPZMBJOOmrjzOvm z*%}{XnXXaq8J0ZfSD(Bn_4Xm_xh5zyC_n3e0yZ+mP~?sPhps~i&05z>6amOKO3rn0 z>G=3mCs=X#@p_|M47`m0!$oE2Du56t=%K|gNsdxss1Blqvy=a2gX6&H$- zCV~YglCfYDR+ah>5MjTcT=xEGszH%ulV;<6L75*ii(}+ES7Ha40)CfxQiMJFMp22| z88V~vJ#+n*vYr5!x|EEJ1?;>MlG#>~nkso|w3Nipu_%ms*TSA;y|oAy-if=`SWPA3 zV^=@53<;ioqCNSEom2k%4O`nwl)L+#u%4VZQJqKF{NrtHZ2+DMI00U`hei)=LxDEk zZ0?dt{^Y|>3;rn_1ZC?(Ggp$y4hBo^P+IcqpTY~~bR0c{^P2|5-~Cckg98vfyf{ds zY2dgCrUi!Q{7)ri<>Le+J-r?Vn*0H{I~8$UeVM_?k$a+|Cc>Lv9lg=juzr({u4B&E z$I~0{*Q~z70P*v4%@^l&>oxEQM)-6?efx%9J$2#)+SleL&<{Eve6_`?&@KW^{k=5x zF)O36ik`h^B=)kI(!@gJ>Ta_+ap(w>=n)Yxuq`6ptuXZHHLE9ENzhxjwt~iCnaC!h zp_F2%iC`MUQBkjp*m|P5fRg?;c^3r%vwbix=`&=kALCbOys^jZnKSQUosLnUyN8F( zB8HifnEKGdghS!$9$yuT^2_1bOD#1zj$%Gs7TQWNir;G z$l-o%{p_w^yH;Q9**>m6;))~EFRO3fz8#qE*fSy(+GCnRNPXb#4QkkELbrvv95{OP zfqVM)e9MUxuu$l1VmZdrGLcenQKAbQG--=ZQuEJEnEd=;0v^iPUe;>nOyor}f;gZz z+wsF}WQ6|Jr|}8W?1rt0AB23w<5rD6*}4J86>rX<@Rm#J#HQHEsryf;sxc zS+h{cc~Jo>XdSz{bec|pMyhUvhD^$cH!!7)l2+E%st+7kwjk@Sdpf{J4jmZHgXR{S z!H&eV2yQ6$8*%)#G~8<|Jf*L8IV%)NF^OnR-i z-c4QsPeG^YbdJ#Q`{0A`>J%+->9A4Q7<_wJ^S;ZG;w=DEp1nMcwz7^fVSVSh+g{V#cuCP3E`l%c9~%)-D%zYyqy;{?UC~j^hsV=h~`M=Z35T@ zwVK;1_6wfbz55PCtLymu^1u!nM;v&6pSfR-`@x^k?gTWGa~Z7|c-WIB!Gr3@DZcdF z%pzF;;I40M$^WE2;O1y2GjZgV>4^EtMSs$dB;&PzP3eV=P5WN&;o;A_N*+u#@6emu zHQzFZGgpv7ph{kQ&Jo3s3~Rd_dQr4gB=-NtJgcibMqUllML4SH>FXEOy(uVg2PQ2w z_3vFNp*JHiy;dn$gDx9VQ#XQ%SmYxtiSNkpO03)Yjn;fRt>|ucqGq4xP1#kcRu9bz*B>!fY8C;V2g5~s zLNztb<&3|V8Lob!g>2-L24oc}{#1?3`nk!v=_CIIkGk1(sm50vRxeh&GfpkFyp3TS zG_L;^D|{PYzOdhuy6Can{0SWf&v*ChxbVaH<%NT$UjfrW&`j zG_H7D_PTuRW3$=&%=GRY8@yY-rfP9)G_W!bf8p}Iw=LnM-r2{bBRKG|rPJT(n{AU1 z^7fQm2?Rvi1TDS$9?}fJknfT1+EvkkKLtwCf37_B9yk*H+O+fW<}##ytg5JV_7%Bf zpZSBxNO@BA_w*2ZU>@W`;5le1Y zynK#X2bQ7g=<1$+*>k@TFlC+kAh2h;OiYtn28^tu>A~!ADeHMr_r5mm_h{^f)+Kag zdQB2t<{0mZ%cuirFrD?~bxMV?-H1NSy{dbKZVkw@6t$~b~TMjibTCOK0uuNq`Fkjz{r zD_K7&IJRGq0d3QJD5>(6?^76a0HJ(dbwd1}t?mdO$v|r5>*^~kO72SLtQM8{pu|x9 zckeDsUxauHx#5>YWubhSuD?X{T=xImdH+ZGIa(VAtQY&>2X}Cho*BFT+{KlEshPPH zHQcOK=$W23wRKB&!e!_au^)L7XNIFQaNL@I8Igvc=zd03@R(-bC@JJ<;L-qt6_eCm zDwuL&z3~xP=?(po*=hOmXk1|>!!huLxX*7*)k&0m)DXV##?4<&r+p-9jFYPv)EjEN z$6(3j8vdRqiNbPiV<;_eHrB12d1hQ&e)X5sxKv+X&#Eko6Ftnsx(+oED)bhoyNgX4 zg5;RPhd<+b(j3YM4_WxUyu5=nPcz~kwomfWKGqQyQw1Ve815vPX!B7BF2!`aik7gy zG=Ws*DT~5fL+k-eEo=B7C>i!fMiQkKn=Y3PnhD?eys8|GV}Mnd@j4ZueiD=9llHu` zOnG$wzW(UZ7t!U`h60PI-$v!Te*LhKBm2roeTmf(grEn~EB!4_Z2&I~QP`PpF2j}) zI~D_dzn4Y(O2)Pg+zcPf!=k9Yd5T8|ovUoRp;uuVrI_Nh<&i*(B32Hfb7FR%+YM-C zz?>dikXEt1V^VbN59MsAqr^Y|1f91FSh9HW1i*yH8>JL2fkhC{5v<%NxZLouwYENi z86`s?Pr|$KSB@iJ0DyZ=#KVth;l2|SmfN?dUCFslUd^HdMN29k)Czpjdrp-(=aPH0t=MDLD$!3!dk`9jKT(+1){ap9#B=~CbJZl3 z&_t>>O|8w{+lVY3E!BhE*DRh++m8Ab(p+QZa=Hv60=>4^%9Y}}MDn~B#VKVHonO@y zT3NvL_@*Spaexz`8F*90HKF~BJm1%NL)C7U4+9&D_J24$fS}M`PTuxQp= z>GT{jl)iiJ-)GK%tsgvcr1s;-)vH$Z!>5HIjK^^itH-KF7L9Iw2rq^=|3Z8FM;Bbo zY-jsm!HgXSAIpHr0VS-S1W==(V&`<9ZF6KaD(%TzMkO)A^iiQP=@#C=9FJFo<#_jcQpF)68{XJHr za%Ra5u852Y)cCCK@Lq!704DCFEBx=n_ifb1H#!M#~7^voNkCr<=(o zq78GPjo!b(A^GW;II5y7Aqk1w5IsDMX+qUS4Yp|5l^(VidYUc=kqc_Z=<+aeQ`J zR#M;=-2%`>!DqgGOU)ymFg?;hI(e#&bFLS_U%WM zUsu?0OR=r=j)=w8r>FLMOhS+Fy&}KM#fmto+v`*8Z4TC{gnE@_T}Ro1&-tvf*|j0C z!hO|z!LNTUQMfZ+ho;|T2l^|P24IT?1kGdhk04y`y+14Y60d4W5qBgP&wBZq1nQ3X?|E}?Jy3NEaFJHLe8|b zoQkwn9!gcd2I;R=f>!3|4(1KX$}$+8 zw|*il8~3nwq88)-s?nQKFY_C)oOjCJG7f7skY8TP@@dX<=#v1*mg0(Wo5TYI~?X->EZG)ZHY*qm_to#Oa}!-v^@4%O|R1%j<=7omZi$C%_tf2nYzA86iaaGWU(G);a(eCT8xo`GkOHwPBZZPEJyF$kh#H zfP+p-66Twgvt0xQ%9R?=Zk#jv7h4C8Ga6Xv88Q-bWy~^(Fx;3-K6>ps4$u)!01e;4 z5sL?}es*>&aUIgJ>htGT{MN&rKJhOZJg3bk`|jR{Q38@pCPapg9ebG$U)P_VMW|=L z9~d-B{}-z+($B3V{&>B2{DCxrymE7M21Cg6Uuw^OBgxa zyLa!EY5W6m7=r0Y(84c?Hq|KKq)igV?HNDqqyp5)8RY|zE@2m{8S;w@|M4TP<0A9M zYD?QDX=!Qj9R0-?tv;-}xxVdG()PFOjPb`>{ie^3smJ8ijSWpqeu+_Q0htW|_HoY@ zquDez#>j{w{9?`G-eghmtV8ULT|4+#$CsIo>`lc=e}k`Tc)}Kpme&NNs?}(y$b}xy zs7Rj0jKp7DFn-{dL>VkVo$G%${PR8Z>un)1Cl*N=ov3^r^h2FRvYzEa|RiB_RktQud9(b#~l2$@sFct$KTkJ`Fr3lZ^XGuWFSFbX2FBqOgsj~i2( zv9p+Jd_N8?hwn2-31g2nr9q;d_x)S(FnzR4@~UIe4?ehm{|i(TObg8bBIv<&*SGu_ zp03qIvEeu}-cOb+q|q;o#z+J;F#a<(t}s4|Aq|VKaYW@?mByc4di8wqs$5pC!-U)` z?n2PzdejKLc(#m@HM;^M#7kD?+)htBqe@#*VLO*28} z?n{Jg`oy}-ib5SadEF9#EL9>k)sF!B6Tur7Rv%*TQ!%& zBHD-@T8B#iaAK3Zj7-hzs|GE9?bwk|a|uzxFaWE(99Ya*{jIQ|z-o*?6Hxn%Kz0nH znsJgoG{RpT8D<otA4!Ar7H(gpgsjgiaRX?4* zQ6gD?z(l{8lHEq&AwM6yU%T=D)g}tBq|*;xJ+SQ;AB<#A^==h2Xu+1^!|Uo=;kyWd z7R^KV{aHf5Fe-1J$?`BK{z$moZ_(WzdGVu3E7*r*__D5zl5DZaA5fmWv?%V;`Cldz z#GZvOa~q7BNQ9uiDxk!B_B=pJC$68vn1Cd%jZM9^H7$p@Bo&&A(BM(0E+M?- z+Vf4vAwkK}aHm;Z1db#-i!w7asY16tI5L5ohQp(zWew4U0>Z0qri9SZWxDv5%$G0_ zf75UT_U|w;(HA(`?HqU-6vhnNe(|z&%{$+*4RW)69Vf0y+-|rKuOygv7u!tMltx5F zEjLu*x-lf@8SsP32wHmLz&bD$7qjt#(6t7;OF^)i;o$J1yxfe0*X@5FF}xt}|36WHm~^}EBBG-c{gT*EFdzKi6I;wgddsh0SHvg#SKiNY{E}pS zk+EoRyPkbX5ZZcm{=1+n%E0k``#vv28s09=LE+O{%Jn(V@NOM-+y6d6o@{Y`=T9+T zLg+-N_v!gLwIT^mx(4;0)oWLF6+HTOes|Di@(oVHk88cq6S`>jXaA(Dn|N_G&XF@f z)8Vt38eKn>3N-BL3!%p2e^1x?*khzKVtxRKWY6KWlwm$5ZLhnVhjr;#Dn15%I4(mb znYnUC{tNI%&&#XOJ0&F~ck4WxK?hSFd(h7u_&2;3zRq}0;QRf8@b7;=eM!{RV+fzd z`iB>X1TK}RS@+*-5`>zn|Nem>B&v750Y0d>ot;mGz`5bS10V$1_3S)J;^*-HbvZhp z%>VBXYaUD`$GrC|Q?8b!%&6C<;>Akd2#AH5-IqiTxaNP4!~k;YBneY|Od3){>*B># zN$}y5lg!+2$^fUm3`Ug<`A?1IJZ;)47Qo(TvM-<6(uBj<88No;^M*k2g7$C!z zA$i(*KDdMWGNEoTdE>(h8PpcF;HZG~?QpH9pk1#%h&^A>?4B0Q0o&0mB-$a?P#d#S zE^gSn4jCvCWcw6@=z^Jqh|wqK2|~_TUH{j&_({~gtUkiC9Yo7k4j}+?Cu9c;&Ma(c zX&I*`b-Mv@<*~ncJyW4ZI z5iDSkKy=}?-R~w+ia3n$3XAesI8_7$@zu^Bz_Sk?QCV~`qO8uP3% zOs7>xBt<0H0!p-U_A5<~nWZL(ApTI@-?P`4IouQ8gUZOT}(L`8&BaUSP!9w(MirwSE%9D0JhF_3uv^W>BU zs?4r8ct@kds0hKAxoM6;>eA)Q%e{3tbYnJq4>^hp0ZtcxXCYL#5Mv$?ZjubSrXGRl z=V8-*UICOC;ICB;^u!BB%#q+CgO9bCgm;(8UCJq>5YVxSbD8@FGs38jtSx^2{^}8X zCeDA@Gza^x#>Sb4D|zy)MlOH0Q~R0%&0Np(D?px%!FBD+#V##rfjhSM%*KW8L3S#I zxQstP=LoTft0zdMBIATi0PW`$6|{JsEIUq75!Obw-&Po0MRSLJM$Yc;7inR!u^p@g zu7q}*sI$<8sq8?)wMYTpys2-JA#Mt;qwQ-H&1EIo~S~vxGMAxJHTyia%|SF?K{eBI{Zm z8;g`0^S}4R_|W-n<;@6FT^|dxjHyR);9VbM0h>DEHCutY0PCXhB)ri`l7*~es3_=j|be}OXW@ywRXv_GiN>!evr1<1@2();$cuCQ=IZE z1i*Gblnl5lXm3`Fz|rMcW~Q~AKxB(8syXL1mrU7!LpH$CQKdvYwOJg?6zHi{@ zD4KH<@mFAL{%AbGY)0~=r$k$EWCX1R+6n3*jg_z7-5SkPRctaVd*p5rIE7w|)(dm0 z=arFkJ_1cS)hrdK(Y?3*cyVVNy$M-coa|;PP*od2i5aiG)<-u6NZZraZzcZS-QA_{ zqO<4b(y74m@SoLsGh*q-a6Vj@`;z=8xcfJ3|2|LbU;|HXSU%R_j=fx zn?I+T6?N(+yw9_h7XTZIfpBF?BStv3H?NDRAl2YM$G|Q5`R}K|E`-LBMN`;FD_V^$NxGbtZN04H^5-( z^yz5R3oQpCBESlUJDXz6CkD$(Y=FflbA{a&j|Qof1P)FLDYH}ShN-?$Yd=Ah0av!J z9UPw9YeDFvs~I+l!~>1_B|30Aj^Z?k`t3^{smMKKC}v4ML7%jR`(Y6Qjd#BC0!A@^ z@0(BSA%1%gADl^7X?%1mc=5`5mRUSg_x|4R*g160jm9WZ9`@ z*8|~M)VLy;e)aGnU4pwuLTYco1TgCQMPIHAEI3#NM41!DkIzbk2wHgRc%MigSUH{- zXeDsD1A2;D>~0*ZDKDSIkg1EU!WxF$i`%A}h#X8-g6ahBBK4Zic$|jr-W{N>NA`wz zr>=o0etVyBqC+OoM`e1Lo0{wmSz@?~Nzjej+Pw~$Ea0V+C!Nmv2_{*X^F+jBj)E#; zvUH;QEJ_eiPP`cU9O>VIQ}VYJq`nsZ#@<)MVGpg(su7wHY}3M0QB{TJ75iUZscSl~ z;n|9ZDp5CB_x@#UKv~dqQWS9-LXWenM~cE^2yC2Rz)8-rV+lcy>(>|lR5LZOWv?t) zCK9QXrZp{Hr+RkSI>f#KWl(bV2dD41It=lG2T1{$V5C~u^loTX7D2ktAN2*HtUB-$ zpx1ChhFQ|^24%m6)>K#1t4yxXtHi`3vf-fiVklPnX)VNz$8C1`wtf&_;jYRfZa)1214d2CExA5j55gC+I1 z)g;-&QbDZVzkdv;ssC2V(9Q4G6!1|1rpd{-2{e>}Z|t$d*AdaNtb{j0)_-b!_Ebad z&}*xsvewdRr}6Qya_fB2Lsj@?zsf zRPg(tQvAf58`tn=kwYYwQ2}`hxzE^G^(ZH8Q=HZ=Trid_DF!I4`(gSa}4sUj|3Vn7jj}KjkuLxZTwbK6o1nHZ5hy zT-wN0enQ3jWo1Iv(LRj;0-O-i48bjrN$|P*lN8AfxD2^c<4b&T zT)cPpZcbV5nXOw($h60JR<7>p;MMDCHYiwns4m$N@??&f4>lR((O>8TMCtf&s=ny5 zS4Cc$49ZktdhwDaP?`*tzEZXhS0wE1Y~-3?|9Lp3LY4W!z8QoLgb5?Nu`KfdL(H2O z;O%{gjwX&26$iu6R5P_h6GOq&;Npe8z9VJD0F;yhYd#vjK~ZBlZE#=(TK2efK&|{q zDY!%0b+KtZ$Bb7G!$S}T`SMfq=hN!)f}f&WzCINL6x;DZ9$@b=vppl6930|E zy!Y(+Ej08(Ms6_IDyT55oO(wv4F!nv=|6$_1K`gr>~D5y3v|uZ!9DT|o=yEk#HJ!` zpr=QE{l9-Cy=;?PRXA_rV79PH&;MqZzBHS)?e8gi)(zA!Ky*=i7F2%>3bo@HpPi_6 zI$B;VWvA&HRFv$bJ$~%ikycT6MHWSy_6Wh78PZWZLQN0gkVpa4c8jcahkV()&tXb2 zxXf9oQheBCrl!=)bqoyb+FSH|Z%PVA%cu2{xokV;`A$L>r_Y;l_fxqk6!|b^cQOgT$>L~mW1}z857wMd6_uSAr z-K*!Ot95n8Sk(rdY`>gC`sS*&>eH4`qY1F@{gV@y`%PO`3;-h#7daQyl6 zMu-&{PrZ`+TQ4WUe~I_wGP6WFOhl@EZ+D#CL^_B_A}@Mcnv)S|uEB3Rcc9>MdHJ$n z;qDR-_`2?V9j;ke^#hhC8brxHJmnvQTsf{0?~A$Y z#t-A%q506H(1O@ z`e7P*arIl&k2mDfoT7YcNx8wuauVQ*$;u}2j?yI|t;2-X^OD(Bkc|~Qof8bFe{{ZA z&Xz5`oY!M+FQ-mSH zx; z{%IJfVl4*rIZ3H|=DLMGK?i`rT>Ps0LL=ggjYl2&l(t`g@tSlfs&SW`q}X(Fa2XSA z!hPH@5oytV%y{VN;h(ROPMJ1uwixd%yY5dAh>#K3H6QNYPDI z#&WoD?6b6;sF`?y1hqp-pAFniE2^%|6WT*ZU-?ism#r=N>wA z62qWMbF2})HhkVX{vUpA>XQ%yn!nLI#oYgbk>|=4E7YcIdJYKQhxBce*-1>CDz6dI z%2N$@bPXvDxnhA(@n)zjath0eyB1#{Gen+FGTrPLOc-8eAH?rU_)^5!L<}%<0Ao2bC zcg8i04mrc5Ge*c`&yjBHoO&8K0KpD>uQ~TSbI%pDt~#Gvk4;nir%yB3a1j5sY4C$D+WAV-u$@xQ}*fr%9vb^KV@$OwQijK z`QN}CR5kGO6sJ*)jQ+SPp4m@-?H$730Nqw`m;wXa0e?u=7wM(o+D=uRISM@KEqOv)WXWEMK5NyZHf_H^~XoS(mn zh0iyS8XIqQm^uYr-VCSB%gCS^$)9h|EzMEp$=C#Uu3uR3d^1##fWosBzYZ1)`0kBz^IdPw>f%GVxwzFS+wm8&A3y?7A}F_62{MD%pru=*Y! z>;9)){5RpYRh~t}m_1v*-&Q~$&wi>mqL*o*9U0&srkIiOrA`}>W@8j50A54Gq@w3S zowROFegYf_ZKZoke6c}dF$~_!Rg+0`!nE7N)wQVkJ@bIFl0uyrS;HODC$X%PCbEza z80#ISQ{m2jwuEw@EDo8rWiRH9?kg|3ebq_K9M~V z9?0x=F1a3+Fc(=Gb1`1q8?Sg~rl61rWYiVHtuWaVSif(d&{YS9qreWX%~ti47d=J9 zfrL*=P(7re?17A80w~AI@vkV=o3nNmkov%u!Uk*gQBEqL7E5hQZJ0iD8;wm&lm-uG zwbt=lz226H-aQG|^j|7n+K$L!<%9Ur#@{ImhWsbN1&KiF9Zka@^m*oKC{#Ur|Ctua zreVJf*w)z6qFp5S3nhC&wAM5+t`Xd(dwU^N8k2|OzRlePWH&Z0tEhWMy{V}bXoUS` zP+7z-a(z}T>cBEV%Y;1L-DF*?j~iFaXGWWQBEr#jQ0^fL>e+MCGYD^%)vqvSv|iIt zZ1xb=Vh^jg(dS-81;ed&Gfo9el-AT6x+vjXtYED|eK>s-dLBAwCd2{`P`OxIBa`s| z^Us=eHLN|1@px&u!BxqlKS~#9e6BDBRmwFzHyt{1{}|-#%9W>DZoos(Z1p5vje_!W zZVn4;3c4w6uOIEjHN~wapX)bfe@f*EkW-HPnb|?b+@&1C3HE(N?_^NUVxJ&myxP_W zYMxVuDEb!N&=c+bkcnQsdbI_TKTCUIkk~AB3$OmKmX+9u$8vulT3X1e_^qy2^2u`} zv#^kDxau=>K&LJq+tt-!og+xKhdxkJ*u5HPBsZD_yfkoV!eksFiA;&i2z(2a1Pd=o z;g`*pO2#C+J@VYH0|)A(b2!+zB&k%D6%DP_X{%Z)e5QeP+^1X%|^* zqUoUxz9XFw1gIwha-+)p7{)1ECaW6aUz=!cnn4-0*jsP**7I|LlYw0ru1vWnEZ7Ts$#%$%~Ov`Fp0mDnY8LiXI`&2cKuMD;SA3m{ZC><<+8 z^Dl)6GOLu>zZ$#HionDdpLG<89&#-C#PHLI<7h8MSRNJa$=oM)D~H0y-4Aqw5${nH z4GRn(UN0@B%N}DKx3l%lBGKZ1>wFVdkdVXva<%O5QY>vpn?#F{?Zu6dZh%b+DT>Nd zI7~z#g7$C~mB$~4_7a&?f7BnoqjW+3qp2|uMt+=3HYhjaA(@q#TKw~1pCQ-Dnfh`_ z)gmAc9#>&Z*|$iiLkM!Z1!bfH0_Jyb*hJ+)tPVq4rErTwNp0)%YooZDnwoQZ$;iMO zt`zsevW@=D=WT=5k1O^()Oq9F=Gx1e?`tmR(`m8rt)i%FBsNC05)4yS{r=|3R1BDS zP;!iz1H;EhUh}34DHvlMblvHVzz#l-%`ZeuvZl{rKX_3-+tGa=nIC7Y^at*#^~-yS zp2h5?xn*57U3KIZLDn+2-&TpTaw>o+s(!hzZ^J+Tj9Z)967hxz zfco;|?o)lAU8-i#rse?6r9v%v>2lW8<>T|d&pe77+cQk7{htW7dGO@3f z(ESDiVVWKJm*T)u9W?H-4w>)QnD*+m<$AL+bHk|V-AU26P4SbJ5ahu>$7+3+^PD@N zMX+7&)HX={j_wI8f+Cx4J%$JJ7$x(C4Y;ulw|uG$%J#*Y`yOX9T5m z=R&=Q6+H}$yr3{@fYwxE(YQvsk!v44vfbFtbs#I@PLWroXlfy*X=C=-IHteP#vgVQlM>70;ky2Ne=PP6-76)ey-(ekwoq+xN75Iqe4oN)!|8GnL#R2X7;!0+Ls8-`8>y?N<_#4IZeoEkp^4W$#{nD1 z{MU)UoXpSfFC(+`tjV`mnFnbZf|*HqU71hk29S z#c8x)VD8k_wPuW0?;WN6QJc2T@44j~b3O9mjC6Dae-E$~ruc%_asN5&$MCp;KhFHn zY^%*HB6GNQ&4-;VaMH#DVoD@p&^`6Sq>RTq>^MA2!ustzdUQel5%u=*#kwt;a_`T? z*3=~ENT=%fY^JG&-9tpM@4T~HCeo*PyExChK?d+b-|>m8EY4!&ZAak*gy;5|UH==5 z#RXJJs3-~x*K>BGri@D2+PH=Y%Wek8fRacJ_~+#0-^0R8C(iE`%XdTn!!+R>BU>qSJy>7q-6WD$Z1{UNoJ#RhTAu0yvp zR63HYcI#G4K8k`L8Q{jM zNC_@oyy&8J4{bkvPAnaXBfU7U_(TC&a)vA{B1lH*Nj`AE6HNDES3b5Avu0sW<${y} zG>6dqEdp7ZB-qDFgDxYDE!GJD>rmjNQ))WhkPKGcQ98=7ka{akO?5>@Gr#N=-q%9I zwz`Q89(?*0GT(+qepD@j%3G9GgHarTagWtu5dECFb6xeu+`4;rK^{PS?T|>hQDl|8 zO>r8rGl)(Eb-mSZ<~1aA+TTP9W4QF8MS$*-MTxpt!+^M1vZxJz(4O@tF*QBAR6AwY z(#Pl9X}9MlJ5g9zD5yFRVa63K_VJlaR2lhezWq(I>jmWpP)0Z|z_S>)7`&y;F8^`9 z+WuyQI^R^u>RfPo)5E{0<`tvsgIJ}*Rh18kDA-u*LpxY8rYfLk)ol8eGaCO_~$H;q0?3B)JfjwD4i}&5#NA zJYUP0yFLbnhF%16+$&jeT$|1*6;6alBU!9U8*g9Xh-z;aqQA2pE4IsDgWqMNF@}QN^@+h09`YkuI(}vt({WJc^YTwaSyM zWaX<)e3sA{5D%t>b)7siv})n$Jr6F$-dm_)vVUU8v8`N%(t$$Gbx_)d9Pj3X1?g_% z_tZB1OB`CB+;gtCrsIKcF!Jd@PL?dQwP%IRR@GV!~tBqy~ikUcCb*1h2M72FL zv7Mj*;JGHqwin#H(h4t)R7yRfPHY zK@UvcH6aet;)-*YFz-jt*n5c8_!+XQSh>{!cVV~%^l{3-Vs+5_YMaQy+NqOaWIB`D=wE_|(_7{X;yb~}pL}t&&k|s8`wz}gurfLmQT;2>B13N*z3};Cr`tsO&Fw7(h5NxDr zb(hyq4VD$oN06*j$5}KEulRltXa7occo$C&TMwkQ%+>WCw=Z~W@5Ooz0e7*T$eY;M z0xbg0-{AZ^Ee$S#WAFjJHGaBw5r36R$QeQ3sk_X z2J(!Y9g=tK-0v(l+~;IKxf(wu7flDlHC zePlMMG?*g9^}|=<7NPHqYA<0fz4VyAjf2B7-aZJ8zeY|BYGUteto_b@VSPaHxzG)f zA07y=pzDkG*Oc){5;E;b^lvIAk3#M>T1)Gk<~9?aX_;=JUdF0yb5u+h(NlUO-ns+(OSTZZf zad={xP-iYIoEE6|Q}SeDq#b%?4yV&1+a~i~>WMMYzDZxc_2v*X-1bQbm@{c{(14R} z_s99mM#NLO^z}{8%*4c7qw<$Hivqh-<5bSjrV>%X$c15sM(m-(MrYi9g*>9c7UlfL zi1^yiMHVmYb1VfgWM{|5ru& zvAHM0)#`bcYCM>g zWwX!8RXbJ>iLz~Nv}HaJ1Y=rnKflu$rY3KBX2die1}Jv{hfZrIQz%5Yck%pry&p@3 z*DO+%J@MlE*$+_rRIQ1Vv6GlD%&n2^kZBldAoU7(lm;bChB%1Lr6Pd-F)O1~!pvlOuMS^SV*xxsS-)Qcx(}XG@9indv`kK9^P8V+;oMcm>1ZOdHQ` z`x7uP+lquP9)DdTBL&}VX%|wo!+?U=l@VF{(tn#zHU68opY>y$lPzj^VwKBSma2& GH~&AbhQCSx literal 0 HcmV?d00001 diff --git a/docs/2.5.0/_sidebar.md b/docs/2.5.0/_sidebar.md new file mode 100644 index 00000000..805fb873 --- /dev/null +++ b/docs/2.5.0/_sidebar.md @@ -0,0 +1,34 @@ +- [**MQTT IO**](/) + +- Documentation + + - [Versions](../versions.md) + +- Configuration + + - [Usage Scenarios](config/scenarios.md) + - [Interrupts](config/interrupts.md) + - [Home Assistant Discovery](config/ha_discovery.md) + - [V2 Changes](config/v2-changes.md) + - Section Reference + - [mqtt](config/reference/mqtt/) + - [gpio_modules](config/reference/gpio_modules/) + - [sensor_modules](config/reference/sensor_modules/) + - [stream_modules](config/reference/stream_modules/) + - [digital_inputs](config/reference/digital_inputs/) + - [digital_outputs](config/reference/digital_outputs/) + - [sensor_inputs](config/reference/sensor_inputs/) + - [logging](config/reference/logging/) + - [reporting](config/reference/reporting/) + - [options](config/reference/options/) + +- Deployment + + - [Supervisor](deployment/supervisor.md) + - [Docker](deployment/docker.md) + +- Development + + - [Config Schema](dev/config_schema.md) + - [Modules](dev/modules/README.md) + - [Changelog](CHANGELOG.md) \ No newline at end of file diff --git a/docs/2.5.0/_sidebar.md.j2 b/docs/2.5.0/_sidebar.md.j2 new file mode 100644 index 00000000..12cdbb88 --- /dev/null +++ b/docs/2.5.0/_sidebar.md.j2 @@ -0,0 +1,27 @@ +- [**MQTT IO**](/) + +- Documentation + + - [Versions](../versions.md) + +- Configuration + + - [Usage Scenarios](config/scenarios.md) + - [Interrupts](config/interrupts.md) + - [Home Assistant Discovery](config/ha_discovery.md) + - [V2 Changes](config/v2-changes.md) + - Section Reference + {%- for ref_section in ref_sections %} + - [{{ ref_section["title"] }}]({{ ref_section["path"] }}) + {%- endfor %} + +- Deployment + + - [Supervisor](deployment/supervisor.md) + - [Docker](deployment/docker.md) + +- Development + + - [Config Schema](dev/config_schema.md) + - [Modules](dev/modules/README.md) + - [Changelog](CHANGELOG.md) diff --git a/docs/2.5.0/config/ha_discovery.md b/docs/2.5.0/config/ha_discovery.md new file mode 100644 index 00000000..64d770d7 --- /dev/null +++ b/docs/2.5.0/config/ha_discovery.md @@ -0,0 +1,83 @@ +# Home Assistant Discovery + +In order to avoid having to configure your devices both on MQTT IO and on Home Assistant, MQTT IO will [send an announcement](https://www.home-assistant.io/docs/mqtt/discovery/) to Home Assistant to notify it of the devices that have been exposed. + +## Configuration + +Enable Home Assistant discovery: + +```yaml +mqtt: + ha_discovery: + enabled: yes +``` + +To modify the announcements for individual inputs/outputs/sensors more appropriate, add the `ha_discovery` section to their config entries and add entries [as specified in the HA docs](https://www.home-assistant.io/docs/mqtt/discovery/) + +### Example + +```yaml +mqtt: + host: localhost + ha_discovery: + enabled: yes + +gpio_modules: + - name: rpi + module: raspberrypi + +digital_inputs: + - name: door_sensor + module: rpi + pin: 1 + ha_discovery: + name: Front Door + device_class: door + +digital_outputs: + - name: hall_fan + module: rpi + pin: 2 + ha_discovery: + name: Hall Fan + device_class: fan +``` + +### Availability + +In order for Home Assistant to establish whether the input/output/sensor is available, it monitors the `state_topic` for `payload_available` and `payload_not_available`. By default, MQTT IO will set this to one of _three_ values depicted in the `mqtt` section as `status_payload_running`, `status_payload_stopped` and `status_payload_dead`. For Home Assistant's availability checking to work correctly, it might be worth changing your status payloads to be one of _two_ values instead: + +```yaml +mqtt: + host: localhost + status_payload_running: available + status_payload_stopped: unavailable + status_payload_dead: unavailable +``` + +Unless set specifically in the `ha_discovery` section of the input/output/sensor configs, `payload_available` will be set to the value of `mqtt.status_payload_running` and `payload_not_available` will be set to the value of `mqtt.status_payload_dead`. + +## Implementation + +After connecting to the MQTT server, MQTT IO will announce digital inputs, digital outputs and sensors to Home Assistant by publishing a JSON payload containing details of the input/output/sensor to the Home Assistant discovery topics. For example, the following JSON might be sent to the `homeassistant/binary_sensor/pi-mqtt-gpio-429373a4/button/config` topic for a digital input: + +```json +{ + "name": "button", + "unique_id": "pi-mqtt-gpio-429373a4_stdio_input_button", + "state_topic": "pimqttgpio/mydevice/input/button", + "availability_topic": "pimqttgpio/mydevice/status", + "payload_available": "running", + "payload_not_available": "dead", + "payload_on": "ON", + "payload_off": "OFF", + "device": { + "manufacturer": "MQTT IO", + "identifiers": [ + "mqtt-gpio", + "pi-mqtt-gpio-429373a4" + ], + "name": "MQTT IO" + } +} +``` diff --git a/docs/2.5.0/config/interrupts.md b/docs/2.5.0/config/interrupts.md new file mode 100644 index 00000000..3dac0c84 --- /dev/null +++ b/docs/2.5.0/config/interrupts.md @@ -0,0 +1,113 @@ +# Interrupts + +Repeatedly checking the value of all of the GPIO inputs we're configured with is a lot of work and can miss level changes if they change back before we poll the pin again. Interrupts exist for this reason. They invert the process, which is to say that the lower level code (C library/kernel etc.) calls the higher level code (MQTT IO's Python code) when an input changes level. This has the benefit of not wasting CPU cycles on polling, as well as being much more responsive to input level changes. + +## Implementation + +The way that interrupts are exposed and supported by different hardware modules and software libraries can vary greatly. + +Sometimes the interrupts are available as software callbacks on the hardware's Python library. Sometimes the hardware will have registers that store which pin triggered the interrupt and what value it had. Sometimes the hardware will simply change the logic level on a dedicated pin to indicate that one of its inputs changed. + +MQTT IO attempts to accommodate these various configurations as best it can. + +Sometimes, like with the Raspberry Pi, this means that we get an interrupt on a pin, our Python callback function is called, but we still have to poll the pin for its current value (this only applies when the interrupt is configured to fire on both rising AND falling edges, as we can infer the value when it only triggers on a rising OR falling edge.) + +Other times, like with the MCP23017, we get no information from the software library that an interrupt has occurred, but by connecting its interrupt output pin to a Raspberry Pi input pin, we can use the software callback to trigger a read of the MCP23017's registers that specify which pin changed, and what value it got set to. + +## Example + +This is an example configuration using the Raspberry Pi and MCP23017 IO expander chip in which (as above) one of the MCP's interrupt output pins is connected to an input on the Raspberry Pi and the software is configured to listen on the Raspberry Pi pin's interrupt to then get the interrupt pin and value from the MCP. + +The following steps occur when the button in the diagram is pushed: + +1. `SW_Push` button is pushed, pulling the MCP's `GPA3` low +2. The MCP pulls its interrupt pin `INTA` low +3. The Raspberry Pi input pin `GPIO5` is pulled low and triggers an interrupt +4. The Raspberry Pi's GPIO library calls MQTT IO's callback +5. MQTT IO checks the `mcp23017` module's interrupt support and uses the `get_int_pins()` and `get_captured_int_pin_values()` methods to figure out which pin triggered the interrupt and what logic level it changed to +6. MQTT IO publishes the value of the pin that changed (`GPA3`) to the `/input/mcp3` MQTT topic + +### Connection Diagram + +![Raspberry Pi MCP23017 circuit diagram](../_images/pimcp.png) + +### Configuration + +This is configured by adding the `interrupt_for` section to a `digital_inputs` entry in the config file: + +```yaml +gpio_modules: + - name: rpi + module: raspberrypi + + - name: mcp + module: mcp23017 + +digital_inputs: + - name: pi5 + module: rpi + pin: 5 + interrupt: falling + interrupt_for: + - mcp3 + + - name: mcp3 + module: mcp23017 + pin: 3 + interrupt: both +``` + +## Module Support + +It's up to each GPIO module to specify which interrupt features are supported by setting flags on its `INTERRUPT_SUPPORT` constant, as well as implement the methods to support them: + +```python +from . import GenericGPIO, InterruptSupport + +class GPIO(GenericGPIO): + """ + Implementation of GPIO class for the MCP23017 IO expander chip. + Pin numbers 0 - 15. + """ + + INTERRUPT_SUPPORT = ( + InterruptSupport.FLAG_REGISTER + | InterruptSupport.CAPTURE_REGISTER + | InterruptSupport.INTERRUPT_PIN + | InterruptSupport.SET_TRIGGERS + ) + + def get_int_pins(self): + """ + Read the register and return a list of pins that triggered the interrupt. + """ + return self.io.int_flag + + def get_captured_int_pin_values(self, pins=None): + """ + Read the register that logs the values of the pins at the point of + the last interrupt, and return a dict. + + If pins is None, then we get the whole register and return it, otherwise + just return the values for the pins requested. + """ + values = self.io.int_cap + if pins is None: + pins = range(16) + pin_values = {} + for pin in pins: + pin_values[pin] = values[pin] + return pin_values + + ... +``` + +This way, when an interrupt pin (for example on Raspberry Pi) is configured as an `interrupt_for` a pin on this module, the `GenericGPIO.get_interrupt_values_remote()` method will use the functions available to establish which pins were changed and what their values were. + +If the hardware doesn't support these features, then `GenericGPIO.get_interrupt_values_remote()` will just fall back to polling the pin(s) and returning those values instead. + +## Flow Chart + +The following is the somewhat complex flow of internal logic that describes the behaviour of the interrupt system. + +![Interrupt handling flow chart](../_images/interrupt_handling.dot.svg) \ No newline at end of file diff --git a/docs/2.5.0/config/reference.md.j2 b/docs/2.5.0/config/reference.md.j2 new file mode 100644 index 00000000..937b4c88 --- /dev/null +++ b/docs/2.5.0/config/reference.md.j2 @@ -0,0 +1,59 @@ +{% for entry in ref_sections -%} +{% if entry["toplevel_name"] == section -%} +#{{ "#" * entry["depth"] }} {{ entry["title"] }} :id={{ entry["element_id"] }} + +{%- if entry["subtitle"] %} + +{{ entry["subtitle"] }} +{%- endif %} + +{%- if "description" in entry["meta"] %} + +{{ entry["meta"]["description"] }} +{%- endif %} + +```yaml +Type: {{ entry["schema"]["type"] }} +Required: {{ entry["schema"]["required"] }} + +{%- if "unit" in entry["meta"] %} +Unit: {{ entry["meta"]["unit"] }} +{%- endif %} + +{%- if "allowed" in entry["schema"] %} +Allowed: {{ entry["schema"]["allowed"] }} +{%- endif %} + +{%- if "min_val" in entry["schema"] %} +Minimum value: {{ entry["schema"]["min_val"] }} +{%- endif %} + +{%- if "max_val" in entry["schema"] %} +Maximum value: {{ entry["schema"]["max_val"] }} +{%- endif %} + +{%- if "allow_unknown" in entry["schema"] %} +Unlisted entries accepted: {{ entry["schema"]["allow_unknown"] }} +{%- endif %} + +{%- if "default" in entry["schema"] %} +Default: {{ entry["schema"]["default"] }} +{%- endif %} +``` + +{%- if "extra_info" in entry["meta"] %} + +?> {{ entry["meta"]["extra_info"] }} +{%- endif %} + +{%- if "yaml_example" in entry["meta"] %} + +**Example**: + +```yaml +{{ entry["meta"]["yaml_example"] -}} +``` +{%- endif %} + +{% endif %} +{%- endfor %} diff --git a/docs/2.5.0/config/reference/digital_inputs/README.md b/docs/2.5.0/config/reference/digital_inputs/README.md new file mode 100644 index 00000000..6b0907db --- /dev/null +++ b/docs/2.5.0/config/reference/digital_inputs/README.md @@ -0,0 +1,333 @@ +# digital_inputs :id=digital_inputs + +List of digital inputs to configure. + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`PIN_SCHEMA` and `INPUT_SCHEMA` values of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +gpio_modules: + - name: rpi + module: raspberrypi + +digital_inputs: + - name: gpio0 + module: rpi + pin: 0 + + - name: gpio1 + module: rpi + pin: 1 +``` + +## digital_inputs.* :id=digital_inputs-star + +*digital_inputs*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=digital_inputs-star-name + +*digital_inputs.**.**name** + +Name of the input. Used in the MQTT topic when publishing input changes. + +The topic that input changes will be published to is: +`/input/` + + +```yaml +Type: string +Required: True +``` + +## module :id=digital_inputs-star-module + +*digital_inputs.**.**module** + +Name of the module configured in `gpio_modules` that this input is attached to. + + +```yaml +Type: string +Required: True +``` + +## pin :id=digital_inputs-star-pin + +*digital_inputs.**.**pin** + +Which of the GPIO module's pins this input refers to. + +```yaml +Type: ['string', 'integer'] +Required: True +``` + +?> Depending on the GPIO module's implementation, this can be either a string +or an integer. + + +## on_payload :id=digital_inputs-star-on_payload + +*digital_inputs.**.**on_payload** + +Payload to be sent when the input changes to what is considered to be "on". +See `inverted` below for the definition of "on" and "off". + + +```yaml +Type: string +Required: False +Default: ON +``` + +?> Make sure to avoid YAML's automatic boolean type conversion when setting this +option by surrounding potential booleans with quotes. +See the "Regexp" section of the +[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that +will be parsed as boolean. + + +## off_payload :id=digital_inputs-star-off_payload + +*digital_inputs.**.**off_payload** + +Payload to be sent when the input changes to what is considered to be "off". +See `inverted` below for the definition of "on" and "off". + + +```yaml +Type: string +Required: False +Default: OFF +``` + +?> Make sure to avoid YAML's automatic boolean type conversion when setting this +option by surrounding potential booleans with quotes. +See the "Regexp" section of the +[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that +will be parsed as boolean. + + +## inverted :id=digital_inputs-star-inverted + +*digital_inputs.**.**inverted** + +Invert the logic level so that "low" levels are considered to be "on" and +"high" levels are considered "off". + + +```yaml +Type: boolean +Required: False +Default: False +``` + +?> This can be useful for when an input is pulled "high" with a resistor and a +device (like a button or another IC) connects it to ground when it's "active". + + +## pullup :id=digital_inputs-star-pullup + +*digital_inputs.**.**pullup** + +Enable the pull-up resistor for this input so that the logic level is pulled +"high" by default. + + +```yaml +Type: boolean +Required: False +Default: False +``` + +?> Not all GPIO modules support pull-up resistors. + +## pulldown :id=digital_inputs-star-pulldown + +*digital_inputs.**.**pulldown** + +Enable the pull-down resistor for this input so that the logic level is pulled +"low" by default. + + +```yaml +Type: boolean +Required: False +Default: False +``` + +?> Not all GPIO modules support pull-down resistors. + +## interrupt :id=digital_inputs-star-interrupt + +*digital_inputs.**.**interrupt** + +Configure this pin to trigger an interrupt when the logic level is "rising", +"falling" or "both". + + +```yaml +Type: string +Required: False +Allowed: ['rising', 'falling', 'both'] +``` + +?> Not all GPIO modules support interrupts, and those that do may do so in +various ways. +TODO: Add link to interrupt documentation. + + +## interrupt_for :id=digital_inputs-star-interrupt_for + +*digital_inputs.**.**interrupt_for** + +List of other pin names that this pin is an interrupt for. + +This is generally used on GPIO modules that provide software callbacks on +interrupts, so that we can attach another "remote" module's interrupt output +pin (one that changes logic level when one of its pins triggers an interrupt) +to this input and use the callback to get the value of the "remote" pin and +publish it on MQTT. + +TODO: Add link to interrupt documentation. + + +```yaml +Type: list +Required: False +``` + +### interrupt_for.* :id=digital_inputs-star-interrupt_for-star + +*digital_inputs.*.interrupt_for*.***** + +```yaml +Type: string +Required: True +``` + +## bouncetime :id=digital_inputs-star-bouncetime + +*digital_inputs.**.**bouncetime** + +Don't trigger interrupts more frequently than once per `bouncetime`. + + +```yaml +Type: integer +Required: False +Unit: milliseconds +Default: 100 +``` + +## retain :id=digital_inputs-star-retain + +*digital_inputs.**.**retain** + +Set the retain flag on MQTT messages published on input change. + +```yaml +Type: boolean +Required: False +Default: False +``` + +## poll_interval :id=digital_inputs-star-poll_interval + +*digital_inputs.**.**poll_interval** + +How long to wait between checking the value of this input. + +```yaml +Type: float +Required: False +Unit: seconds +Default: 0.1 +``` + +?> When the pin is configured as an interrupt, the pin is no longer polled. +The only exception to this is if the pin is configured as an interrupt for +another pin. In this case, whether or not we poll is decided by the +`poll_when_interrupt_for` setting below. + + +## poll_when_interrupt_for :id=digital_inputs-star-poll_when_interrupt_for + +*digital_inputs.**.**poll_when_interrupt_for** + +Poll this pin when it's configured as an interrupt for another pin. + +```yaml +Type: boolean +Required: False +Default: True +``` + +?> Polling the pin when it's configured as an interrupt for another pin is useful +in order to make sure that if we somehow miss an interrupt on this pin (the +remote module's interrupt output pin goes low ("triggered")), we +don't end up stuck in that state where we don't handle the remote module's +interrupt at all. If we poll the "triggered" value on this pin and our +interrupt handling hasn't dealt with it, then we'll handle it here. + + +## ha_discovery :id=digital_inputs-star-ha_discovery + +*digital_inputs.**.**ha_discovery** + +Configures the +[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) +for this pin. + +Any values entered into this section will be sent as part of the discovery +config payload. See the above link for documentation. + + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +**Example**: + +```yaml +digital_inputs: + - name: livingroom_motion + module: rpi + ha_discovery: + component: binary_sensor + name: Living Room Motion + device_class: motion +``` + +### component :id=digital_inputs-star-ha_discovery-component + +*digital_inputs.*.ha_discovery*.**component** + +Type of component to report this input as to Home Assistant. + +```yaml +Type: string +Required: False +Default: binary_sensor +``` + diff --git a/docs/2.5.0/config/reference/digital_outputs/README.md b/docs/2.5.0/config/reference/digital_outputs/README.md new file mode 100644 index 00000000..3baae31f --- /dev/null +++ b/docs/2.5.0/config/reference/digital_outputs/README.md @@ -0,0 +1,254 @@ +# digital_outputs :id=digital_outputs + +List of digital outputs to configure. + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`PIN_SCHEMA` and `OUTPUT_SCHEMA` values of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +gpio_modules: + - name: rpi + module: raspberrypi + +digital_outputs: + - name: gpio0 + module: rpi + pin: 0 + + - name: gpio1 + module: rpi + pin: 1 +``` + +## digital_outputs.* :id=digital_outputs-star + +*digital_outputs*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=digital_outputs-star-name + +*digital_outputs.**.**name** + +Name of the output. Used in the MQTT topics that are subscribed to in order to +change the output value according to received MQTT messages, as well as in the +MQTT topic for publishing output changes. + +The topics subscribed to for each output are: +- `/output//set` +- `/output//set_on_ms` +- `/output//set_off_ms` + +The topic that output changes will be published to is: +`/output/` + + +```yaml +Type: string +Required: True +``` + +## module :id=digital_outputs-star-module + +*digital_outputs.**.**module** + +Name of the module configured in `gpio_modules` that this output is attached to. + + +```yaml +Type: string +Required: True +``` + +## pin :id=digital_outputs-star-pin + +*digital_outputs.**.**pin** + +Which of the GPIO module's pins this output refers to. + +```yaml +Type: ['string', 'integer'] +Required: True +``` + +?> Depending on the GPIO module's implementation, this can be either a string +or an integer. + + +## on_payload :id=digital_outputs-star-on_payload + +*digital_outputs.**.**on_payload** + +Payload to consider as "on" when received to the `/set` topic for this output. +See `inverted` below for the definition of "on" and "off". + + +```yaml +Type: string +Required: False +Default: ON +``` + +?> Make sure to avoid YAML's automatic boolean type conversion when setting this +option by surrounding potential booleans with quotes. +See the "Regexp" section of the +[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that +will be parsed as boolean. + + +## off_payload :id=digital_outputs-star-off_payload + +*digital_outputs.**.**off_payload** + +Payload to consider as "off" when received to the `/set` topic for this output. +See `inverted` below for the definition of "on" and "off". + + +```yaml +Type: string +Required: False +Default: OFF +``` + +?> Make sure to avoid YAML's automatic boolean type conversion when setting this +option by surrounding potential booleans with quotes. +See the "Regexp" section of the +[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that +will be parsed as boolean. + + +## inverted :id=digital_outputs-star-inverted + +*digital_outputs.**.**inverted** + +Invert the logic level so that "low" levels are considered to be "on" and +"high" levels are considered "off". + + +```yaml +Type: boolean +Required: False +Default: False +``` + +?> This can be useful for when an output turns something on when its output is +"low". + + +## timed_set_ms :id=digital_outputs-star-timed_set_ms + +*digital_outputs.**.**timed_set_ms** + +How long to set an output to the desired value on receipt of an MQTT message +to the `/set` topic before then setting it back to the opposite value. + + +```yaml +Type: integer +Required: False +Unit: milliseconds +``` + +?> This may be useful if the output controls a device where leaving the ouput +"on" for too long would be detrimental. Using this option means that you don't +have to rely on a second "off" message getting through MQTT for the output to +return to a safe state. + + +## initial :id=digital_outputs-star-initial + +*digital_outputs.**.**initial** + +Set the output to an initial "high" or "low" state when the software starts. + + +```yaml +Type: string +Required: False +Allowed: ['high', 'low'] +``` + +## publish_initial :id=digital_outputs-star-publish_initial + +*digital_outputs.**.**publish_initial** + +Whether to publish an MQTT message for the initial "high" or "low" state set +above. + + +```yaml +Type: boolean +Required: False +Default: False +``` + +## retain :id=digital_outputs-star-retain + +*digital_outputs.**.**retain** + +Set the retain flag on MQTT messages published on output change. + +```yaml +Type: boolean +Required: False +Default: False +``` + +## ha_discovery :id=digital_outputs-star-ha_discovery + +*digital_outputs.**.**ha_discovery** + +Configures the +[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) +for this pin. + +Any values entered into this section will be sent as part of the discovery +config payload. See the above link for documentation. + + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +**Example**: + +```yaml +digital_outputs: + - name: garage_door1 + module: rpi + ha_discovery: + component: switch + name: Ferrari Garage Door + device_class: garage_door +``` + +### component :id=digital_outputs-star-ha_discovery-component + +*digital_outputs.*.ha_discovery*.**component** + +Type of component to report this output as to Home Assistant. + +```yaml +Type: string +Required: False +Default: switch +``` + diff --git a/docs/2.5.0/config/reference/gpio_modules/README.md b/docs/2.5.0/config/reference/gpio_modules/README.md new file mode 100644 index 00000000..cc14636c --- /dev/null +++ b/docs/2.5.0/config/reference/gpio_modules/README.md @@ -0,0 +1,79 @@ +# gpio_modules :id=gpio_modules + +List of GPIO modules to configure for use with inputs and/or outputs. + + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`CONFIG_SCHEMA` value of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +gpio_modules: + - name: rpi_gpio + module: raspberrypi + + - name: pcf + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x20 +``` + +## gpio_modules.* :id=gpio_modules-star + +*gpio_modules*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=gpio_modules-star-name + +*gpio_modules.**.**name** + +Your name for this configuration of the module. Will be referred to by entries +in the `digital_inputs` and/or `digital_outputs` sections. + + +```yaml +Type: string +Required: True +``` + +## module :id=gpio_modules-star-module + +*gpio_modules.**.**module** + +Name of the module in the code. This is listed in the README's +"Supported Hardware" section in brackets. + + +```yaml +Type: string +Required: True +``` + +## cleanup :id=gpio_modules-star-cleanup + +*gpio_modules.**.**cleanup** + +Whether to run the module's `cleanup()` method on exit. + +```yaml +Type: boolean +Required: False +Default: True +``` + diff --git a/docs/2.5.0/config/reference/logging/README.md b/docs/2.5.0/config/reference/logging/README.md new file mode 100644 index 00000000..66fc0d64 --- /dev/null +++ b/docs/2.5.0/config/reference/logging/README.md @@ -0,0 +1,14 @@ +# logging :id=logging + +Config to pass directly to +[Python's logging module](https://docs.python.org/3/library/logging.config.html#logging-config-dictschema) +to influence the logging output of the software. + + +```yaml +Type: dict +Required: False +Unlisted entries accepted: True +Default: {'version': 1, 'handlers': {'console': {'class': 'logging.StreamHandler', 'formatter': 'default', 'level': 'INFO'}}, 'formatters': {'default': {'format': '%(asctime)s %(name)s [%(levelname)s] %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S'}}, 'loggers': {'mqtt_io': {'level': 'INFO', 'handlers': ['console'], 'propagate': True}}} +``` + diff --git a/docs/2.5.0/config/reference/mqtt/README.md b/docs/2.5.0/config/reference/mqtt/README.md new file mode 100644 index 00000000..c7302edf --- /dev/null +++ b/docs/2.5.0/config/reference/mqtt/README.md @@ -0,0 +1,445 @@ +# mqtt :id=mqtt + +Contains the configuration data used for connecting to an MQTT server. + + +```yaml +Type: dict +Required: True +``` + +**Example**: + +```yaml +mqtt: + host: test.mosquitto.org + port: 8883 + topic_prefix: mqtt_io + ha_discovery: + enabled: yes + tls: + enabled: yes + ca_certs: mosquitto.org.crt + certfile: client.crt + keyfile: client.key +``` + +## host :id=mqtt-host + +*mqtt*.**host** + +Host name or IP address of the MQTT server. + +```yaml +Type: string +Required: True +``` + +## port :id=mqtt-port + +*mqtt*.**port** + +Port number to connect to on the MQTT server. + +```yaml +Type: integer +Required: False +Default: 1883 +``` + +## user :id=mqtt-user + +*mqtt*.**user** + +Username to authenticate with on the MQTT server. + +```yaml +Type: string +Required: False +Default: +``` + +## password :id=mqtt-password + +*mqtt*.**password** + +Password to authenticate with on the MQTT server. + +```yaml +Type: string +Required: False +Default: +``` + +## client_id :id=mqtt-client_id + +*mqtt*.**client_id** + +[MQTT client ID](https://www.cloudmqtt.com/blog/2018-11-21-mqtt-what-is-client-id.html) to use on the MQTT server. + + +```yaml +Type: string +Required: False +Default: +``` + +## topic_prefix :id=mqtt-topic_prefix + +*mqtt*.**topic_prefix** + +Prefix to use for all topics. + +```yaml +Type: string +Required: False +Default: +``` + +?> For example, a `topic_prefix` of `home/livingroom` would make a digital input +called "doorbell" publish its changes to the `home/livingroom/input/doorbell` +topic. + + +## clean_session :id=mqtt-clean_session + +*mqtt*.**clean_session** + +Whether or not to start a +[clean MQTT session](https://www.hivemq.com/blog/mqtt-essentials-part-7-persistent-session-queuing-messages/) +on every MQTT connection. + + +```yaml +Type: boolean +Required: False +Default: False +``` + +## protocol :id=mqtt-protocol + +*mqtt*.**protocol** + +Version of the MQTT protocol to use. + +```yaml +Type: string +Required: False +Allowed: ['3.1', '3.1.1'] +Default: 3.1.1 +``` + +?> This renders in the documentation as a float, but should always be set within quotes. + + +## keepalive :id=mqtt-keepalive + +*mqtt*.**keepalive** + +How frequently in seconds to send +[ping packets](https://www.hivemq.com/blog/mqtt-essentials-part-10-alive-client-take-over/) +to the MQTT server. + + +```yaml +Type: integer +Required: False +Unit: seconds +Default: 10 +``` + +## status_topic :id=mqtt-status_topic + +*mqtt*.**status_topic** + +Topic on which to send messages about the running status of this software. + +```yaml +Type: string +Required: False +Default: status +``` + +?> Sends the payloads configured in `status_payload_running`, +`status_payload_stopped` and `status_payload_dead`. + + +## status_payload_running :id=mqtt-status_payload_running + +*mqtt*.**status_payload_running** + +Payload to send on the status topic when the software is running. + +```yaml +Type: string +Required: False +Default: running +``` + +## status_payload_stopped :id=mqtt-status_payload_stopped + +*mqtt*.**status_payload_stopped** + +Payload to send on the status topic when the software has exited cleanly. + +```yaml +Type: string +Required: False +Default: stopped +``` + +## status_payload_dead :id=mqtt-status_payload_dead + +*mqtt*.**status_payload_dead** + +Payload to send on the status topic when the software has exited unexpectedly. + +```yaml +Type: string +Required: False +Default: dead +``` + +?> Uses [MQTT Last Will and Testament](https://www.hivemq.com/blog/mqtt-essentials-part-9-last-will-and-testament/) +to make the server automatically send this payload if our connection fails. + + +## client_module :id=mqtt-client_module + +*mqtt*.**client_module** + +MQTT Client implementation module path. + +```yaml +Type: string +Required: False +Default: mqtt_io.mqtt.aiomqtt +``` + +?> There's currently only one implementation, which uses the +[aiomqtt](https://github.com/sbtinstruments/aiomqtt/) client. + + +## ha_discovery :id=mqtt-ha_discovery + +*mqtt*.**ha_discovery** + +```yaml +Type: dict +Required: False +``` + +### enabled :id=mqtt-ha_discovery-enabled + +*mqtt.ha_discovery*.**enabled** + +Enable [Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) +of our configured devices. + + +```yaml +Type: boolean +Required: True +``` + +### prefix :id=mqtt-ha_discovery-prefix + +*mqtt.ha_discovery*.**prefix** + +Prefix for the Home Assistant MQTT discovery topic. + +```yaml +Type: string +Required: False +Default: homeassistant +``` + +### name :id=mqtt-ha_discovery-name + +*mqtt.ha_discovery*.**name** + +Name to identify this "device" in Home Assistant. + +```yaml +Type: string +Required: False +Default: MQTT IO +``` + +## tls :id=mqtt-tls + +*mqtt*.**tls** + +TLS/SSL settings for connecting to the MQTT server over an encrypted connection. + + +```yaml +Type: dict +Required: False +``` + +**Example**: + +```yaml +mqtt: + host: localhost + tls: + enabled: yes + ca_certs: mosquitto.org.crt + certfile: client.crt + keyfile: client.key +``` + +### enabled :id=mqtt-tls-enabled + +*mqtt.tls*.**enabled** + +Enable a secure connection to the MQTT server. + +```yaml +Type: boolean +Required: True +``` + +?> Most of these options map directly to the +[`tls_set()` arguments](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) +on the Paho MQTT client. + + +### ca_certs :id=mqtt-tls-ca_certs + +*mqtt.tls*.**ca_certs** + +Path to the Certificate Authority certificate files that are to be treated +as trusted by this client. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +``` + +### certfile :id=mqtt-tls-certfile + +*mqtt.tls*.**certfile** + +Path to the PEM encoded client certificate. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +``` + +### keyfile :id=mqtt-tls-keyfile + +*mqtt.tls*.**keyfile** + +Path to the PEM encoded client private key. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +``` + +### cert_reqs :id=mqtt-tls-cert_reqs + +*mqtt.tls*.**cert_reqs** + +Defines the certificate requirements that the client imposes on the MQTT server. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +Allowed: ['CERT_NONE', 'CERT_OPTIONAL', 'CERT_REQUIRED'] +Default: CERT_REQUIRED +``` + +?> By default this is `CERT_REQUIRED`, which means that the broker must provide a certificate. + + +### tls_version :id=mqtt-tls-tls_version + +*mqtt.tls*.**tls_version** + +Specifies the version of the SSL/TLS protocol to be used. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +``` + +?> By default the highest TLS version is detected. + + +### ciphers :id=mqtt-tls-ciphers + +*mqtt.tls*.**ciphers** + +Which encryption ciphers are allowable for this connection. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set) + + +```yaml +Type: string +Required: False +``` + +### insecure :id=mqtt-tls-insecure + +*mqtt.tls*.**insecure** + +Configure verification of the server hostname in the server certificate. +[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-insecure-set) + + +```yaml +Type: boolean +Required: False +Default: False +``` + +?> If set to true, it is impossible to guarantee that the host you are +connecting to is not impersonating your server. This can be useful in +initial server testing, but makes it possible for a malicious third party +to impersonate your server through DNS spoofing, for example. +Do not use this function in a real system. Setting value to true means there +is no point using encryption. + + +## reconnect_delay :id=mqtt-reconnect_delay + +*mqtt*.**reconnect_delay** + +Time in seconds to wait between reconnect attempts. + + +```yaml +Type: integer +Required: False +Default: 2 +``` + +## reconnect_count :id=mqtt-reconnect_count + +*mqtt*.**reconnect_count** + +Max number of retries of connections before giving up and exiting. +Null value means infinite reconnects (default). +The counter is reset when the connection is reestablished successfully. + + +```yaml +Type: integer +Required: False +Default: None +``` + diff --git a/docs/2.5.0/config/reference/options/README.md b/docs/2.5.0/config/reference/options/README.md new file mode 100644 index 00000000..d9de834c --- /dev/null +++ b/docs/2.5.0/config/reference/options/README.md @@ -0,0 +1,29 @@ +# options :id=options + +Miscellaneous options regarding the runtime behaviour of MQTT IO. + +```yaml +Type: dict +Required: False +Default: {} +``` + +**Example**: + +```yaml +options: + install_requirements: no +``` + +## install_requirements :id=options-install_requirements + +*options*.**install_requirements** + +Whether to install missing module packages on startup. + +```yaml +Type: boolean +Required: False +Default: True +``` + diff --git a/docs/2.5.0/config/reference/reporting/README.md b/docs/2.5.0/config/reference/reporting/README.md new file mode 100644 index 00000000..3f874673 --- /dev/null +++ b/docs/2.5.0/config/reference/reporting/README.md @@ -0,0 +1,54 @@ +# reporting :id=reporting + +Configuration for reporting back to the developers using +[Sentry](https://sentry.io/welcome/) to help diagnose issues. + +*This is **not** enabled by default* + + +```yaml +Type: dict +Required: False +``` + +?> Your config file is included in the report, but has the host, port and username +hashed and the password removed. Sentry's SDK automatically attempts to remove +password data, but the other values may still be exposed within the Python traceback +context. + + +**Example**: + +```yaml +reporting: + enabled: yes + issue_id: 123 +``` + +## enabled :id=reporting-enabled + +*reporting*.**enabled** + +Enable the sending of error reports to the developers if the software crashes. + + +```yaml +Type: boolean +Required: True +``` + +## issue_id :id=reporting-issue_id + +*reporting*.**issue_id** + +The GitHub Issue ID that the specific error relates to. + +```yaml +Type: integer +Required: False +``` + +?> This is useful if you've reported a specific issue on the project repository and +want to provide additional context to help the developers diagnose the issue. + + diff --git a/docs/2.5.0/config/reference/sensor_inputs/README.md b/docs/2.5.0/config/reference/sensor_inputs/README.md new file mode 100644 index 00000000..5f5fd07b --- /dev/null +++ b/docs/2.5.0/config/reference/sensor_inputs/README.md @@ -0,0 +1,176 @@ +# sensor_inputs :id=sensor_inputs + +List of sensor inputs to configure. + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`MODULE_SCHEMA` values of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +sensor_modules: + - name: dht + module: dht22 + type: AM2302 + pin: 4 + +sensor_inputs: + - name: workshop_temp + module: dht + type: temperature + interval: 30 + + - name: workshop_humidity + module: dht + type: humidity + interval: 60 +``` + +## sensor_inputs.* :id=sensor_inputs-star + +*sensor_inputs*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=sensor_inputs-star-name + +*sensor_inputs.**.**name** + +Name of the sensor. Used in the MQTT topic when publishing sensor values. + +The topic that sensor values will be published to is: +`/sensor/` + + +```yaml +Type: string +Required: True +``` + +## module :id=sensor_inputs-star-module + +*sensor_inputs.**.**module** + +Name of the module configured in `sensor_modules` that this sensor reading +comes from. + + +```yaml +Type: string +Required: True +``` + +## retain :id=sensor_inputs-star-retain + +*sensor_inputs.**.**retain** + +Set the retain flag on MQTT messages published on sensor read. + +```yaml +Type: boolean +Required: False +Default: False +``` + +## interval :id=sensor_inputs-star-interval + +*sensor_inputs.**.**interval** + +How long to wait between checking the value of this sensor. + +```yaml +Type: integer +Required: False +Unit: seconds +Default: 60 +``` + +## digits :id=sensor_inputs-star-digits + +*sensor_inputs.**.**digits** + +How many decimal places to round the sensor reading to. + +```yaml +Type: integer +Required: False +Default: 2 +``` + +## ha_discovery :id=sensor_inputs-star-ha_discovery + +*sensor_inputs.**.**ha_discovery** + +Configures the +[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) +for this sensor. + +Any values entered into this section will be sent as part of the discovery +config payload. See the above link for documentation. + + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +**Example**: + +```yaml +sensor_inputs: + - name: workshop_temp + module: dht + type: temperature + ha_discovery: + name: Workshop Temperature + device_class: temperature + + - name: workshop_humidity + module: dht + type: humidity + ha_discovery: + name: Workshop Humidity + device_class: humidity +``` + +### component :id=sensor_inputs-star-ha_discovery-component + +*sensor_inputs.*.ha_discovery*.**component** + +Type of component to report this sensor as to Home Assistant. + +```yaml +Type: string +Required: False +Default: sensor +``` + +### expire_after :id=sensor_inputs-star-ha_discovery-expire_after + +*sensor_inputs.*.ha_discovery*.**expire_after** + +How long after receiving a sensor update to declare it invalid. + +```yaml +Type: integer +Required: False +``` + +?> Defaults to `interval` * 2 + 5 + + diff --git a/docs/2.5.0/config/reference/sensor_modules/README.md b/docs/2.5.0/config/reference/sensor_modules/README.md new file mode 100644 index 00000000..44609ed8 --- /dev/null +++ b/docs/2.5.0/config/reference/sensor_modules/README.md @@ -0,0 +1,80 @@ +# sensor_modules :id=sensor_modules + +List of sensor modules to configure for use with sensor inputs. + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`CONFIG_SCHEMA` value of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +sensor_modules: + - name: dht + module: dht22 + type: AM2302 + pin: 4 + + - name: ds + module: ds18b + type: DS18S20 + address: 000803702e49 +``` + +## sensor_modules.* :id=sensor_modules-star + +*sensor_modules*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=sensor_modules-star-name + +*sensor_modules.**.**name** + +Your name for this configuration of the module. Will be referred to by entries +in the `sensor_inputs` section. + + +```yaml +Type: string +Required: True +``` + +## module :id=sensor_modules-star-module + +*sensor_modules.**.**module** + +Name of the module in the code. This is listed in the README's +"Supported Hardware" section in brackets. + + +```yaml +Type: string +Required: True +``` + +## cleanup :id=sensor_modules-star-cleanup + +*sensor_modules.**.**cleanup** + +Whether to run the module's `cleanup()` method on exit. + +```yaml +Type: boolean +Required: False +Default: True +``` + diff --git a/docs/2.5.0/config/reference/stream_modules/README.md b/docs/2.5.0/config/reference/stream_modules/README.md new file mode 100644 index 00000000..9f4815b7 --- /dev/null +++ b/docs/2.5.0/config/reference/stream_modules/README.md @@ -0,0 +1,135 @@ +# stream_modules :id=stream_modules + +List of stream modules to configure. + +```yaml +Type: list +Required: False +Default: [] +``` + +?> Some modules require extra config entries, specified by the modules themselves. +Until the documentation is written for the individual modules, please refer to the +`CONFIG_SCHEMA` value of the module's code in +[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules). +TODO: Link this to the pending wiki pages on each module's requirements. + + +**Example**: + +```yaml +stream_modules: + - name: network_switch + module: serial + device: /dev/ttyUSB1 + baud: 115200 + interval: 10 + + - name: ups + module: serial + type: /dev/ttyUSB0 + baud: 9600 + interval: 1 +``` + +## stream_modules.* :id=stream_modules-star + +*stream_modules*.***** + +```yaml +Type: dict +Required: +Unlisted entries accepted: True +``` + +## name :id=stream_modules-star-name + +*stream_modules.**.**name** + +Your name for this configuration of the module. Will be used in the topic on +which the stream's data is published and the topic on which messages can be +sent for writing to the stream. + + +```yaml +Type: string +Required: True +``` + +## module :id=stream_modules-star-module + +*stream_modules.**.**module** + +Name of the module in the code. This is listed in the README's +"Supported Hardware" section in brackets. + + +```yaml +Type: string +Required: True +``` + +## cleanup :id=stream_modules-star-cleanup + +*stream_modules.**.**cleanup** + +Whether to run the module's `cleanup()` method on exit. + +```yaml +Type: boolean +Required: False +Default: True +``` + +## retain :id=stream_modules-star-retain + +*stream_modules.**.**retain** + +Whether to set the `retain` flag on MQTT messages publishing data received +from the stream. + + +```yaml +Type: boolean +Required: False +Default: False +``` + +## read_interval :id=stream_modules-star-read_interval + +*stream_modules.**.**read_interval** + +How long to wait between polling the stream for new data. + +```yaml +Type: float +Required: False +Unit: seconds +Default: 60 +``` + +## read :id=stream_modules-star-read + +*stream_modules.**.**read** + +Whether to poll this stream for incoming data and publish it on an MQTT topic. + + +```yaml +Type: boolean +Required: False +Default: True +``` + +## write :id=stream_modules-star-write + +*stream_modules.**.**write** + +Whether to subscribe to MQTT messages on a topic and write messages received on it to the stream. + +```yaml +Type: boolean +Required: False +Default: True +``` + diff --git a/docs/2.5.0/config/scenarios.md b/docs/2.5.0/config/scenarios.md new file mode 100644 index 00000000..d30507f4 --- /dev/null +++ b/docs/2.5.0/config/scenarios.md @@ -0,0 +1,154 @@ +# Usage Scenarios + +Since MQTT IO is a piece of software that ties one thing (MQTT) to another (GPIO, sensors, streams), there are endless scenarios in which it can be used. Home Automation is where the software was born, but anywhere you require programmatic control of hardware devices, MQTT IO may be of use. + +The following are a few simple examples which attempt to show most of the basic configuration options in place, and how they might be used. + +## Remote controlled mains sockets + +A Raspberry Pi with an 8 channel PCF8574 IO chip, connected to relays which connect and disconnect the live wire to 4 mains sockets. + + + + +**Example configuration** + +```yaml +mqtt: + host: test.mosquitto.org + port: 1883 + user: "" + password: "" + topic_prefix: home/livingroom/sockets + +gpio_modules: + - name: sockets_gpio + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x20 + +digital_outputs: + - name: socket1 + module: sockets_gpio + pin: 1 + on_payload: "ON" + off_payload: "OFF" + pullup: yes + + - name: socket2 + module: sockets_gpio + pin: 2 + on_payload: "ON" + off_payload: "OFF" + pullup: yes + + - name: socket3 + module: sockets_gpio + pin: 3 + on_payload: "ON" + off_payload: "OFF" + pullup: yes + + - name: socket4 + module: sockets_gpio + pin: 4 + on_payload: "ON" + off_payload: "OFF" + pullup: yes +``` + +This configuration uses the PCF8574 GPIO module to set 4 GPIO pins of the PCF8574 as outputs, then subscribes to messages on an MQTT topic for each output. Sending the configured on/off payload to these topics will cause the software to turn the outputs on and off, therefore supplying power to, or removing power from the individual sockets. These sockets may then be used for any general purpose, such as powering lights, heaters or fans. + +In order to turn each individual socket on and off, you'd send the following MQTT messages: + +```yaml +home/livingroom/sockets/output/socket1/set: ON +``` + +```yaml +home/livingroom/sockets/output/socket1/set: OFF +``` + +By varying the `socket1` part of the topic, you're able to choose which socket you'd like to control. + +## Temperature and humidity sensor + +A Raspberry Pi with a DHT22 temperature and humidity sensor connected to pin 4 of its built in GPIO pins. + +**Example configuration** + +```yaml +mqtt: + host: test.mosquitto.org + port: 1883 + user: "" + password: "" + topic_prefix: home/livingroom/climate + +sensor_modules: + - name: dht22_sensor + module: dht22 + type: AM2302 + pin: 4 + +sensor_inputs: + - name: temperature + module: dht22_sensor + interval: 10 + digits: 4 + type: temperature + + - name: humidity + module: dht22_sensor + interval: 10 + digits: 4 + type: humidity +``` + +This configuration will poll the DHT22 sensor every 10 seconds and publish MQTT messages such as the following: + +```yaml +home/livingroom/climate/sensor/temperature: 23 +home/livingroom/climate/sensor/humidity: 45 +``` + +## Float switch for water tank + +A Beaglebone Black with a float switch connected to one of its built in GPIO pins which is pulled to ground when the water tank is full and the float switch engages. + +**Example configuration** + +```yaml +mqtt: + host: test.mosquitto.org + port: 1883 + user: "" + password: "" + topic_prefix: home/rainwater + +gpio_modules: + - name: beaglebone_gpio + module: beaglebone + +digital_inputs: + - name: tank + module: beaglebone_gpio + pin: GPIO0_26 + on_payload: full + off_payload: ok + inverted: yes + pullup: yes +``` + +This configuration will poll the `GPIO0_26` pin of the Beaglebone's built in GPIO and publish an MQTT message when it changes from low to high and vice versa: + +```yaml +home/rainwater/input/tank: ok +home/rainwater/input/tank: full +``` + + + + + + diff --git a/docs/2.5.0/config/v2-changes.md b/docs/2.5.0/config/v2-changes.md new file mode 100644 index 00000000..cdf27cc3 --- /dev/null +++ b/docs/2.5.0/config/v2-changes.md @@ -0,0 +1,48 @@ +# V2 Config Changes + +If you've been running MQTT IO (PI MQTT GPIO) before 07-03-2021 then you may need to make some changes to your config file to use with the new version. + +Config files from the v0.x version are mostly compatible, but some sections have been reorganised: + +## MQTT section + +- Moved the Home Assistant discovery options into their own [`ha_discovery` section](https://flyte.github.io/mqtt-io/#/config/reference/mqtt/?id=mqtt-ha_discovery). + +## All IO sections + +_These changes affect the `digital_inputs`, `digital_outputs` and `sensor_inputs` sections._ + +- Added `.*.ha_discovery` section as documented [here (`digital_inputs`)](https://flyte.github.io/mqtt-io/#/config/reference/digital_inputs/?id=digital_inputs-star-ha_discovery), [here (`digital_outputs`)](https://flyte.github.io/mqtt-io/#/config/reference/digital_outputs/?id=digital_outputs-star-ha_discovery) and [here (`sensor_inputs`)](https://flyte.github.io/mqtt-io/#/config/reference/sensor_inputs/?id=sensor_inputs-star-ha_discovery). + +## Sensors + +- Removed `sensor_inputs.*.unit_of_measurement` in favour of adding it implicitly `sensor_inputs.*.ha_discovery.unit_of_measurement` +- Moved `sensor_inputs.*.expire_after` explicitly to `sensor_inputs.*.ha_discovery.expire_after`. + +## Streams + +- Removed `stream_reads` and `stream_writes` in favour of just listing streams in `stream_modules` with `read` and `write` booleans to enable or disable the functionality. + +## Logging + +If you use a `logging` section, you'll need to update the `mqtt_gpio` logger to `mqtt_io` as the module name has changed. + +```yaml +logging: + version: 1 + handlers: + console: + class: logging.StreamHandler + formatter: default + level: DEBUG + formatters: + default: + format: "%(asctime)s %(name)s [%(levelname)-8s] %(message)s" + datefmt: "%Y-%m-%d %H:%M:%S" + loggers: + mqtt_io: # <-- this! + level: DEBUG + handlers: + - console + propagate: yes +``` diff --git a/docs/2.5.0/deployment/docker.md b/docs/2.5.0/deployment/docker.md new file mode 100644 index 00000000..5532304f --- /dev/null +++ b/docs/2.5.0/deployment/docker.md @@ -0,0 +1,31 @@ +# Deployment with Docker + +_Current state: experimental and unmaintained_ + +Two images have been created for Docker. One using the x86_64 architecture (for Intel and AMD CPUs) and one for the ARM architecture (for Raspberry Pi etc.). The tags of the images are therefore `flyte/mqtt-gpio:x86_64` and `flyte/mqtt-gpio:armv7l`. These are the outputs of `uname -m` on the two platforms they've been built on. For the following examples I'll assume you're running on Raspberry Pi. + +You may also run this software using Docker. You must create your config file as above, then run the docker image: + +``` +docker run -ti --rm -v /path/to/your/config.yml:/config.yml flyte/mqtt-gpio:armv7l +``` + +Or to run in the background: + +``` +docker run -d --name mqtt-gpio -v /path/to/your/config.yml:/config.yml flyte/mqtt-gpio:armv7l +``` + +You'll most likely want to use some hardware devices in your config, since that's what this project is all about. For example, if you wish to use the i2c bus, pass it through with a `--device` parameter: + +``` +docker run -ti --rm -v /path/to/your/config.yml:/config.yml --device /dev/i2c-0 flyte/mqtt-gpio:armv7l +``` + +If you aren't able to find the exact device path to use, then you can also run the docker container in `--privileged` mode which will pass all of the devices through from the host: + +``` +docker run -ti --rm -v /path/to/your/config.yml:/config.yml --privileged flyte/mqtt-gpio:armv7l +``` + +_Please raise an issue on Github if you find that any of this information is incorrect._ diff --git a/docs/2.5.0/deployment/supervisor.md b/docs/2.5.0/deployment/supervisor.md new file mode 100644 index 00000000..c87973fc --- /dev/null +++ b/docs/2.5.0/deployment/supervisor.md @@ -0,0 +1,46 @@ +# Deployment using Supervisor + +MQTT IO is not tied to any specific deployment method, but one recommended way is to use `virtualenv` and `supervisor`. This will launch the project at boot time and handle restarting and log file rotation. It's quite simple to set up: + +If using Raspbian, install `supervisor` with `apt`. + +```bash +sudo apt-get update +sudo apt-get install supervisor +``` + +Not strictly necessary, but it's recommended to install the project into a [virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/#lower-level-virtualenv). + +```bash +sudo apt-get install python-venv +cd /home/pi +python3 -m venv ve +. ve/bin/activate +pip install mqtt-io +``` + +Create yourself a config file, following instructions and examples above, and save it somewhere, such as `/home/pi/mqtt-io.yml`. + +Create a [supervisor config file](http://supervisord.org/configuration.html#program-x-section-settings) in /etc/supervisor/conf.d/mqtt-io.conf something along the lines of the following: + +```ini +[program:mqtt_io] +command = /home/pi/ve/bin/python -m mqtt_io mqtt-io.yml +directory = /home/pi +redirect_stderr = true +stdout_logfile = /var/log/mqtt-io.log +``` + +Save the file and then run the following to update supervisor and start the program running. + +```bash +sudo supervisorctl update +``` + +Check the status of your new supervisor job: + +```bash +sudo supervisorctl status +``` + +Check the [supervisor docs](http://supervisord.org/running.html#supervisorctl-command-line-options) for more `supervisorctl` commands. diff --git a/docs/2.5.0/dev/config_schema.md b/docs/2.5.0/dev/config_schema.md new file mode 100644 index 00000000..3aaec928 --- /dev/null +++ b/docs/2.5.0/dev/config_schema.md @@ -0,0 +1,9 @@ +# Config Schema + +The software is configured using a single YAML file specified as the first argument upon execution. + +In order to help avoid any misconfigurations, the provided configuration file is tested against a [Cerberus](https://docs.python-cerberus.org/en/stable/) schema and the program will display errors and exit during its initialisation phase if errors are found. Failing fast is preferable to only failing when, for example, an MQTT is received and the software attempts to control a module accordingly. This enables the user to fix the config while it's still fresh in mind, instead of some arbitrary amount of time down the line when the software may no longer be being supervised. + +The main configuration schema is laid out in `mqtt_io/config/config.schema.yml` and further schema may be optionally set as part of each module in the `CONFIG_SCHEMA` constant. This behaviour allows the modules to optionally require extra configuration specific to them. + +Sensor and stream modules may also specify a config schema to be applied to each of the configured sensors and streams within the `sensor_inputs`, `stream_reads` and `stream_writes` sections. \ No newline at end of file diff --git a/docs/2.5.0/dev/modules/README.md b/docs/2.5.0/dev/modules/README.md new file mode 100644 index 00000000..e0479cb5 --- /dev/null +++ b/docs/2.5.0/dev/modules/README.md @@ -0,0 +1,238 @@ +# Hardware Support Modules + +In order to support as much hardware as possible without changing the project's core code, every supported piece of hardware will have a relevant GPIO, sensor or stream module. These reside in `mqtt_io/modules` within the `gpio`, `sensor` and `stream` folders respectively. In each of these folders is an `__init__.py` file which contains the base class for each type of module. The hardware modules must include a class which overrides this base class and is named `GPIO`, `Sensor` or `Stream`. + +## Requirements + +In order for a module to specify its requirements, a module-level constant is used which lists them in the same format as the `pip install` command. + +[mqtt_io.modules.gpio.raspberrypi:REQUIREMENTS](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/raspberrypi.py#L13): + +```python +REQUIREMENTS = ("RPi.GPIO",) +``` + +## Config Schema + +Along with the base module schema in `mqtt_io/config/config.schema.yml`, each hardware support module is able to specify extra config schema to allow the user to provide the details needed for using the specific hardware. + +To specify extra schema for the module-level config sections (`gpio_modules`, `sensor_modules`, `stream_modules`), a _module-level_ constant called `CONFIG_SCHEMA` is set, containing the [Cerberus Schema](https://docs.python-cerberus.org/en/stable/schemas.html) to add to the base schema. + +To specify extra schema for the pin-level config sections (`digital_inputs`, `digital_outputs`, `sensor_inputs` etc.), a _class-level_ constant called `PIN_SCHEMA` is set on the module's main class (`GPIO`, `Sensor`, `Stream`), containing the [Cerberus Schema](https://docs.python-cerberus.org/en/stable/schemas.html) to add to the base schema. + +If the pin-level schema only applies to an input or an output (in the case of a GPIO module), then instead of setting it on the `PIN_SCHEMA` class-level constant, use `INPUT_SCHEMA` or `OUTPUT_SCHEMA` respectively. + +[mqtt_io.modules.gpio.gpiod:CONFIG_SCHEMA](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/gpiod.py#L18): + +```python +CONFIG_SCHEMA = { + "chip": {"type": "string", "required": False, "default": "/dev/gpiochip0"} +} +``` + +## GPIO Modules + +... + +### Lifecycle + +#### `setup_module()` + +During software startup, each GPIO module's `setup_module()` method will be called once per instance of the module in the `gpio_modules` section of the config file. + +[mqtt_io.modules.gpio:GenericGPIO.setup_module](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/__init__.py#L109): + +```python +def setup_module(self) -> None: + """ + Called on initialisation of the GPIO module during the startup phase. + + The module's config from the `gpio_modules` section of the config file is stored + in `self.config`. + """ +``` + +For example, the `pcf8574` module's `setup_module()` method will be called twice given the following config: + +```yaml +gpio_modules: + - name: pcf1 + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x20 + + - name: pcf2 + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x21 +``` + +Within this method we import any Python dependencies. It's important to not do this at module level, so that the core code is able to import the module before its dependencies are installed. + +The GPIO library is then initialised and an object may be stored (usually at `self.io`) for use by the module during runtime. + +It may be appropriate to build mappings of pin directions (input, output), pullups (up, down, off) and interrupt edges (rising, falling, both) if appropriate for this hardware. The base GenericGPIO class uses its own constants to refer to these, so the mappings translate the base GenericGPIO's constants to ones used by the hardware's Python library. + +[mqtt_io.modules.gpio:PinDirection](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/__init__.py#L22): + +```python +class PinDirection(Enum): + """ + Whether the GPIO pin is an input or an output. + """ + + INPUT = auto() + OUTPUT = auto() +``` + +[mqtt_io.modules.gpio:PinPUD](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/__init__.py#L31): + +```python +class PinPUD(Enum): + """ + Whether the GPIO pin should be pulled up, down or not anywhere. + """ + + OFF = auto() + UP = auto() + DOWN = auto() +``` + +[mqtt_io.modules.gpio:InterruptEdge](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/__init__.py#L41): + +```python +class InterruptEdge(Enum): + """ + Whether to trigger an interrupt on rising edge, falling edge or both. + """ + + RISING = auto() + FALLING = auto() + BOTH = auto() +``` + +The `raspberrypi` GPIO module is a good example of the above: + +[mqtt_io.modules.gpio.raspberrypi:GPIO.setup_module](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/raspberrypi.py#L23): + +```python +def setup_module(self) -> None: + # pylint: disable=import-outside-toplevel,import-error + import RPi.GPIO as gpio # type: ignore + + self.io = gpio + self.direction_map = {PinDirection.INPUT: gpio.IN, PinDirection.OUTPUT: gpio.OUT} + + self.pullup_map = { + PinPUD.OFF: gpio.PUD_OFF, + PinPUD.UP: gpio.PUD_UP, + PinPUD.DOWN: gpio.PUD_DOWN, + } + + self.interrupt_edge_map = { + InterruptEdge.RISING: gpio.RISING, + InterruptEdge.FALLING: gpio.FALLING, + InterruptEdge.BOTH: gpio.BOTH, + } + + gpio.setmode(gpio.BCM) +``` + +#### Polling Loop + +If a digital input is not configured as an [interrupt](config/interrupts.md) (or even [sometimes if it is](config/reference/digital_inputs/?id=digital_inputs-star-interrupt_for)), then a loop will be created which polls the pin's current value and publishes a `DigitalInputChangedEvent` event when it does. As part of the initialisation of each pin, a callback function to publish the new value on MQTT will be subscribed to this event. + +[mqtt_io.server.MqttIo._init_digital_inputs](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/server.py#L377): + +```python +def _init_digital_inputs(self) -> None: + async def publish_callback(event: DigitalInputChangedEvent) -> None: + in_conf = self.digital_input_configs[event.input_name] + value = event.to_value != in_conf["inverted"] + val = in_conf["on_payload"] if value else in_conf["off_payload"] + self.mqtt_task_queue.put_nowait( + PriorityCoro( + self._mqtt_publish( + MQTTMessageSend( + "/".join( + ( + self.config["mqtt"]["topic_prefix"], + INPUT_TOPIC, + event.input_name, + ) + ), + val.encode("utf8"), + retain=in_conf["retain"], + ) + ), + MQTT_PUB_PRIORITY, + ) + ) + self.event_bus.subscribe(DigitalInputChangedEvent, publish_callback) +``` + +#### `setup_pin()` + +For each of the entries in `digital_inputs` and `digital_outputs`, `setup_pin()` will be called. This step is for configuring the hardware's pins to be input or outputs, or anything else that must be set at pin level. + +[mqtt_io.modules.gpio:GenericGPIO.setup_pin](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/__init__.py#L118): + +```python +def setup_pin( + self, + pin: PinType, + direction: PinDirection, + pullup: PinPUD, + pin_config: ConfigType, + initial: Optional[str] = None, +) -> None: + """ + Called on initialisation of each pin of the GPIO module during the startup phase. + + The `pin_config` passed in here is the pin's entry in the `digital_inputs` or + `digital_outputs` section of the config file. + """ +``` + +For example, it would be called three times given the following configuration: + +```yaml +digital_inputs: + - name: doorbell + module: raspberrypi + pin: 1 + + - name: lightswitch + module: raspberrypi + pin: 2 + +digital_outputs: + - name: lights + module: raspberrypi + pin: 3 +``` + +Here's the `raspberrypi` GPIO module's `setup_pin()` implementation: + +[mqtt_io.modules.gpio.raspberrypi:GPIO.setup_pin](https://github.com/flyte/mqtt-io/blob/38ded49cb917733aafab18d3bf50b74ae3fee014/mqtt_io/modules/gpio/raspberrypi.py#L44): + +```python +def setup_pin( + self, + pin: PinType, + direction: PinDirection, + pullup: PinPUD, + pin_config: ConfigType, + initial: Optional[str] = None, +) -> None: + direction = self.direction_map[direction] + pullup = self.pullup_map[pullup] + + initial_int = {None: -1, "low": 0, "high": 1}[initial] + self.io.setup(pin, direction, pull_up_down=pullup, initial=initial_int) +``` + + +## TODO + +- Define when 'inverted' values are expected to be inverted or raw. \ No newline at end of file diff --git a/docs/2.5.0/dev/modules/README.md.j2 b/docs/2.5.0/dev/modules/README.md.j2 new file mode 100644 index 00000000..f009c9fe --- /dev/null +++ b/docs/2.5.0/dev/modules/README.md.j2 @@ -0,0 +1,166 @@ +# Hardware Support Modules + +In order to support as much hardware as possible without changing the project's core code, every supported piece of hardware will have a relevant GPIO, sensor or stream module. These reside in `mqtt_io/modules` within the `gpio`, `sensor` and `stream` folders respectively. In each of these folders is an `__init__.py` file which contains the base class for each type of module. The hardware modules must include a class which overrides this base class and is named `GPIO`, `Sensor` or `Stream`. + +## Requirements + +In order for a module to specify its requirements, a module-level constant is used which lists them in the same format as the `pip install` command. + +{{ source( + "mqtt_io.modules.gpio.raspberrypi", + "//Assign[targets/Name/id='REQUIREMENTS']", + "mqtt_io.modules.gpio.raspberrypi:REQUIREMENTS" +)}} + +## Config Schema + +Along with the base module schema in `mqtt_io/config/config.schema.yml`, each hardware support module is able to specify extra config schema to allow the user to provide the details needed for using the specific hardware. + +To specify extra schema for the module-level config sections (`gpio_modules`, `sensor_modules`, `stream_modules`), a _module-level_ constant called `CONFIG_SCHEMA` is set, containing the [Cerberus Schema](https://docs.python-cerberus.org/en/stable/schemas.html) to add to the base schema. + +To specify extra schema for the pin-level config sections (`digital_inputs`, `digital_outputs`, `sensor_inputs` etc.), a _class-level_ constant called `PIN_SCHEMA` is set on the module's main class (`GPIO`, `Sensor`, `Stream`), containing the [Cerberus Schema](https://docs.python-cerberus.org/en/stable/schemas.html) to add to the base schema. + +If the pin-level schema only applies to an input or an output (in the case of a GPIO module), then instead of setting it on the `PIN_SCHEMA` class-level constant, use `INPUT_SCHEMA` or `OUTPUT_SCHEMA` respectively. + +{{ source( + "mqtt_io.modules.gpio.gpiod", + "//Assign[targets/Name/id='CONFIG_SCHEMA']", + "mqtt_io.modules.gpio.gpiod:CONFIG_SCHEMA" +)}} + +## GPIO Modules + +... + +### Lifecycle + +#### `setup_module()` + +During software startup, each GPIO module's `setup_module()` method will be called once per instance of the module in the `gpio_modules` section of the config file. + +{{ source( + "mqtt_io.modules.gpio", + "//ClassDef[name='GenericGPIO']//FunctionDef[name='setup_module']", + "mqtt_io.modules.gpio:GenericGPIO.setup_module" +) }} + +For example, the `pcf8574` module's `setup_module()` method will be called twice given the following config: + +```yaml +gpio_modules: + - name: pcf1 + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x20 + + - name: pcf2 + module: pcf8574 + i2c_bus_num: 1 + chip_addr: 0x21 +``` + +Within this method we import any Python dependencies. It's important to not do this at module level, so that the core code is able to import the module before its dependencies are installed. + +The GPIO library is then initialised and an object may be stored (usually at `self.io`) for use by the module during runtime. + +It may be appropriate to build mappings of pin directions (input, output), pullups (up, down, off) and interrupt edges (rising, falling, both) if appropriate for this hardware. The base GenericGPIO class uses its own constants to refer to these, so the mappings translate the base GenericGPIO's constants to ones used by the hardware's Python library. + +{{ source( + "mqtt_io.modules.gpio", + "//ClassDef[name='PinDirection']", + "mqtt_io.modules.gpio:PinDirection" +) }} + +{{ source( + "mqtt_io.modules.gpio", + "//ClassDef[name='PinPUD']", + "mqtt_io.modules.gpio:PinPUD" +) }} + +{{ source( + "mqtt_io.modules.gpio", + "//ClassDef[name='InterruptEdge']", + "mqtt_io.modules.gpio:InterruptEdge" +) }} + +The `raspberrypi` GPIO module is a good example of the above: + +{{ source( + "mqtt_io.modules.gpio.raspberrypi", + "//ClassDef[name='GPIO']//FunctionDef[name='setup_module']", + "mqtt_io.modules.gpio.raspberrypi:GPIO.setup_module" +) }} + +#### Polling Loop + +If a digital input is not configured as an [interrupt](config/interrupts.md) (or even [sometimes if it is](config/reference/digital_inputs/?id=digital_inputs-star-interrupt_for)), then a loop will be created which polls the pin's current value and publishes a `DigitalInputChangedEvent` event when it does. As part of the initialisation of each pin, a callback function to publish the new value on MQTT will be subscribed to this event. + +{{ source_link( + "mqtt_io.server", + "//ClassDef[name='MqttIo']//FunctionDef[name='_init_digital_inputs']", + "mqtt_io.server.MqttIo._init_digital_inputs" +)}} + +```python +{{ sources_raw( + [ + ( + "mqtt_io.server", + "//ClassDef[name='MqttIo']//FunctionDef[name='_init_digital_inputs']", + "//ClassDef[name='MqttIo']//FunctionDef[name='_init_digital_inputs']/body/*[1]" + ), + ( + "mqtt_io.server", + "//ClassDef[name='MqttIo']//FunctionDef[name='_init_digital_inputs']//AsyncFunctionDef[name='publish_callback']", + None + ), + ( + "mqtt_io.server", + "//ClassDef[name='MqttIo']//FunctionDef[name='_init_digital_inputs']//Call[args//id='DigitalInputChangedEvent']", + None + ) + ], + dedent=True +)}} +``` + +#### `setup_pin()` + +For each of the entries in `digital_inputs` and `digital_outputs`, `setup_pin()` will be called. This step is for configuring the hardware's pins to be input or outputs, or anything else that must be set at pin level. + +{{ source( + "mqtt_io.modules.gpio", + "//ClassDef[name='GenericGPIO']//FunctionDef[name='setup_pin']", + "mqtt_io.modules.gpio:GenericGPIO.setup_pin" +) }} + +For example, it would be called three times given the following configuration: + +```yaml +digital_inputs: + - name: doorbell + module: raspberrypi + pin: 1 + + - name: lightswitch + module: raspberrypi + pin: 2 + +digital_outputs: + - name: lights + module: raspberrypi + pin: 3 +``` + +Here's the `raspberrypi` GPIO module's `setup_pin()` implementation: + +{{ source( + "mqtt_io.modules.gpio.raspberrypi", + "//ClassDef[name='GPIO']//FunctionDef[name='setup_pin']", + "mqtt_io.modules.gpio.raspberrypi:GPIO.setup_pin" +) }} + + +## TODO + +- Define when 'inverted' values are expected to be inverted or raw. diff --git a/docs/2.5.0/generate_docs.py b/docs/2.5.0/generate_docs.py new file mode 100644 index 00000000..588ee3fc --- /dev/null +++ b/docs/2.5.0/generate_docs.py @@ -0,0 +1,455 @@ +import ast +import json +import os +import pathlib +import re +import shutil +import textwrap +from contextlib import contextmanager +from importlib import import_module +from os import environ as env +from os.path import join +from tempfile import TemporaryDirectory +from typing import Any, Dict, Iterator, List, Optional, Set, Tuple + +import semver +import yaml +from ast_to_xml import module_source +from git import Repo +from jinja2 import Template + +from mqtt_io.types import ConfigType + +GITHUB_REPO = "https://github.com/flyte/mqtt-io" + +WORKSPACE_DIR = pathlib.Path(__file__).parent.parent.absolute() + +CONFIG_SCHEMA_PATH = join(WORKSPACE_DIR, "mqtt_io/config/config.schema.yml") +README_TEMPLATE = join(WORKSPACE_DIR, "README.md.j2") +MODULES_DIR = join(WORKSPACE_DIR, "mqtt_io/modules") + +DOCS_SRC_DIR = join(WORKSPACE_DIR, "docs_src") + +DOCS_DIR = join(WORKSPACE_DIR, "docs") + +SIDEBAR_TEMPLATE = join(DOCS_SRC_DIR, "_sidebar.md.j2") +CONTENT_TEMPLATE = join(DOCS_SRC_DIR, "config/reference.md.j2") +MODULES_DOC_TEMPLATE = join(DOCS_SRC_DIR, "dev/modules/README.md.j2") +VERSIONS_TEMPLATE = join(DOCS_SRC_DIR, "versions.md.j2") + +MAIN_INDEX = join(DOCS_DIR, "index.html") +VERSIONS_FILE = join(DOCS_DIR, "versions.md") + +REF_ENTRIES: List[Dict[str, Any]] = [] + +REPO = Repo(str(WORKSPACE_DIR)) +# REPO_WAS_DIRTY = REPO.is_dirty() + + +def head() -> Any: + try: + ret = REPO.active_branch + except TypeError: + ret = next((tag for tag in REPO.tags if tag.commit == REPO.head.commit), None) + if ret is None: + ret = REPO.head + return ret + + +HEAD = head() +REF_NAME = str(HEAD) + + +def get_build_dir() -> str: + docs_dir = join(DOCS_DIR, REF_NAME) + os.makedirs(docs_dir, exist_ok=True) + return docs_dir + + +BUILD_DIR = get_build_dir() + + +@contextmanager +def gh_pages_branch() -> Iterator[None]: + previous_head = head() + repo_was_dirty = REPO.is_dirty() + if repo_was_dirty: + print("Stashing dirty repo...") + REPO.git.stash() + print("Checking out 'gh-pages'...") + REPO.heads["gh-pages"].checkout(force=True) + try: + yield + finally: + print(f"Checking out '{previous_head}'...") + REPO.git.checkout(REF_NAME) + if repo_was_dirty: + print("Popping stashed changes...") + REPO.git.stash("pop") + + +def get_version_list() -> List[str]: + with gh_pages_branch(): + return next(os.walk(DOCS_DIR))[1] + + +def commit_to_gh_pages_branch( + docs_path: str, versions_contents: str, main_index_contents: Optional[str] +) -> None: + with gh_pages_branch(): + print("Pulling gh-pages branch...") + REPO.git.pull() + print("Writing versions file...") + with open(VERSIONS_FILE, "w") as versions_file: + versions_file.write(versions_contents) + if main_index_contents is not None: + print("Writing main index file...") + with open(MAIN_INDEX, "w") as main_index_file: + main_index_file.write(main_index_contents) + shutil.rmtree(BUILD_DIR) + shutil.copytree(docs_path, BUILD_DIR) + for path in (BUILD_DIR, MAIN_INDEX, VERSIONS_FILE): + print(f"Adding '{path}' to git index...") + REPO.index.add([path]) + print("Committing...") + REPO.index.commit(f"Generate {REF_NAME} docs") + print("Pushing gh-pages branch to origin...") + REPO.remotes.origin.push() + + +def copy_docs_src(docs_path: str) -> None: + shutil.copytree(DOCS_SRC_DIR, docs_path, dirs_exist_ok=True) + + +def sort_semver_versions(versions: Set[str]) -> List[str]: + semver_versions = set() + for v in versions: + try: + semver_versions.add(semver.VersionInfo.parse(v)) + except ValueError: + continue + return list(map(str, sorted(list(semver_versions), reverse=True))) + + +def generate_main_index(versions: Set[str]) -> str: + semver_versions = sort_semver_versions(versions) + try: + highest_version = semver_versions[0] + except IndexError: + highest_version = REF_NAME + print(f"Generating main index to redirect to {highest_version}") + return f"""\ + + + + + + + + Redirecting to '{highest_version}' documentation version... + + + + + +""" + + +def title_id(entry_name: str, parents: List[str]) -> str: + tid = "" + if parents: + tid += ("-".join(parents)) + "-" + tid += entry_name + return tid.replace("*", "star") + + +class ConfigSchemaParser: + @staticmethod + def parse_schema_section( + section: ConfigType, + container: List[Dict[str, Any]], + parents: Optional[List[str]] = None, + ) -> None: + if parents is None: + parents = [] + else: + parents = parents.copy() + + child_schema = section.get("schema") + if child_schema: + parents.append("*") + ConfigSchemaParser.parse_schema_section(child_schema, container, parents) + return + + for entry_name in section.keys(): + entry: ConfigType = section[entry_name] + ConfigSchemaParser.parse_cerberus_section( + entry_name, entry, container, parents + ) + + @staticmethod + def parse_cerberus_section( + entry_name: str, + section: ConfigType, + container: List[Dict[str, Any]], + parents: List[str], + ) -> None: + parents = parents.copy() + child_schema = section.get("schema") + + children: List[Dict[str, Any]] = [] + toplevel_name = parents[0] if parents else entry_name + + depth = len([x for x in parents if x != "*"]) + tid = title_id(entry_name, parents) + section.setdefault("meta", {})["title_id"] = tid + path = f"config/reference/{toplevel_name}/" + if parents: + path += f"?id={tid}" + parents_str = ".".join(parents).replace("*", "*") + entry_str = entry_name.replace("*", "*") + subtitle = f"*{parents_str}*.**{entry_str}**" if parents else None + + entry = dict( + toplevel_name=toplevel_name, + title=entry_name if entry_name != "*" else f"{parents[-1]}.*", + subtitle=subtitle, + entry_name=entry_name, + element_id=tid, + depth=depth, + path=path, + schema=section, + meta=section.get("meta", {}), + ) + container.append(entry) + REF_ENTRIES.append(entry) + + if child_schema: + parents.append(entry_name) + if "type" in child_schema: + ConfigSchemaParser.parse_cerberus_section( + "*", child_schema, children, parents + ) + else: + ConfigSchemaParser.parse_schema_section(child_schema, children, parents) + + +def generate_readmes(docs_path: str) -> None: + blacklist = ("__init__", "mock", "stdio") + modules_and_titles = ( + ("gpio", "GPIO Modules"), + ("sensor", "Sensors"), + ("stream", "Streams"), + ) + module_strings: Dict[str, Dict[str, str]] = {} + for module_type, title in modules_and_titles: + for file_name, ext in [ + os.path.splitext(x) for x in os.listdir(join(MODULES_DIR, module_type)) + ]: + if ext != ".py" or file_name in blacklist: + continue + with open(join(MODULES_DIR, module_type, file_name + ext)) as module_file: + parsed = ast.parse(module_file.read()) + expr = parsed.body[0] + assert ( + expr.lineno == 1 + and isinstance(expr, ast.Expr) + and hasattr(expr, "value") + and isinstance(expr.value, ast.Constant) + and isinstance(expr.value.value, str) + ), f"The {module_type}.{file_name} module should have a docstring at the top" + module_strings.setdefault(title, {})[file_name] = expr.value.value.strip() + + with open(README_TEMPLATE) as readme_template_file: + readme_template: Template = Template(readme_template_file.read()) + + ctx = dict(supported_hardware=module_strings, version=REF_NAME) + + with open(join(WORKSPACE_DIR, "README.md"), "w") as readme_file: + readme_file.write(readme_template.render(dict(**ctx, repo=True))) + + with open(join(docs_path, "README.md"), "w") as readme_file: + readme_file.write(readme_template.render(dict(**ctx, repo=False))) + + +def generate_changelog(docs_path: str) -> None: + print("Copying changelog...") + shutil.copyfile(join(WORKSPACE_DIR, "CHANGELOG.md"), join(docs_path, "CHANGELOG.md")) + + +def document_gpio_module() -> None: + # TODO: Tasks pending completion -@flyte at 07/03/2021, 11:19:04 + # Continue writing this to document the modules in some way. + module = import_module("mqtt_io.modules.gpio.raspberrypi") + requirements = getattr(module, "REQUIREMENTS", None) + config_schema = getattr(module, "CONFIG_SCHEMA", None) + interrupt_support = getattr(module.GPIO, "INTERRUPT_SUPPORT", None) + pin_schema = getattr(module.GPIO, "PIN_SCHEMA", None) + input_schema = getattr(module.GPIO, "INPUT_SCHEMA", None) + output_schema = getattr(module.GPIO, "OUTPUT_SCHEMA", None) + + +# def get_source(path: str) -> str: +# module_path, member_path = re.match(r"([\w\.]+):?(.*)", path).groups() +# module = import_module(module_path) +# module_filepath = pathlib.Path(module.__file__) +# url = f"https://github.com/flyte/mqtt-io/blob/develop/{module_filepath.relative_to(THIS_DIR)}" +# if not member_path: +# return f"[`{path}`]({url}):\n\n```python\n{inspect.getsource(module)}```" +# target = module +# for member in member_path.split("."): +# target = getattr(target, member) +# _, lineno = inspect.getsourcelines(target) +# url += f"#L{lineno}" +# return f"[`{path}`]({url}):\n\n```python\n{dedent(inspect.getsource(target))}```" + + +def get_source(module_path: str, xpath: str, title: str) -> str: + module = import_module(module_path) + module_filepath = pathlib.Path(module.__file__) + src, attrib = module_source(module, xpath)[0] + url = "%s/blob/%s/%s#L%s" % ( + GITHUB_REPO, + HEAD.commit.hexsha, + module_filepath.relative_to(WORKSPACE_DIR), + attrib["lineno"], + ) + return f"[{title}]({url}):\n\n```python\n{src.rstrip()}\n```" + + +def get_source_link(module_path: str, xpath: str, title: str) -> str: + module = import_module(module_path) + module_filepath = pathlib.Path(module.__file__) + _, attrib = module_source(module, xpath)[0] + url = "%s/blob/%s/%s#L%s" % ( + GITHUB_REPO, + HEAD.commit.hexsha, + module_filepath.relative_to(WORKSPACE_DIR), + attrib["lineno"], + ) + return f"[{title}]({url}):" + + +def get_source_raw( + module_path: str, xpath: str, until_xpath: Optional[str] = None, dedent: bool = False +) -> str: + module = import_module(module_path) + src, _ = module_source(module, xpath, until_xpath=until_xpath, dedent=dedent)[0] + return src + + +def get_sources_raw( + sources_spec: List[Tuple[str, str, Optional[str]]], dedent: bool = False +) -> str: + src = "\n".join(get_source_raw(*x, dedent=False) for x in sources_spec) + if dedent: + src = textwrap.dedent(src) + return src + + +def generate_modules_doc(docs_path: str) -> None: + ctx = dict( + source=get_source, + source_link=get_source_link, + source_raw=get_source_raw, + sources_raw=get_sources_raw, + ) + + print("Loading modules doc template...") + with open(MODULES_DOC_TEMPLATE) as modules_doc_template_file: + modules_doc_template: Template = Template(modules_doc_template_file.read()) + + print("Generating modules doc...") + modules_dir = join(docs_path, "dev/modules") + os.makedirs(modules_dir, exist_ok=True) + with open(join(modules_dir, "README.md"), "w") as readme_file: + readme_file.write(modules_doc_template.render(ctx)) + + +def generate_versions(versions: Set[str]) -> str: + release_versions = sort_semver_versions(versions) + other_versions_set = set() + for version in versions: + if version not in release_versions: + other_versions_set.add(version) + other_versions: List[str] = sorted(list(other_versions_set)) + + ctx = dict(releases=release_versions, other_versions=other_versions) + + with open(VERSIONS_TEMPLATE) as versions_template_file: + versions_template: Template = Template(versions_template_file.read()) + + return versions_template.render(ctx) + + +def generate_docs(docs_path: str) -> None: + print(f"Loading YAML config schema from '{CONFIG_SCHEMA_PATH}'...") + with open(CONFIG_SCHEMA_PATH, "r") as config_schema_file: + config_schema: ConfigType = yaml.safe_load(config_schema_file) + + print(f"Loading sidebar template from '{SIDEBAR_TEMPLATE}'...") + with open(SIDEBAR_TEMPLATE, "r") as sidebar_template_file: + sidebar_template: Template = Template(sidebar_template_file.read()) + + print(f"Loading content template from '{CONTENT_TEMPLATE}'...") + with open(CONTENT_TEMPLATE, "r") as content_template_file: + content_template: Template = Template(content_template_file.read()) + + top_level_section_names: List[str] = list(config_schema.keys()) + ConfigSchemaParser.parse_schema_section(config_schema, []) + + versions = set(get_version_list()) + versions.add(REF_NAME) + + copy_docs_src(docs_path) + + main_sidebar_path = join(docs_path, "_sidebar.md") + print(f"Writing main sidebar file '{main_sidebar_path}'...") + with open(main_sidebar_path, "w") as main_sidebar_file: + main_sidebar_file.write( + sidebar_template.render( + dict(ref_sections=[x for x in REF_ENTRIES if x["depth"] == 0]) + ) + ) + + for tl_section in top_level_section_names: + section_path = join(docs_path, f"config/reference/{tl_section}") + md_path = join(section_path, "README.md") + + print(f"Making directory (if not exists) '{section_path}'...") + os.makedirs(section_path, exist_ok=True) + + print(f"Making section markdown file '{md_path}'...") + with open(md_path, "w") as md_file: + md_file.write( + content_template.render( + dict(ref_sections=REF_ENTRIES, section=tl_section) + ) + ) + + json_schema_path = join(docs_path, "schema.json") + print(f"Making JSON config schema file '{json_schema_path}'...") + with open(json_schema_path, "w") as json_schema_file: + json.dump(config_schema, json_schema_file, indent=2) + + # generate_module_docs() + generate_readmes(docs_path) + generate_changelog(docs_path) + generate_modules_doc(docs_path) + + versions_contents = generate_versions(versions) + main_index_contents = generate_main_index(versions) + + commit_to_gh_pages_branch(docs_path, versions_contents, main_index_contents) + + +def main() -> None: + with TemporaryDirectory() as tempdir: + generate_docs(tempdir) + + +if __name__ == "__main__": + main() diff --git a/docs/2.5.0/index.html b/docs/2.5.0/index.html new file mode 100644 index 00000000..b82747f0 --- /dev/null +++ b/docs/2.5.0/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + +
+ + + + + + + + + diff --git a/docs/2.5.0/schema.json b/docs/2.5.0/schema.json new file mode 100644 index 00000000..4f2cde88 --- /dev/null +++ b/docs/2.5.0/schema.json @@ -0,0 +1,998 @@ +{ + "mqtt": { + "meta": { + "description": "Contains the configuration data used for connecting to an MQTT server.\n", + "yaml_example": "mqtt:\n host: test.mosquitto.org\n port: 8883\n topic_prefix: mqtt_io\n ha_discovery:\n enabled: yes\n tls:\n enabled: yes\n ca_certs: mosquitto.org.crt\n certfile: client.crt\n keyfile: client.key\n", + "title_id": "mqtt" + }, + "type": "dict", + "required": true, + "schema": { + "host": { + "meta": { + "description": "Host name or IP address of the MQTT server.", + "title_id": "mqtt-host" + }, + "type": "string", + "empty": false, + "required": true + }, + "port": { + "meta": { + "description": "Port number to connect to on the MQTT server.", + "title_id": "mqtt-port" + }, + "type": "integer", + "min": 1, + "max": 65535, + "required": false, + "default": 1883 + }, + "user": { + "meta": { + "description": "Username to authenticate with on the MQTT server.", + "title_id": "mqtt-user" + }, + "type": "string", + "required": false, + "default": "" + }, + "password": { + "meta": { + "description": "Password to authenticate with on the MQTT server.", + "title_id": "mqtt-password" + }, + "type": "string", + "required": false, + "default": "" + }, + "client_id": { + "meta": { + "description": "[MQTT client ID](https://www.cloudmqtt.com/blog/2018-11-21-mqtt-what-is-client-id.html) to use on the MQTT server.\n", + "title_id": "mqtt-client_id" + }, + "type": "string", + "required": false, + "default": "" + }, + "topic_prefix": { + "meta": { + "description": "Prefix to use for all topics.", + "extra_info": "For example, a `topic_prefix` of `home/livingroom` would make a digital input\ncalled \"doorbell\" publish its changes to the `home/livingroom/input/doorbell`\ntopic.\n", + "title_id": "mqtt-topic_prefix" + }, + "type": "string", + "required": false, + "default": "", + "coerce": "rstrip_slash" + }, + "clean_session": { + "meta": { + "description": "Whether or not to start a\n[clean MQTT session](https://www.hivemq.com/blog/mqtt-essentials-part-7-persistent-session-queuing-messages/)\non every MQTT connection.\n", + "title_id": "mqtt-clean_session" + }, + "type": "boolean", + "required": false, + "default": false + }, + "protocol": { + "meta": { + "description": "Version of the MQTT protocol to use.", + "extra_info": "This renders in the documentation as a float, but should always be set within quotes.\n", + "title_id": "mqtt-protocol" + }, + "type": "string", + "required": false, + "empty": false, + "coerce": "tostring", + "default": "3.1.1", + "allowed": [ + "3.1", + "3.1.1" + ] + }, + "keepalive": { + "meta": { + "description": "How frequently in seconds to send\n[ping packets](https://www.hivemq.com/blog/mqtt-essentials-part-10-alive-client-take-over/)\nto the MQTT server.\n", + "unit": "seconds", + "title_id": "mqtt-keepalive" + }, + "type": "integer", + "min": 1, + "required": false, + "default": 10 + }, + "status_topic": { + "meta": { + "description": "Topic on which to send messages about the running status of this software.", + "extra_info": "Sends the payloads configured in `status_payload_running`,\n`status_payload_stopped` and `status_payload_dead`.\n", + "title_id": "mqtt-status_topic" + }, + "type": "string", + "required": false, + "default": "status" + }, + "status_payload_running": { + "meta": { + "description": "Payload to send on the status topic when the software is running.", + "title_id": "mqtt-status_payload_running" + }, + "type": "string", + "required": false, + "default": "running" + }, + "status_payload_stopped": { + "meta": { + "description": "Payload to send on the status topic when the software has exited cleanly.", + "title_id": "mqtt-status_payload_stopped" + }, + "type": "string", + "required": false, + "default": "stopped" + }, + "status_payload_dead": { + "meta": { + "description": "Payload to send on the status topic when the software has exited unexpectedly.", + "extra_info": "Uses [MQTT Last Will and Testament](https://www.hivemq.com/blog/mqtt-essentials-part-9-last-will-and-testament/)\nto make the server automatically send this payload if our connection fails.\n", + "title_id": "mqtt-status_payload_dead" + }, + "type": "string", + "required": false, + "default": "dead" + }, + "client_module": { + "meta": { + "description": "MQTT Client implementation module path.", + "extra_info": "There's currently only one implementation, which uses the\n[aiomqtt](https://github.com/sbtinstruments/aiomqtt/) client.\n", + "title_id": "mqtt-client_module" + }, + "type": "string", + "required": false, + "default": "mqtt_io.mqtt.aiomqtt" + }, + "ha_discovery": { + "type": "dict", + "required": false, + "schema": { + "enabled": { + "meta": { + "description": "Enable [Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/)\nof our configured devices.\n", + "title_id": "mqtt-ha_discovery-enabled" + }, + "type": "boolean", + "required": true + }, + "prefix": { + "meta": { + "description": "Prefix for the Home Assistant MQTT discovery topic.", + "title_id": "mqtt-ha_discovery-prefix" + }, + "type": "string", + "required": false, + "default": "homeassistant", + "coerce": "rstrip_slash" + }, + "name": { + "meta": { + "description": "Name to identify this \"device\" in Home Assistant.", + "title_id": "mqtt-ha_discovery-name" + }, + "type": "string", + "required": false, + "default": "MQTT IO" + } + }, + "meta": { + "title_id": "mqtt-ha_discovery" + } + }, + "tls": { + "meta": { + "description": "TLS/SSL settings for connecting to the MQTT server over an encrypted connection.\n", + "yaml_example": "mqtt:\n host: localhost\n tls:\n enabled: yes\n ca_certs: mosquitto.org.crt\n certfile: client.crt\n keyfile: client.key\n", + "title_id": "mqtt-tls" + }, + "type": "dict", + "required": false, + "schema": { + "enabled": { + "meta": { + "description": "Enable a secure connection to the MQTT server.", + "extra_info": "Most of these options map directly to the\n[`tls_set()` arguments](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\non the Paho MQTT client.\n", + "title_id": "mqtt-tls-enabled" + }, + "type": "boolean", + "required": true + }, + "ca_certs": { + "meta": { + "description": "Path to the Certificate Authority certificate files that are to be treated\nas trusted by this client.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "title_id": "mqtt-tls-ca_certs" + }, + "type": "string", + "required": false + }, + "certfile": { + "meta": { + "description": "Path to the PEM encoded client certificate.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "title_id": "mqtt-tls-certfile" + }, + "type": "string", + "required": false + }, + "keyfile": { + "meta": { + "description": "Path to the PEM encoded client private key.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "title_id": "mqtt-tls-keyfile" + }, + "type": "string", + "required": false + }, + "cert_reqs": { + "meta": { + "description": "Defines the certificate requirements that the client imposes on the MQTT server.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "extra_info": "By default this is `CERT_REQUIRED`, which means that the broker must provide a certificate.\n", + "title_id": "mqtt-tls-cert_reqs" + }, + "type": "string", + "required": false, + "allowed": [ + "CERT_NONE", + "CERT_OPTIONAL", + "CERT_REQUIRED" + ], + "default": "CERT_REQUIRED" + }, + "tls_version": { + "meta": { + "description": "Specifies the version of the SSL/TLS protocol to be used.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "extra_info": "By default the highest TLS version is detected.\n", + "title_id": "mqtt-tls-tls_version" + }, + "type": "string", + "required": false + }, + "ciphers": { + "meta": { + "description": "Which encryption ciphers are allowable for this connection.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-set)\n", + "title_id": "mqtt-tls-ciphers" + }, + "type": "string", + "required": false + }, + "insecure": { + "meta": { + "description": "Configure verification of the server hostname in the server certificate.\n[More info](https://www.eclipse.org/paho/index.php?page=clients/python/docs/index.php#tls-insecure-set)\n", + "extra_info": "If set to true, it is impossible to guarantee that the host you are\nconnecting to is not impersonating your server. This can be useful in\ninitial server testing, but makes it possible for a malicious third party\nto impersonate your server through DNS spoofing, for example.\nDo not use this function in a real system. Setting value to true means there\nis no point using encryption.\n", + "title_id": "mqtt-tls-insecure" + }, + "type": "boolean", + "required": false, + "default": false + } + } + }, + "reconnect_delay": { + "meta": { + "description": "Time in seconds to wait between reconnect attempts.\n", + "title_id": "mqtt-reconnect_delay" + }, + "type": "integer", + "required": false, + "default": 2, + "min": 1 + }, + "reconnect_count": { + "meta": { + "description": "Max number of retries of connections before giving up and exiting.\nNull value means infinite reconnects (default).\nThe counter is reset when the connection is reestablished successfully.\n", + "title_id": "mqtt-reconnect_count" + }, + "type": "integer", + "required": false, + "default": null, + "nullable": true, + "min": 0 + } + } + }, + "gpio_modules": { + "meta": { + "description": "List of GPIO modules to configure for use with inputs and/or outputs.\n", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`CONFIG_SCHEMA` value of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "gpio_modules:\n - name: rpi_gpio\n module: raspberrypi\n \n - name: pcf\n module: pcf8574\n i2c_bus_num: 1\n chip_addr: 0x20\n", + "title_id": "gpio_modules" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Your name for this configuration of the module. Will be referred to by entries\nin the `digital_inputs` and/or `digital_outputs` sections.\n", + "title_id": "gpio_modules-star-name" + }, + "type": "string", + "required": true, + "empty": false + }, + "module": { + "meta": { + "description": "Name of the module in the code. This is listed in the README's\n\"Supported Hardware\" section in brackets.\n", + "title_id": "gpio_modules-star-module" + }, + "type": "string", + "required": true, + "empty": false + }, + "cleanup": { + "meta": { + "description": "Whether to run the module's `cleanup()` method on exit.", + "title_id": "gpio_modules-star-cleanup" + }, + "type": "boolean", + "required": false, + "default": true + } + }, + "meta": { + "title_id": "gpio_modules-star" + } + } + }, + "sensor_modules": { + "meta": { + "description": "List of sensor modules to configure for use with sensor inputs.", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`CONFIG_SCHEMA` value of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "sensor_modules:\n - name: dht\n module: dht22\n type: AM2302\n pin: 4\n \n - name: ds\n module: ds18b\n type: DS18S20\n address: 000803702e49\n", + "title_id": "sensor_modules" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Your name for this configuration of the module. Will be referred to by entries\nin the `sensor_inputs` section.\n", + "title_id": "sensor_modules-star-name" + }, + "type": "string", + "required": true, + "empty": false + }, + "module": { + "meta": { + "description": "Name of the module in the code. This is listed in the README's\n\"Supported Hardware\" section in brackets.\n", + "title_id": "sensor_modules-star-module" + }, + "type": "string", + "required": true, + "empty": false + }, + "cleanup": { + "meta": { + "description": "Whether to run the module's `cleanup()` method on exit.", + "title_id": "sensor_modules-star-cleanup" + }, + "type": "boolean", + "required": false, + "default": true + } + }, + "meta": { + "title_id": "sensor_modules-star" + } + } + }, + "stream_modules": { + "meta": { + "description": "List of stream modules to configure.", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`CONFIG_SCHEMA` value of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "stream_modules:\n - name: network_switch\n module: serial\n device: /dev/ttyUSB1\n baud: 115200\n interval: 10\n\n - name: ups\n module: serial\n type: /dev/ttyUSB0\n baud: 9600\n interval: 1\n", + "title_id": "stream_modules" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Your name for this configuration of the module. Will be used in the topic on\nwhich the stream's data is published and the topic on which messages can be\nsent for writing to the stream.\n", + "title_id": "stream_modules-star-name" + }, + "type": "string", + "required": true, + "empty": false + }, + "module": { + "meta": { + "description": "Name of the module in the code. This is listed in the README's\n\"Supported Hardware\" section in brackets.\n", + "title_id": "stream_modules-star-module" + }, + "type": "string", + "required": true, + "empty": false + }, + "cleanup": { + "meta": { + "description": "Whether to run the module's `cleanup()` method on exit.", + "title_id": "stream_modules-star-cleanup" + }, + "type": "boolean", + "required": false, + "default": true + }, + "retain": { + "meta": { + "description": "Whether to set the `retain` flag on MQTT messages publishing data received\nfrom the stream.\n", + "title_id": "stream_modules-star-retain" + }, + "type": "boolean", + "required": false, + "default": false + }, + "read_interval": { + "meta": { + "description": "How long to wait between polling the stream for new data.", + "unit": "seconds", + "title_id": "stream_modules-star-read_interval" + }, + "type": "float", + "required": false, + "default": 60, + "min": 0.01 + }, + "read": { + "meta": { + "description": "Whether to poll this stream for incoming data and publish it on an MQTT topic.\n", + "title_id": "stream_modules-star-read" + }, + "type": "boolean", + "required": false, + "default": true + }, + "write": { + "meta": { + "description": "Whether to subscribe to MQTT messages on a topic and write messages received on it to the stream.", + "title_id": "stream_modules-star-write" + }, + "type": "boolean", + "required": false, + "default": true + } + }, + "meta": { + "title_id": "stream_modules-star" + } + } + }, + "digital_inputs": { + "meta": { + "description": "List of digital inputs to configure.", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`PIN_SCHEMA` and `INPUT_SCHEMA` values of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "gpio_modules:\n - name: rpi\n module: raspberrypi\n\ndigital_inputs:\n - name: gpio0\n module: rpi\n pin: 0\n\n - name: gpio1\n module: rpi\n pin: 1\n", + "title_id": "digital_inputs" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Name of the input. Used in the MQTT topic when publishing input changes.\n\nThe topic that input changes will be published to is:\n`/input/`\n", + "title_id": "digital_inputs-star-name" + }, + "type": "string", + "required": true, + "empty": false + }, + "module": { + "meta": { + "description": "Name of the module configured in `gpio_modules` that this input is attached to.\n", + "title_id": "digital_inputs-star-module" + }, + "type": "string", + "required": true, + "empty": false + }, + "pin": { + "meta": { + "description": "Which of the GPIO module's pins this input refers to.", + "extra_info": "Depending on the GPIO module's implementation, this can be either a string\nor an integer.\n", + "title_id": "digital_inputs-star-pin" + }, + "type": [ + "string", + "integer" + ], + "required": true, + "empty": false + }, + "on_payload": { + "meta": { + "description": "Payload to be sent when the input changes to what is considered to be \"on\".\nSee `inverted` below for the definition of \"on\" and \"off\".\n", + "extra_info": "Make sure to avoid YAML's automatic boolean type conversion when setting this\noption by surrounding potential booleans with quotes.\nSee the \"Regexp\" section of the\n[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that\nwill be parsed as boolean.\n", + "title_id": "digital_inputs-star-on_payload" + }, + "type": "string", + "required": false, + "empty": false, + "default": "ON" + }, + "off_payload": { + "meta": { + "description": "Payload to be sent when the input changes to what is considered to be \"off\".\nSee `inverted` below for the definition of \"on\" and \"off\".\n", + "extra_info": "Make sure to avoid YAML's automatic boolean type conversion when setting this\noption by surrounding potential booleans with quotes.\nSee the \"Regexp\" section of the\n[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that\nwill be parsed as boolean.\n", + "title_id": "digital_inputs-star-off_payload" + }, + "type": "string", + "required": false, + "empty": false, + "default": "OFF" + }, + "inverted": { + "meta": { + "description": "Invert the logic level so that \"low\" levels are considered to be \"on\" and\n\"high\" levels are considered \"off\".\n", + "extra_info": "This can be useful for when an input is pulled \"high\" with a resistor and a\ndevice (like a button or another IC) connects it to ground when it's \"active\".\n", + "title_id": "digital_inputs-star-inverted" + }, + "type": "boolean", + "required": false, + "default": false + }, + "pullup": { + "meta": { + "description": "Enable the pull-up resistor for this input so that the logic level is pulled\n\"high\" by default.\n", + "extra_info": "Not all GPIO modules support pull-up resistors.", + "title_id": "digital_inputs-star-pullup" + }, + "type": "boolean", + "required": false, + "default": false + }, + "pulldown": { + "meta": { + "description": "Enable the pull-down resistor for this input so that the logic level is pulled\n\"low\" by default.\n", + "extra_info": "Not all GPIO modules support pull-down resistors.", + "title_id": "digital_inputs-star-pulldown" + }, + "type": "boolean", + "required": false, + "default": false + }, + "interrupt": { + "meta": { + "description": "Configure this pin to trigger an interrupt when the logic level is \"rising\",\n\"falling\" or \"both\".\n", + "extra_info": "Not all GPIO modules support interrupts, and those that do may do so in\nvarious ways.\nTODO: Add link to interrupt documentation.\n", + "title_id": "digital_inputs-star-interrupt" + }, + "type": "string", + "required": false, + "allowed": [ + "rising", + "falling", + "both" + ] + }, + "interrupt_for": { + "meta": { + "description": "List of other pin names that this pin is an interrupt for.\n\nThis is generally used on GPIO modules that provide software callbacks on\ninterrupts, so that we can attach another \"remote\" module's interrupt output\npin (one that changes logic level when one of its pins triggers an interrupt)\nto this input and use the callback to get the value of the \"remote\" pin and\npublish it on MQTT.\n\nTODO: Add link to interrupt documentation.\n", + "title_id": "digital_inputs-star-interrupt_for" + }, + "type": "list", + "required": false, + "minlength": 1, + "schema": { + "type": "string", + "required": true, + "meta": { + "title_id": "digital_inputs-star-interrupt_for-star" + } + } + }, + "bouncetime": { + "meta": { + "description": "Don't trigger interrupts more frequently than once per `bouncetime`.\n", + "unit": "milliseconds", + "title_id": "digital_inputs-star-bouncetime" + }, + "type": "integer", + "required": false, + "default": 100, + "min": 1 + }, + "retain": { + "meta": { + "description": "Set the retain flag on MQTT messages published on input change.", + "title_id": "digital_inputs-star-retain" + }, + "type": "boolean", + "required": false, + "default": false + }, + "poll_interval": { + "meta": { + "description": "How long to wait between checking the value of this input.", + "unit": "seconds", + "extra_info": "When the pin is configured as an interrupt, the pin is no longer polled.\nThe only exception to this is if the pin is configured as an interrupt for\nanother pin. In this case, whether or not we poll is decided by the\n`poll_when_interrupt_for` setting below.\n", + "title_id": "digital_inputs-star-poll_interval" + }, + "type": "float", + "required": false, + "default": 0.1 + }, + "poll_when_interrupt_for": { + "meta": { + "description": "Poll this pin when it's configured as an interrupt for another pin.", + "extra_info": "Polling the pin when it's configured as an interrupt for another pin is useful\nin order to make sure that if we somehow miss an interrupt on this pin (the\nremote module's interrupt output pin goes low (\"triggered\")), we\ndon't end up stuck in that state where we don't handle the remote module's\ninterrupt at all. If we poll the \"triggered\" value on this pin and our\ninterrupt handling hasn't dealt with it, then we'll handle it here.\n", + "title_id": "digital_inputs-star-poll_when_interrupt_for" + }, + "type": "boolean", + "required": false, + "default": true + }, + "ha_discovery": { + "meta": { + "description": "Configures the\n[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/)\nfor this pin.\n\nAny values entered into this section will be sent as part of the discovery\nconfig payload. See the above link for documentation.\n", + "yaml_example": "digital_inputs:\n - name: livingroom_motion\n module: rpi\n ha_discovery:\n component: binary_sensor\n name: Living Room Motion\n device_class: motion\n", + "title_id": "digital_inputs-star-ha_discovery" + }, + "type": "dict", + "allow_unknown": true, + "schema": { + "component": { + "meta": { + "description": "Type of component to report this input as to Home Assistant.", + "title_id": "digital_inputs-star-ha_discovery-component" + }, + "type": "string", + "required": false, + "empty": false, + "default": "binary_sensor" + } + } + } + }, + "meta": { + "title_id": "digital_inputs-star" + } + } + }, + "digital_outputs": { + "meta": { + "description": "List of digital outputs to configure.", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`PIN_SCHEMA` and `OUTPUT_SCHEMA` values of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "gpio_modules:\n - name: rpi\n module: raspberrypi\n\ndigital_outputs:\n - name: gpio0\n module: rpi\n pin: 0\n\n - name: gpio1\n module: rpi\n pin: 1\n", + "title_id": "digital_outputs" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Name of the output. Used in the MQTT topics that are subscribed to in order to\nchange the output value according to received MQTT messages, as well as in the\nMQTT topic for publishing output changes.\n\nThe topics subscribed to for each output are:\n- `/output//set`\n- `/output//set_on_ms`\n- `/output//set_off_ms`\n\nThe topic that output changes will be published to is:\n`/output/`\n", + "title_id": "digital_outputs-star-name" + }, + "type": "string", + "required": true + }, + "module": { + "meta": { + "description": "Name of the module configured in `gpio_modules` that this output is attached to.\n", + "title_id": "digital_outputs-star-module" + }, + "type": "string", + "required": true + }, + "pin": { + "meta": { + "description": "Which of the GPIO module's pins this output refers to.", + "extra_info": "Depending on the GPIO module's implementation, this can be either a string\nor an integer.\n", + "title_id": "digital_outputs-star-pin" + }, + "type": [ + "string", + "integer" + ], + "required": true, + "empty": false + }, + "on_payload": { + "meta": { + "description": "Payload to consider as \"on\" when received to the `/set` topic for this output.\nSee `inverted` below for the definition of \"on\" and \"off\".\n", + "extra_info": "Make sure to avoid YAML's automatic boolean type conversion when setting this\noption by surrounding potential booleans with quotes.\nSee the \"Regexp\" section of the\n[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that\nwill be parsed as boolean.\n", + "title_id": "digital_outputs-star-on_payload" + }, + "type": "string", + "required": false, + "empty": false, + "default": "ON" + }, + "off_payload": { + "meta": { + "description": "Payload to consider as \"off\" when received to the `/set` topic for this output.\nSee `inverted` below for the definition of \"on\" and \"off\".\n", + "extra_info": "Make sure to avoid YAML's automatic boolean type conversion when setting this\noption by surrounding potential booleans with quotes.\nSee the \"Regexp\" section of the\n[YAML bool docs](https://yaml.org/type/bool.html) for all of the values that\nwill be parsed as boolean.\n", + "title_id": "digital_outputs-star-off_payload" + }, + "type": "string", + "required": false, + "empty": false, + "default": "OFF" + }, + "inverted": { + "meta": { + "description": "Invert the logic level so that \"low\" levels are considered to be \"on\" and\n\"high\" levels are considered \"off\".\n", + "extra_info": "This can be useful for when an output turns something on when its output is\n\"low\".\n", + "title_id": "digital_outputs-star-inverted" + }, + "type": "boolean", + "required": false, + "default": false + }, + "timed_set_ms": { + "meta": { + "description": "How long to set an output to the desired value on receipt of an MQTT message\nto the `/set` topic before then setting it back to the opposite value.\n", + "unit": "milliseconds", + "extra_info": "This may be useful if the output controls a device where leaving the ouput\n\"on\" for too long would be detrimental. Using this option means that you don't\nhave to rely on a second \"off\" message getting through MQTT for the output to\nreturn to a safe state.\n", + "title_id": "digital_outputs-star-timed_set_ms" + }, + "type": "integer", + "required": false, + "empty": true + }, + "initial": { + "meta": { + "description": "Set the output to an initial \"high\" or \"low\" state when the software starts.\n", + "title_id": "digital_outputs-star-initial" + }, + "type": "string", + "required": false, + "allowed": [ + "high", + "low" + ] + }, + "publish_initial": { + "meta": { + "description": "Whether to publish an MQTT message for the initial \"high\" or \"low\" state set\nabove.\n", + "title_id": "digital_outputs-star-publish_initial" + }, + "type": "boolean", + "required": false, + "default": false + }, + "retain": { + "meta": { + "description": "Set the retain flag on MQTT messages published on output change.", + "title_id": "digital_outputs-star-retain" + }, + "type": "boolean", + "required": false, + "default": false + }, + "ha_discovery": { + "meta": { + "description": "Configures the\n[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/)\nfor this pin.\n\nAny values entered into this section will be sent as part of the discovery\nconfig payload. See the above link for documentation.\n", + "yaml_example": "digital_outputs:\n - name: garage_door1\n module: rpi\n ha_discovery:\n component: switch\n name: Ferrari Garage Door\n device_class: garage_door\n", + "title_id": "digital_outputs-star-ha_discovery" + }, + "type": "dict", + "allow_unknown": true, + "schema": { + "component": { + "meta": { + "description": "Type of component to report this output as to Home Assistant.", + "title_id": "digital_outputs-star-ha_discovery-component" + }, + "type": "string", + "required": false, + "empty": false, + "default": "switch" + } + } + } + }, + "meta": { + "title_id": "digital_outputs-star" + } + } + }, + "sensor_inputs": { + "meta": { + "description": "List of sensor inputs to configure.", + "extra_info": "Some modules require extra config entries, specified by the modules themselves.\nUntil the documentation is written for the individual modules, please refer to the\n`MODULE_SCHEMA` values of the module's code in\n[the repository](https://github.com/flyte/pi-mqtt-gpio/tree/feature/asyncio/mqtt_io/modules).\nTODO: Link this to the pending wiki pages on each module's requirements.\n", + "yaml_example": "sensor_modules:\n - name: dht\n module: dht22\n type: AM2302\n pin: 4\n\nsensor_inputs:\n - name: workshop_temp\n module: dht\n type: temperature\n interval: 30\n\n - name: workshop_humidity\n module: dht\n type: humidity\n interval: 60\n", + "title_id": "sensor_inputs" + }, + "type": "list", + "required": false, + "default": [], + "schema": { + "type": "dict", + "allow_unknown": true, + "schema": { + "name": { + "meta": { + "description": "Name of the sensor. Used in the MQTT topic when publishing sensor values.\n\nThe topic that sensor values will be published to is:\n`/sensor/`\n", + "title_id": "sensor_inputs-star-name" + }, + "type": "string", + "required": true, + "empty": false + }, + "module": { + "meta": { + "description": "Name of the module configured in `sensor_modules` that this sensor reading\ncomes from.\n", + "title_id": "sensor_inputs-star-module" + }, + "type": "string", + "required": true, + "empty": false + }, + "retain": { + "meta": { + "description": "Set the retain flag on MQTT messages published on sensor read.", + "title_id": "sensor_inputs-star-retain" + }, + "type": "boolean", + "required": false, + "default": false + }, + "interval": { + "meta": { + "description": "How long to wait between checking the value of this sensor.", + "unit": "seconds", + "title_id": "sensor_inputs-star-interval" + }, + "type": "integer", + "required": false, + "default": 60, + "min": 1 + }, + "digits": { + "meta": { + "description": "How many decimal places to round the sensor reading to.", + "title_id": "sensor_inputs-star-digits" + }, + "type": "integer", + "required": false, + "default": 2, + "min": 0 + }, + "ha_discovery": { + "meta": { + "description": "Configures the\n[Home Assistant MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/)\nfor this sensor.\n\nAny values entered into this section will be sent as part of the discovery\nconfig payload. See the above link for documentation.\n", + "yaml_example": "sensor_inputs:\n - name: workshop_temp\n module: dht\n type: temperature\n ha_discovery:\n name: Workshop Temperature\n device_class: temperature\n\n - name: workshop_humidity\n module: dht\n type: humidity\n ha_discovery:\n name: Workshop Humidity\n device_class: humidity\n", + "title_id": "sensor_inputs-star-ha_discovery" + }, + "type": "dict", + "allow_unknown": true, + "schema": { + "component": { + "meta": { + "description": "Type of component to report this sensor as to Home Assistant.", + "title_id": "sensor_inputs-star-ha_discovery-component" + }, + "type": "string", + "required": false, + "empty": false, + "default": "sensor" + }, + "expire_after": { + "meta": { + "description": "How long after receiving a sensor update to declare it invalid.", + "extra_info": "Defaults to `interval` * 2 + 5\n", + "title_id": "sensor_inputs-star-ha_discovery-expire_after" + }, + "type": "integer", + "required": false, + "min": 1 + } + } + } + }, + "meta": { + "title_id": "sensor_inputs-star" + } + } + }, + "logging": { + "meta": { + "description": "Config to pass directly to\n[Python's logging module](https://docs.python.org/3/library/logging.config.html#logging-config-dictschema)\nto influence the logging output of the software.\n", + "title_id": "logging" + }, + "type": "dict", + "required": false, + "allow_unknown": true, + "default": { + "version": 1, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "default", + "level": "INFO" + } + }, + "formatters": { + "default": { + "format": "%(asctime)s %(name)s [%(levelname)s] %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S" + } + }, + "loggers": { + "mqtt_io": { + "level": "INFO", + "handlers": [ + "console" + ], + "propagate": true + } + } + } + }, + "reporting": { + "meta": { + "description": "Configuration for reporting back to the developers using\n[Sentry](https://sentry.io/welcome/) to help diagnose issues.\n\n*This is **not** enabled by default*\n", + "extra_info": "Your config file is included in the report, but has the host, port and username\nhashed and the password removed. Sentry's SDK automatically attempts to remove\npassword data, but the other values may still be exposed within the Python traceback\ncontext.\n", + "yaml_example": "reporting:\n enabled: yes\n issue_id: 123\n", + "title_id": "reporting" + }, + "type": "dict", + "required": false, + "schema": { + "enabled": { + "meta": { + "description": "Enable the sending of error reports to the developers if the software crashes.\n", + "title_id": "reporting-enabled" + }, + "type": "boolean", + "required": true + }, + "issue_id": { + "meta": { + "description": "The GitHub Issue ID that the specific error relates to.", + "extra_info": "This is useful if you've reported a specific issue on the project repository and\nwant to provide additional context to help the developers diagnose the issue.\n", + "title_id": "reporting-issue_id" + }, + "type": "integer", + "required": false + } + } + }, + "options": { + "meta": { + "description": "Miscellaneous options regarding the runtime behaviour of MQTT IO.", + "yaml_example": "options:\n install_requirements: no\n", + "title_id": "options" + }, + "type": "dict", + "required": false, + "default": {}, + "schema": { + "install_requirements": { + "meta": { + "description": "Whether to install missing module packages on startup.", + "title_id": "options-install_requirements" + }, + "type": "boolean", + "required": false, + "default": true + } + } + } +} \ No newline at end of file diff --git a/docs/2.5.0/versions.md.j2 b/docs/2.5.0/versions.md.j2 new file mode 100644 index 00000000..b8b06f9d --- /dev/null +++ b/docs/2.5.0/versions.md.j2 @@ -0,0 +1,14 @@ +# Documentation Versions + +## Releases + +{% for ver in releases %} +-
{{ ver }} +{%- endfor %} + +## Other Versions + +{% for ver in other_versions %} +- {{ ver }} +{%- endfor %} + diff --git a/docs/index.html b/docs/index.html index 33fbcbb8..5c9a908a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,11 +1,11 @@ - + - + - Redirecting to '2.4.3' documentation version... + Redirecting to '2.5.0' documentation version...