From f2cb2d3744b9fd33dd3e57084a9d339d3fbfe4a9 Mon Sep 17 00:00:00 2001 From: Boernsman Date: Thu, 28 Jan 2021 12:29:09 +0100 Subject: [PATCH] webasto discovery works now --- debian/control | 15 ++ debian/nymea-plugin-webasto.install.in | 1 + discovery/discovery.cpp | 272 +++++++++++++++++++++++++ discovery/discovery.h | 75 +++++++ discovery/host.cpp | 94 +++++++++ discovery/host.h | 70 +++++++ nymea-plugins-modbus.pro | 1 + webasto/README.md | 17 ++ webasto/integrationpluginwebasto.cpp | 200 ++++++++++++++++++ webasto/integrationpluginwebasto.h | 69 +++++++ webasto/integrationpluginwebasto.json | 116 +++++++++++ webasto/meta.json | 13 ++ webasto/webasto.cpp | 86 ++++++++ webasto/webasto.h | 173 ++++++++++++++++ webasto/webasto.png | Bin 0 -> 32508 bytes webasto/webasto.pro | 17 ++ 16 files changed, 1219 insertions(+) create mode 100644 debian/nymea-plugin-webasto.install.in create mode 100644 discovery/discovery.cpp create mode 100644 discovery/discovery.h create mode 100644 discovery/host.cpp create mode 100644 discovery/host.h create mode 100644 webasto/README.md create mode 100644 webasto/integrationpluginwebasto.cpp create mode 100644 webasto/integrationpluginwebasto.h create mode 100644 webasto/integrationpluginwebasto.json create mode 100644 webasto/meta.json create mode 100644 webasto/webasto.cpp create mode 100644 webasto/webasto.h create mode 100644 webasto/webasto.png create mode 100644 webasto/webasto.pro diff --git a/debian/control b/debian/control index 25d8750..bcffd6f 100644 --- a/debian/control +++ b/debian/control @@ -110,6 +110,21 @@ Description: nymea.io plugin for wallbe ev charging stations . This package will install the nymea.io plugin for wallbe +Package: nymea-plugin-webasto +Architecture: any +Section: libs +Depends: ${shlibs:Depends}, + ${misc:Depends}, + nymea-plugins-modbus-translations +Description: nymea.io plugin for Webasto Live EV charging stations + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for webasto + Package: nymea-plugins-modbus-translations Section: misc diff --git a/debian/nymea-plugin-webasto.install.in b/debian/nymea-plugin-webasto.install.in new file mode 100644 index 0000000..1eb7eaa --- /dev/null +++ b/debian/nymea-plugin-webasto.install.in @@ -0,0 +1 @@ +usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginwebasto.so diff --git a/discovery/discovery.cpp b/discovery/discovery.cpp new file mode 100644 index 0000000..96c459c --- /dev/null +++ b/discovery/discovery.cpp @@ -0,0 +1,272 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "discovery.h" + +#include +#include +#include +#include +#include +#include +NYMEA_LOGGING_CATEGORY(dcDiscovery, "Discovery") + +Discovery::Discovery(QObject *parent) : QObject(parent) +{ + connect(&m_timeoutTimer, &QTimer::timeout, this, &Discovery::onTimeout); +} + +void Discovery::discoverHosts(int timeout) +{ + if (isRunning()) { + qCWarning(dcDiscovery()) << "Discovery already running. Cannot start twice."; + return; + } + m_timeoutTimer.start(timeout * 1000); + + foreach (const QString &target, getDefaultTargets()) { + QProcess *discoveryProcess = new QProcess(this); + m_discoveryProcesses.append(discoveryProcess); + connect(discoveryProcess, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + + QStringList arguments; + arguments << "-oX" << "-" << "-n" << "-sn"; + arguments << target; + + qCDebug(dcDiscovery()) << "Scanning network:" << "nmap" << arguments.join(" "); + discoveryProcess->start(QStringLiteral("nmap"), arguments); + } +} + +void Discovery::abort() +{ + foreach (QProcess *discoveryProcess, m_discoveryProcesses) { + if (discoveryProcess->state() == QProcess::Running) { + qCDebug(dcDiscovery()) << "Kill running discovery process"; + discoveryProcess->terminate(); + discoveryProcess->waitForFinished(5000); + } + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + qDeleteAll(m_scanResults); + m_scanResults.clear(); +} + +bool Discovery::isRunning() const +{ + return !m_discoveryProcesses.isEmpty() || !m_pendingArpLookups.isEmpty() || !m_pendingNameLookups.isEmpty(); +} + +void Discovery::discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *discoveryProcess = static_cast(sender()); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcDiscovery()) << "Nmap error failed. Is nmap installed correctly?"; + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + finishDiscovery(); + return; + } + + QByteArray data = discoveryProcess->readAll(); + m_discoveryProcesses.removeAll(discoveryProcess); + discoveryProcess->deleteLater(); + discoveryProcess = nullptr; + + QXmlStreamReader reader(data); + + int foundHosts = 0; + qCDebug(dcDiscovery()) << "nmap finished network discovery:"; + while (!reader.atEnd() && !reader.hasError()) { + QXmlStreamReader::TokenType token = reader.readNext(); + if(token == QXmlStreamReader::StartDocument) + continue; + + if(token == QXmlStreamReader::StartElement && reader.name() == "host") { + bool isUp = false; + QString address; + QString macAddress; + QString vendor; + while (!reader.atEnd() && !reader.hasError() && !(token == QXmlStreamReader::EndElement && reader.name() == "host")) { + token = reader.readNext(); + + if (reader.name() == "address") { + QString addr = reader.attributes().value("addr").toString(); + QString type = reader.attributes().value("addrtype").toString(); + if (type == "ipv4" && !addr.isEmpty()) { + address = addr; + } else if (type == "mac") { + macAddress = addr; + vendor = reader.attributes().value("vendor").toString(); + } + } + + if (reader.name() == "status") { + QString state = reader.attributes().value("state").toString(); + if (!state.isEmpty()) + isUp = state == "up"; + } + } + + if (isUp) { + foundHosts++; + qCDebug(dcDiscovery()) << " - host:" << address; + + Host *host = new Host(); + host->setAddress(address); + + if (!macAddress.isEmpty()) { + host->setMacAddress(macAddress); + } else { + QProcess *arpLookup = new QProcess(this); + m_pendingArpLookups.insert(arpLookup, host); + connect(arpLookup, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(arpLookupDone(int,QProcess::ExitStatus))); + arpLookup->start("arp", {"-vn"}); + } + + host->setHostName(vendor); + QHostInfo::lookupHost(address, this, SLOT(hostLookupDone(QHostInfo))); + m_pendingNameLookups.insert(address, host); + + m_scanResults.append(host); + } + } + } + + if (foundHosts == 0 && m_discoveryProcesses.isEmpty()) { + qCDebug(dcDiscovery()) << "Network scan successful but no hosts found in this network"; + finishDiscovery(); + } +} + +void Discovery::hostLookupDone(const QHostInfo &info) +{ + Host *host = m_pendingNameLookups.take(info.addresses().first().toString()); + if (!host) { + // Probably aborted... + return; + } + if (info.error() != QHostInfo::NoError) { + qWarning(dcDiscovery()) << "Host lookup failed:" << info.errorString(); + } + if (host->hostName().isEmpty() || info.hostName() != host->address()) { + host->setHostName(info.hostName()); + } + + finishDiscovery(); +} + +void Discovery::arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus) +{ + QProcess *p = static_cast(sender()); + p->deleteLater(); + + Host *host = m_pendingArpLookups.take(p); + + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qCWarning(dcDiscovery()) << "ARP lookup process failed for host" << host->address(); + finishDiscovery(); + return; + } + + QString data = QString::fromLatin1(p->readAll()); + foreach (QString line, data.split('\n')) { + line.replace(QRegExp("[ ]{1,}"), " "); + QStringList parts = line.split(" "); + if (parts.count() >= 3 && parts.first() == host->address() && parts.at(1) == "ether") { + host->setMacAddress(parts.at(2)); + break; + } + } + finishDiscovery(); +} + +void Discovery::onTimeout() +{ + qWarning(dcDiscovery()) << "Timeout hit. Stopping discovery"; + while (!m_discoveryProcesses.isEmpty()) { + QProcess *discoveryProcess = m_discoveryProcesses.takeFirst(); + disconnect(this, SLOT(discoveryFinished(int,QProcess::ExitStatus))); + discoveryProcess->terminate(); + delete discoveryProcess; + } + foreach (QProcess *p, m_pendingArpLookups.keys()) { + p->terminate(); + m_scanResults.removeAll(m_pendingArpLookups.value(p)); + delete p; + } + m_pendingArpLookups.clear(); + m_pendingNameLookups.clear(); + finishDiscovery(); +} + +QStringList Discovery::getDefaultTargets() +{ + QStringList targets; + foreach (const QHostAddress &interface, QNetworkInterface::allAddresses()) { + if (!interface.isLoopback() && interface.scopeId().isEmpty() && interface.protocol() == QAbstractSocket::IPv4Protocol) { + QPair pair = QHostAddress::parseSubnet(interface.toString() + "/24"); + QString newTarget = QString("%1/%2").arg(pair.first.toString()).arg(pair.second); + if (!targets.contains(newTarget)) { + targets.append(newTarget); + } + } + } + return targets; +} + +void Discovery::finishDiscovery() +{ + if (m_discoveryProcesses.count() > 0 || m_pendingNameLookups.count() > 0 || m_pendingArpLookups.count() > 0) { + // Still busy... + return; + } + + QList hosts; + foreach (Host *host, m_scanResults) { + if (!host->macAddress().isEmpty()) { + hosts.append(*host); + } + } + qDeleteAll(m_scanResults); + m_scanResults.clear(); + + qCDebug(dcDiscovery()) << "Found" << hosts.count() << "network devices"; + m_timeoutTimer.stop(); + emit finished(hosts); +} + diff --git a/discovery/discovery.h b/discovery/discovery.h new file mode 100644 index 0000000..02284c3 --- /dev/null +++ b/discovery/discovery.h @@ -0,0 +1,75 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DISCOVERY_H +#define DISCOVERY_H + +#include +#include +#include +#include + +#include "host.h" + +class Discovery : public QObject +{ + Q_OBJECT +public: + explicit Discovery(QObject *parent = nullptr); + + void discoverHosts(int timeout); + void abort(); + bool isRunning() const; + +signals: + void finished(const QList &hosts); + +private: + QStringList getDefaultTargets(); + + void finishDiscovery(); + +private slots: + void discoveryFinished(int exitCode, QProcess::ExitStatus exitStatus); + void hostLookupDone(const QHostInfo &info); + void arpLookupDone(int exitCode, QProcess::ExitStatus exitStatus); + void onTimeout(); + +private: + QList m_discoveryProcesses; + QTimer m_timeoutTimer; + + QHash m_pendingArpLookups; + QHash m_pendingNameLookups; + QList m_scanResults; +}; + +#endif // DISCOVERY_H + diff --git a/discovery/host.cpp b/discovery/host.cpp new file mode 100644 index 0000000..f1e80a5 --- /dev/null +++ b/discovery/host.cpp @@ -0,0 +1,94 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "host.h" + +Host::Host() +{ + qRegisterMetaType(); + qRegisterMetaType >(); +} + +QString Host::macAddress() const +{ + return m_macAddress; +} + +void Host::setMacAddress(const QString &macAddress) +{ + m_macAddress = macAddress; +} + +QString Host::hostName() const +{ + return m_hostName; +} + +void Host::setHostName(const QString &hostName) +{ + m_hostName = hostName; +} + +QString Host::address() const +{ + return m_address; +} + +void Host::setAddress(const QString &address) +{ + m_address = address; +} + +void Host::seen() +{ + m_lastSeenTime = QDateTime::currentDateTime(); +} + +QDateTime Host::lastSeenTime() const +{ + return m_lastSeenTime; +} + +bool Host::reachable() const +{ + return m_reachable; +} + +void Host::setReachable(bool reachable) +{ + m_reachable = reachable; +} + +QDebug operator<<(QDebug dbg, const Host &host) +{ + dbg.nospace() << "Host(" << host.macAddress() << "," << host.hostName() << ", " << host.address() << ", " << (host.reachable() ? "up" : "down") << ")"; + return dbg.space(); +} + diff --git a/discovery/host.h b/discovery/host.h new file mode 100644 index 0000000..8fe28a2 --- /dev/null +++ b/discovery/host.h @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef HOST_H +#define HOST_H + +#include +#include +#include + +class Host +{ +public: + Host(); + + QString macAddress() const; + void setMacAddress(const QString &macAddress); + + QString hostName() const; + void setHostName(const QString &hostName); + + QString address() const; + void setAddress(const QString &address); + + void seen(); + QDateTime lastSeenTime() const; + + bool reachable() const; + void setReachable(bool reachable); + +private: + QString m_macAddress; + QString m_hostName; + QString m_address; + QDateTime m_lastSeenTime; + bool m_reachable; +}; +Q_DECLARE_METATYPE(Host) + +QDebug operator<<(QDebug dbg, const Host &host); + +#endif // HOST_H + diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro index c1261e8..694d2c8 100644 --- a/nymea-plugins-modbus.pro +++ b/nymea-plugins-modbus.pro @@ -7,6 +7,7 @@ PLUGIN_DIRS = \ sunspec \ unipi \ wallbe \ + webasto \ message(============================================) message("Qt version:" $$[QT_VERSION]) diff --git a/webasto/README.md b/webasto/README.md new file mode 100644 index 0000000..2d5677e --- /dev/null +++ b/webasto/README.md @@ -0,0 +1,17 @@ +# Webasto + +## Supported Things + +* AC Wallbox Live + +## Requirements + +* The packages 'nymea-plugin-webasto' must be installed. +* The modbus server must be enabled +* The setting 'Modbus Slave Register Address Set' must be set to 'TQ-DM100' +* The setting 'Modbus TCP Server Port Number' must be set to 502 + +## More + +https://dealers.webasto.com/Sections/Public/Documents.aspx?SectionId=6&CategoryId=9&ProductTypeId=66&ProductId=630&ShowResult=true + diff --git a/webasto/integrationpluginwebasto.cpp b/webasto/integrationpluginwebasto.cpp new file mode 100644 index 0000000..e8029b1 --- /dev/null +++ b/webasto/integrationpluginwebasto.cpp @@ -0,0 +1,200 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginwebasto.h" +#include "plugininfo.h" + +#include "types/param.h" + +#include +#include +#include +#include + + +IntegrationPluginWebasto::IntegrationPluginWebasto() +{ +} + +void IntegrationPluginWebasto::init() +{ + m_discovery = new Discovery(this); + connect(m_discovery, &Discovery::finished, this, [this](const QList &hosts) { + + foreach (const Host &host, hosts) { + if (!host.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive)) + continue; + + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString().isEmpty()) { + //This device got probably manually setup, to enable auto rediscovery the MAC address needs to setup + if (existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() == host.address()) { + qCDebug(dcWebasto()) << "Wallbox MAC Address has been discovered" << existingThing->name() << host.macAddress(); + existingThing->setParamValue(liveWallboxThingMacAddressParamTypeId, host.macAddress()); + + } + } else if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString() == host.macAddress()) { + if (existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() != host.address()) { + qCDebug(dcWebasto()) << "Wallbox IP Address has changed, from" << existingThing->paramValue(liveWallboxThingIpAddressParamTypeId).toString() << "to" << host.address(); + existingThing->setParamValue(liveWallboxThingIpAddressParamTypeId, host.address()); + + } else { + qCDebug(dcWebasto()) << "Wallbox" << existingThing->name() << "IP address has not changed" << host.address(); + } + break; + } + } + } + }); +} + +void IntegrationPluginWebasto::discoverThings(ThingDiscoveryInfo *info) +{ + qCDebug(dcWebasto()) << "Discover things"; + if (info->thingClassId() == liveWallboxThingClassId) { + m_discovery->discoverHosts(25); + connect(m_discovery, &Discovery::finished, info, [this, info] (const QList &hosts) { + + foreach (const Host &host, hosts) { + if (!host.hostName().contains("webasto", Qt::CaseSensitivity::CaseInsensitive)) + continue; + + qCDebug(dcWebasto()) << " - " << host.hostName() << host.address() << host.macAddress(); + ThingDescriptor descriptor(liveWallboxThingClassId, "Wallbox", host.address() + " (" + host.macAddress() + ")"); + + // Rediscovery + foreach (Thing *existingThing, myThings()) { + if (existingThing->paramValue(liveWallboxThingMacAddressParamTypeId).toString() == host.macAddress()) { + qCDebug(dcWebasto()) << " - Device is already added"; + descriptor.setThingId(existingThing->id()); + break; + } + } + ParamList params; + params << Param(liveWallboxThingMacAddressParamTypeId, host.macAddress()); + params << Param(liveWallboxThingIpAddressParamTypeId, host.address()); + descriptor.setParams(params); + info->addThingDescriptor(descriptor); + } + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "discoverThings", QString("Unhandled thingClassId: %1").arg(info->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + qCDebug(dcWebasto()) << "Setup thing" << thing->name(); + + if (thing->thingClassId() == liveWallboxThingClassId) { + + if (m_webastoConnections.contains(thing)) { + // Clean up after reconfiguration + m_webastoConnections.take(thing)->deleteLater(); + } + QHostAddress address = QHostAddress(thing->paramValue(liveWallboxThingIpAddressParamTypeId).toString()); + Webasto *webasto = new Webasto(address, 502, thing); + m_webastoConnections.insert(thing, webasto); + connect(webasto, &Webasto::destroyed, this, [thing, this] {m_webastoConnections.remove(thing);}); + //TODO signal socket + //TODO emit setup finished + connect(webasto, &Webasto::connectionChanged, info, [info] (bool connected) { + if (connected) + info->finish(Thing::ThingErrorNoError); + }); + } else { + Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::postSetupThing(Thing *thing) +{ + qCDebug(dcWebasto()) << "Post setup thing" << thing->name(); + + if (thing->thingClassId() == liveWallboxThingClassId) { + Webasto *connection = m_webastoConnections.value(thing); + if (!connection) { + qCWarning(dcWebasto()) << "Can't find connection to thing"; + } + + } else { + Q_ASSERT_X(false, "postSetupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::executeAction(ThingActionInfo *info) +{ + Thing *thing = info->thing(); + Action action = info->action(); + + if (thing->thingClassId() == liveWallboxThingClassId) { + + Webasto *connection = m_webastoConnections.value(thing); + if (!connection) { + qCWarning(dcWebasto()) << "Can't find connection to thing"; + return info->finish(Thing::ThingErrorHardwareNotAvailable); + } + + if (action.actionTypeId() == liveWallboxPowerActionTypeId) { + // Enable/Disable the charging process + } else if (action.actionTypeId() == liveWallboxChargeCurrentActionTypeId) { + + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled actionTypeId: %1").arg(action.actionTypeId().toString()).toUtf8()); + } + } else { + Q_ASSERT_X(false, "executeAction", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8()); + } +} + +void IntegrationPluginWebasto::thingRemoved(Thing *thing) +{ + qCDebug(dcWebasto()) << "Delete thing" << thing->name(); + if (thing->thingClassId() == liveWallboxThingClassId) { + } + + if (myThings().isEmpty()) { + //Stop timer + } +} + +void IntegrationPluginWebasto::onConnectionChanged(bool connected) +{ + Webasto *connection = static_cast(sender()); + Thing *thing = m_webastoConnections.key(connection); + if (!thing) { + qCWarning(dcWebasto()) << "On connection changed, thing not found for connection"; + return; + } + thing->setStateValue(liveWallboxConnectedStateTypeId, connected); +} + diff --git a/webasto/integrationpluginwebasto.h b/webasto/integrationpluginwebasto.h new file mode 100644 index 0000000..6224c8d --- /dev/null +++ b/webasto/integrationpluginwebasto.h @@ -0,0 +1,69 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINWEBASTO_H +#define INTEGRATIONPLUGINWEBASTO_H + +#include "integrations/integrationplugin.h" +#include "plugintimer.h" +#include "webasto.h" +#include "../discovery/discovery.h" +#include "../discovery/host.h" + +#include +#include +#include + +class IntegrationPluginWebasto : public IntegrationPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginwebasto.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginWebasto(); + void init() override; + void discoverThings(ThingDiscoveryInfo *info) override; + void setupThing(ThingSetupInfo *info) override; + void postSetupThing(Thing *thing) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + Discovery *m_discovery = nullptr; + QHash m_webastoConnections; + QHash m_asyncActions; + +private slots: + void onConnectionChanged(bool connected); +}; + +#endif // INTEGRATIONPLUGINWEBASTO_H diff --git a/webasto/integrationpluginwebasto.json b/webasto/integrationpluginwebasto.json new file mode 100644 index 0000000..3f964a5 --- /dev/null +++ b/webasto/integrationpluginwebasto.json @@ -0,0 +1,116 @@ +{ + "id": "9fa369ab-c225-4447-9a23-f4911d9b056c", + "name": "Webasto", + "displayName": "webasto", + "vendors": [ + { + "id": "274f4453-6acf-4204-be21-379abbe3b5a7", + "name": "webasto", + "displayName": "Webasto", + "thingClasses": [ + { + "id": "48472124-3199-4827-990a-b72069bd5658", + "displayName": "Live Wallbox", + "name": "liveWallbox", + "createMethods": ["discovery"], + "interfaces": ["evcharger", "connectable"], + "paramTypes": [ + { + "id": "51fa3ea8-e819-46ca-b975-1bee6285441c", + "name": "ipAddress", + "displayName": "IP address", + "type": "QString", + "defaultValue": "0.0.0.0" + }, + { + "id": "4aa97965-fc1c-488a-92a6-848c214564bc", + "name": "macAddress", + "displayName": "MAC address", + "type": "QString", + "defaultValue": "", + "readOnly": true + } + ], + "stateTypes":[ + { + "id": "7e6ed2b4-aa8a-4bf6-b20b-84ecc6cc1508", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "name": "connected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "537e01ac-9290-421d-a5cb-987d9e088941", + "name": "chargeTime", + "displayName": "Charging Time", + "unit": "Minutes", + "type": "int", + "defaultValue": 0, + "displayNameEvent": "Charging time changed" + }, + { + "id": "b076353b-e911-444f-80ad-3f78c4075d1a", + "name": "chargePointState", + "displayName": "Charge point state", + "displayNameEvent": "Charge point state changed", + "type": "QString", + "possibleValues": [ + "No vehicle attached", + "Vehicle attached, no permission", + "Charging authorized", + "Charging", + "Charging paused", + "Charge successful (car still attached)", + "Charging stopped by user (car still attached)", + "Charging error (car still attached)", + "Charging station reserved (No car attached)", + "User not authorized (car attached)" + ], + "defaultValue": "No vehicle attached" + }, + { + "id": "a1a452f9-de93-4c31-b71b-c74264f85a3e", + "name": "cableState", + "displayName": "Cable state", + "displayNameEvent": "Cable state changed", + "type": "QString", + "possibleValues": [ + "No cable attached", + "Cable attached but no car attached)", + "Cable attached and car attached", + "Cable attached,car attached and lock active" + ], + "defaultValue": "No cable attached" + }, + { + "id": "3c054603-d933-4e30-a2cc-2177beaaffdb", + "name": "power", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "displayNameAction": "Start charging", + "displayNameEvent": "Charging status changed", + "writable": true + }, + { + "id": "96ed77ce-c5cf-4981-8a72-b619f5702724", + "name": "chargeCurrent", + "displayName": "Charging current", + "displayNameAction": "Set charging current", + "displayNameEvent": "Charging current changed", + "type": "double", + "unit": "Ampere", + "minValue": 6.00, + "maxValue": 80.00, + "defaultValue": 6.00, + "writable": true + } + ] + } + ] + } + ] +} + diff --git a/webasto/meta.json b/webasto/meta.json new file mode 100644 index 0000000..c226af8 --- /dev/null +++ b/webasto/meta.json @@ -0,0 +1,13 @@ +{ + "title": "webasto", + "tagline": "Integrates Webasto Live DC wallbox into nymea.", + "icon": "webasto.png", + "stability": "consumer", + "offline": true, + "technologies": [ + "network" + ], + "categories": [ + "tool" + ] +} diff --git a/webasto/webasto.cpp b/webasto/webasto.cpp new file mode 100644 index 0000000..339e73f --- /dev/null +++ b/webasto/webasto.cpp @@ -0,0 +1,86 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "webasto.h" +#include "extern-plugininfo.h" + +Webasto::Webasto(const QHostAddress &address, uint port, QObject *parent) : + QObject(parent) +{ + m_modbusConnection = new QModbusTcpClient(this); + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkPortParameter, port); + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); + m_modbusConnection->setNumberOfRetries(3); + m_modbusConnection->setTimeout(1000); +} + +void Webasto::setAddress(const QHostAddress &address) +{ + qCDebug(dcWebasto()) << "Webasto: set address" << address; + m_modbusConnection->setConnectionParameter(QModbusDevice::NetworkAddressParameter, address.toString()); +} + +QHostAddress Webasto::address() const +{ + return QHostAddress(m_modbusConnection->connectionParameter(QModbusDevice::NetworkAddressParameter).toString()); +} + +bool Webasto::connected() +{ + return (m_modbusConnection->state() == QModbusTcpClient::State::ConnectedState); +} + +void Webasto::getBasicInformation() +{ + +} + +void Webasto::getUserId() +{ + +} + +void Webasto::getSessionInformation() +{ + +} + +void Webasto::setLiveBit() +{ + +} + +QUuid Webasto::writeHoldingRegister() +{ + QUuid request = QUuid::createUuid(); + + + return request; +} diff --git a/webasto/webasto.h b/webasto/webasto.h new file mode 100644 index 0000000..c64f8eb --- /dev/null +++ b/webasto/webasto.h @@ -0,0 +1,173 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef WEBASTO_H +#define WEBASTO_H + +#include +#include +#include +#include +#include + +class Webasto : public QObject +{ + Q_OBJECT +public: + enum ChargePointState { + ChargePointStateNoVehicleAttached = 0, + ChargePointStateVehicleAttachedNoPermission, + ChargePointStateChargingAuthorized, + ChargePointStateCharging, + ChargePointStateChargingPaused, + ChargePointStateChargeSuccessfulCarStillAttached, + ChargePointStateChargingStoppedByUserCarStillAttached, + ChargePointStateChargingErrorCarStillAttached, + ChargePointStateChargingStationReservedNorCarAttached, + ChargePointStateUserNotAuthorizedCarAttached + }; + Q_ENUM(ChargePointState) + + enum CableState { + CableStateNoCableAttached = 0, + CableStateCableAttachedNoCarAttached, + CableStateCableAttachedCarAttached, + CableStateCableAttachedCarAttachedLockActive + }; + Q_ENUM(CableState) + + enum EvseSate { + EvseSateStarting = 0, + EvseSateRunning, + EvseSateError + }; + Q_ENUM(EvseSate) + + struct BasicInformation { + double currentL1; // [A] + double currentL2; // [A] + double currentL3; // [A] + double activePower; // [W] + uint activePowerL1; + uint activePowerL2; + uint activePowerL3; + double energyMeter; // [kWh] + }; + + struct CurrentLimitations { + double maxCurrent; + double minimumCurrentLimit; + double maxCurrentFromEvse; + double maxCurrentFromCable; + double maxCurrentFromEV; + }; + + explicit Webasto(const QHostAddress &address, uint port = 502, QObject *parent = nullptr); + + void setAddress(const QHostAddress &address); + QHostAddress address() const; + bool connected(); + +private: + enum TqModbusRegister { + // Get Basic Information polls Register 1000 to 1037 + TqChargePointState = 1000, // State of the charging device + TqChargeState = 1001, // Charging + TqEVSEState = 1002, // State of the charging station + TqCableState = 1004, // State of the charging cable + TqEVSEError = 1006, // Error code of the charging station + TqCurrentL1 = 1008, // Charging current L1 + TqCurrentL2 = 1010, // Charging current L2 + TqCurrentL3 = 1012, // Charging current L3 + TqActivePower = 1020, // Electric Power that can be changed to f.e. mechanical, chemical, thermic power + TqActivePowerL1 = 1024, // Active power L1 + TqActivePowerL2 = 1028, // Active power L2 + TqActivePowerL3 = 1032, // Active power L3 + TqEnergyMeter = 1036, // Meter reading of the charging station + // Get Current Limitatoins polls register 1100 to 1110 + TqMaxCurrent = 1100, // Maximal charging current UINT of the hardware (EVSE, cable, EV) + TqMinimumCurrentLimit = 1102, // Minimal charging current of the hardware (EVSE, cable, EV) + TqMaxCurrentFromEVSE = 1104, // Maximal charging current of the charging station + TqMaxCurrentFromCable = 1106, // Maximal charging current of the cable + TqMaxCurrentFromEV = 1108, // Maximal charging current of the EV + // Get User Priority + TqUserPriority = 1200, // Priorities of the user 0: not defined 1: high priority - 10: low priority + // Get Battery state + TqEVBatteryState = 1300, // Returns an estimate of the SoC + TqEVBatteryCapacity = 1302, // Returns an estimate of the EV Battery Capacity + // Get Schedule polls register 1400 to 1414 + TqScheduleType = 1400, // Type/information of traveling 0: energy that has to be charged, 1: Specification of the desired battery charge (Needs: state of the battery) + TqRequiredEnergy = 1402, // Desired energy + TqRequiredBatteryState = 1404, // Desired state of the battery + TqScheduledTime = 1408, // Departure time + TqScheduledDate = 1412, // Departure date + // Set session polls register 1500 to 15014 + TqChargedEnergy = 1502, // Sum of charged energy for the current session + TqStartTime = 1504, // Start time of charging process + TqChargingTime = 1508, // Duration since beginning of charge + TqEndTime = 1512, // End time of charging process + // Get user id polls register 1600 to 1620 + TqUserId = 1600, // 24 Bytes long User ID (OCPP IdTag) from the current session + TqSmartVehicleDetected = 1620, //Returns 1 if an EV currently connected is a smart vehicle, or 0 if no EV connected or it is not a smart vehicle, + // Get failsafe polls register 2000 to 2004 + TqSafeCurrent = 2000, // Max. charge current under communication failure + TqComTimeout = 2002, // Communication timeout + // Get Charge power polls register 5000 to 5002 + TqChargePower = 5000, // Charge power + TqChargeCurrent = 5001, // Charge current + TqLifeBit = 6000 // Communication monitoring 0/1 Toggle-Bit EM writes 1, Live deletes it and puts it on 0. + }; + + QModbusTcpClient *m_modbusConnection = nullptr; + QHostAddress m_address; + uint m_unitId = 255; + + void getBasicInformation(); + void getUserId(); + void getCurrentLimitations(); + void getSessionInformation(); + void getFailsafeSpecs(); + void getChargeCurrentAndPower(); + void getUserPriority(); + void getBatteryState(); + void setLiveBit(); + +private: + QTimer *m_lifeBitTimer = nullptr; + QUuid writeHoldingRegister(); + +signals: + void connectionChanged(bool connected); + void userIdReceived(const QByteArray &userId); + void currentLimitationsReceived(const CurrentLimitations &limitations); + void userPriorityReceived(uint userPriority); // 0 lowest - 10 highest +}; + +#endif // WEBASTO_H diff --git a/webasto/webasto.png b/webasto/webasto.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8e396351cdd8959d7d081e48c55473e17dffd5 GIT binary patch literal 32508 zcmb@u^;gti)F@0hNJ>d}cMaVsA>D(3C?M(3EuGRGf;7_I-61)Eq%=baNZ0-Fz0b4m z`v<&>wFZ7Tv(G-elL$?9MQjW*3^+JAY-J_6cW`h>)xh6RXehuZtJp&~@C)U=s-hg+ z^UF_OM{yz?95tM>oV1Qd*3qh`j|rsX`3$>_#a#T;Q zOe)e3Rt&R_KQ0|)Cy@2#)B)AN4+*s;*fWgCtOkV$w0VsIwMx>}B{=cgNi))2A-Z_B#wFvpiGPKTP^e?%m%8@eGn!M*si$@yIAIX-RvFy&;waFE8mX zg72rQxMgE9#9g$Wktwbh-Gyph0b^#^jPFSuJIL~_gE9Xv26uWZ2n*Y6cHysliAZ$j zE>gG#pj!qj^(zou%gB7rThicXSBipQGnxiZ#}t#9V^H}2;GJfA5$V;K@tsLiWPmMT z-RNvoPGw?4a6=x-qf(M`Xi?mPWAQ(4EOH#+089JEPw7t*#yd9SSg2_=`O{A{VcqrUiG8k;2o4Dnx$0kT%-F80NZ`K|50oLf@XPVz5H;! zb+6wV8=rc!@TuOseE@<52qBT}-KFJO`A~CgJfCD;>?Ed{6F@iOvOC|VH zbv&E;!K4Oe*5ff}KCn*SvE+4Xj(NfpYx(=4UOF z(5#=sCEF#?78*(A=|rA&^bvIx5k3iOx#86E8?zfGHo!DK!OGlga1K5`vV3S_;Svt z#60d5D%DLg@W3SO^|ld6FurhjD{KhRr~e3|A|h6?RWEXA|6B5{*wQ05OxY3-q7k_qpiAikdVX|3jGet*juTsy(rcb_)g@xb*ou?H;ov zZT1Vu6t7e~PJnh`#QilB__hkN|41!dLke(Q-U6B3T*6@c&5nYy_wkElHqMm?SO?d< z0#H_yFF9!x9r=%RjOD!;204`az{w9HO;cWLfEtYIO>}z)Vpm!ssTQn%vi%Q_3dFIr zZ^K;y0=GpL%mfI-?-cBB^LTu1C4~@Ic0dZ<6#>>S=LjISs%SCjQ4g2~0^&o2|C%+Y zLn;YKw6ApX&AOL!@3zsPH2=Yi_Dl@t=nx`edzML>LPZ$XQg7Y=_>lR+H}T_6%)h2X zXx}!x6<>_d_WV&brZe!k6QyA{8vdf79rJGdSZr#=TU@_WP!P}eYLCe=>M`IONmj8g zFJcVbfHd+V`0_N!6_x;S37?K3K=pcMQ(M(>*+qIG2rp+y2&gjwVfeVM-(8j|M7R?W zP@9BUSa#(#J-H|{ohf`X!M8&3DBdzS!!hjPn<-vtrJ03<}slDgK(2smyzX~gKi5n9W>Q0&%VAxq&B9?BC`pc}mb z2JFy_p;D8Y`v4{-czx>zrg*Ix%eyJ>lM}}Sk9!&i;4liM0}Skc+>-vk%=JTN-}x2b zl7u+0?wkvdEI3vrShN}r57y25-?2fGp>tCi4-aaV(Y{&soQv9*GwKuFsDVj<+Tg9d zP%U1sZWXxGGTK?u9wmjxBHkO`&d8XfTHi@4>pl^1Bf;)EHRLg=Ksh04UB9XE4C26J++>N5t{XG<%C#SB+-C?thGs*aykv z^?T?3k2_(6BdbM7$-RDKXGu#JN8I%W*92=wj_QM<>oQ=bWy%XmF%3>Z3Sc?=Z*Bik zl~8&31K5*>t(jC?u7$W-uHbiI;lmOj2(%lgm54}#*Pqe9zZ{Hq*6?Kmr$;7h+*>4x zR_C{U&ER36V>4kwi)OlS0TjlnqkjJX5{tzOnFb(pGFubELQ7NO;A*1kGVlC0AKZ7*%HlhqV4m+x_^SvhwXY^71i5El z2+L^O3uw7)3N(#&^cXX}Y7234bB51EsCWoaN13W7+$fVYDrslB?mfZfsVabX7^ znDq=gDS7k)WXPAz`I1e|O)5E>UGg8)WEOj58bBvl4IGf!<7?peoDLi-5lILr?xJBx zfq4I;A_S~zK<9pF{pKRW?3Fs!N98jG9uk@!63<@v0M#I-@+?lwp5)bMun zx@U%8SNoWaiR}T8kH?D=hk<0kN#ZX7NkZt61z$BFTFnJCY~YZe8y|o#Xm-DoUKG7y zRKT6ZWc@b3E*H_2_8V7bD96h zZ`O@(tNN`E8!yw}TCJw^VHOe`fK_18BlUttR(#bG7CS)xli;5PEcTuN)Uu*UOpDM>dVWwCiFS z_7owvr2vZBZzd)v5hpVS(b8#2)jRk(?^p#0(M3|^5?HBi2FAHDkB$ro)1F{9H~at z>pm2KoO}$Y3Sc^w6~>)M3_z3~{E8E%cA5FMv-8#S^6}Hv6ww*=8e+t@8yK+~=L_O! zl*d|K_%jlvhSN=kF2B%y;Vt5egTYa=uY7+a0f523#F1MeiN`5kSNL`Dp|4D<+RTAXRE1uoBe~aTv42p?h{u z#P%}dufJeeC3R9C3AJTDi#NO~-AZ6`$|YcR2t>+Yz3vp>=;E|0>4c}9CU9HBeBjPB z;9?a;C@q@S{7c*Gwkpkq#842ny!|_l-NjO-;_W$YDSvw^DcsUra5;3X2e;ZEW@RL=09j(>n#mA532x{3~R0%mjP9WG$K{$?YIT@jfKwenj8+fj&HCoR-aEc74Z=gkIX=$+rXUtVbl<6FI(90Zl3!EEp7SYlI4N^Pe?gP)h<-yyCk{E5 zdVo^2Jl(Ow?C!ZW0`c_sZTa;|{)P>8!)MHhZI^FhS}D@IW+1M`m7x|jgSiHZn44U< z$KyW6Z~KDn>upofZe^oKU5j@LI{Z^9<$ zQN8G$;Hte(X9BYQu^qoCe=v>D)9oG>dM@V1U+lX6CG@1Sz5q%8=u7H>beXz!hDK`G z6VSuP_NbtUgIqCuB6IW3p-Df6$Fp>#f{tiuYW^cpH6tS`x0+%CHb-WMBCb5TZ z*PljGm`QQDK}3oyZhCN4BCZJXytD)20qzD}9+mWsdw5}~oiFY`y|S6E-A0WnNaoZw zkcO?E&GJcq2=Z@+30vo*Oebop_iklYkaXq9!QTIMWRS2bZR6T)otU_I&0b5qtS-PF zKHYIFC#UfzCZ4o&T~wue#NU|$J1cbf6LW`YN9oT@W%|@Wc4U7RinkigO1tl_x+3My z%{I0-3X(D^^9l|~fJiJNYxhGO(U$w)*p#K=8y$^fIiD`v6(5)Ki1XhpK7$$J;r+h42n2mci4nIjJhIqc!DCX0 zITBOEGzojGZK!qDhTi8aMq*y*b4J=JC}OzgLp&OF;>^BY=CYef4MwHHd_=e;#pEm;_Ep))HxDN zSLUibMzpeIa5LGZok-QawMn~+v6iNj1>61C9D9~iG$D$W>{9{KO(Xt(DVH}c(BA@X45dHKizRO9%21~oWb`E8b~$VC9;gP2bcdsvPT?qS z^Om&)%ehdR9d#K9^F7Bz^xmZF#TGHX8r;J0IU4k+`Nz3=NBb*jsGxU2gg_?4B|Fdj zaF-&?Bc{7pGv?2|Ikppy6Ao;PyhP&(Vco94bpZp~6Zzu%NR5Kt6f&J>ytwdG`^awg zLibtR)$gASa0|R)>~fr_6flr_`ZDpRV#zfT{?Bz9iG8ExDeUV3LxztAxJ(U~s@+0( z6h|O+xGTAqpt*&y5)Hqv1$t9)CdJ@i_#9iBoyv^jQ6ITB$>2<)bJL}1Z^hqbJ~MS0 z`qy$KBK4@4pTwgQvLo78u?Cb3oe zGj%(uT4|^-Ye~6KB&t66d5#yKZI~Z2PQ~ocy4jpo=9n#K3v&tf)N3CNtR~-oPFTJp zgmCQ5u4MJu6gcNs9;3Z8en#-23V(lgczh+NT>E}KOsRgcHqAsWTQs9}n}}M2&INCp z)2w5adjqvUOk%V?WhFJd+>ydBM5W#*EihdXuLXaZWii7sAR|@OPkmA$$Ge3?fh!uOVKXgh|1vdwBmzS`$*G}hqM9|DnzDwL{b7o}v(h)@%-s^V0)S>EqP`Ay%K``NX zMq8mowI{OISU)&DS zP&x=AocENmzAID;ksEu%d%&bcW>SnUzNr+PpQy2^)DbnX7 zvs3JYdPL`%)O)AILid*{3*;8jf?-PaoECSNZd!!FyN+GgyhV50S{~`B0kSIO%!Cq( zKKoQ6#f7}73jf@8RwJD#KJwc%ydILaDm$?O3WaSr__njV6cPO=h~nLlBn(RmgCfV- zWm{$XM335}NOCSswR69@u0*->)U0Iod(-WEPVj)Y%)ELEal;EE3C=&ex&IUcRRh)M z!-RDO2s-y5Bn!hCCB3XmYuzgrMosj+@=`8I63*J6X6IMUj~y-meHV%!KZ+*&CI5)4 zo$75evQb3kw#0q9i2agHpH$TN=Ibd(-&&MOH4b~7-Q~}{gtJ_v4+Qh^iiOwcWu8HD zss%7=-;vtjZ~Jawn%^c&+>E`vKqlPp3tw(=;sn&d5x%*1l%04;u{2ko>du+ie4l15+x$#P7ihV)*ew(WG zuy#3mS(F}ViW(^oK`J-O zr+dz_V-D0!mZBp>hqGTC+jtUI10%MDyoAbr?LRK5z zCPJGtg-SKoy+~QR22w6kW|(~r=%>%KU0S6VuE}MKK)pb{jHhQh{B`x4V>x}HQ(>Re zYbnFVb3Jwoe=*;H*6oAJg5WhE>VH>@Tg{;U2)HPP=q`bGqJ)eI=7^m@aoCEP3M|z+ z;HGyhYYQXlj;ORG0V)*TKW@J&*Ebg2$+kso1_wN#mi}~~;cjAY-Kt}!`B!jHd-ndN z66!>2)a}Ae?_%^DJ-msKME878u!SVlNi-rQ4v=p%R;)dgBTP$YW^~)or^u+?&}T*~ z-}9*MYty0dH}S5@^=7*u1;aACYy_W42o@q>Pl0Iv6(ZlYWu{xwSLGjn{H^Y=w@gtF z+5(6!^1a;|Kfja*qm`C{tUBpRyr%N_qtCOtJ*8$#)Ypduxv`MSufgx~k0k>yOoqY< z8G7z+Vs_@vbDQo`H1Z$jAkk>|oL2TZe}2EUE0_O=0_7Rc?7e@ozN-S@w2$|7YgHDc z2~nspUQu8E9BRdyc84F+xMvvE^2#xSA0tVunjxyj=Lie%CvLpgGU%78Ea<$Xg-Z7- z0)^%95{+MVmmnw&#yjYhmX1+Pw%%7l+3haVFlM&SCz8x6ZHa!rC$@$9MgrXD>uwUL z)_B`d;NfZdf=gsMoM8&C$`?$7_y4`l_RDTnyDIu_=-k9_%xxjyu6?vJXu`+zEs)Z% zT+{F00)?gSa9-hRT^3^bm`)uqJL_tkR3WuYcl>>O;7y{7usZyCCZ<2I)kW+T7eOrZ zlp_-tQy1V8g_Zzo$H+mXp(qFV^4oK8m%T>kClU|$_ia=m$pH^5AKs_)o@f7RrzC%k z)gQNt0$PR;YD)tK#^;o^BF)Uq=e|*o9X zQ3YiGJsxso!O~j8;J2)`%07a$PvxCd#^yV>T%R>=VT^y52j4xXvt`)6mE%J6&B*-f zt!?jcWiaiE>0||-vQX(kOxMWYK?Y?|I#CSjh{2~%)qAoTdh`@j_WfgNxE$){skBVP zsPlPf37=SE5vA4ZE~5vux}*y?{N(N5{fL66p8)YtFu9t$bTpRT;Z+3)rn$ar4p2k! z^5wqUUIuJ#m6c}XkuI6bGr`>>ZUuHgd(#YzEJ+WviUXI;bCDM|D;UBpq|VNoJ(u=d{igx7P#Yi*_c~VVJP=OF>KcKhUF)n1l3sqk z7Sc8X$`OBWs;)aX_#JM6R}n^RZ!$+!|IsDE?!A7k4Ryl+8YOH41XrfrW#>K*YAh~E zW(pOEAA9H;Un-@!fdccy^l`X&MkmV;w^?DzuJ(Bw3jJJFtbwO-{6j-D#K3j+6DW+=FA56JQZ;p9$&P`x#h&AI`Xe{qXF5yY1i{cg=-Z=PY`<;sq&KrguWo`d7p)J{ zm@y^p=Ju}gHKE+f0WnMaq9E@9b?!P)&h_Hr!Xd5mW=$5+4c68YhZiRC{hfNKH2>2u#PjoIN$!^cMe&(Ll`iIejuJDt zH$jl#hjI(rtfIfB!C?!7cIJnX&?m3AkYgL@+>t~Nr1|{&v_0YiTng?&Qx4`sSAH%u ziS&_2+xQhEyO#t^;#Xn4hosU3WcqOWZLt=tNxWkaq**35u>8`wZ)c#=hHf*@~`do6bxtS@JX9JV8wxxqM+YS1&{7 zlp}P|?s-Yq)O&szz(~)8M>YI#3L2ZA^V4WAD3<=555YUT7ftx>_{Z*S; zw^!BXhXs~=H5j8f-_BDiFo&w#&j* z2h$)k%B`q%fh7+jFPe*vx&u@D{MqL0XvkENhscpo{2XxufoY0U*Bm7j3D*UZ%qkqjtYB?ab6Y^M%B3GaR3CEkzTP{UjNnfe+X;+(#{~N!?DuZNpz(w6$;MyAU(+ z1PNx}T8YsV6#V@{*vp(GD;$=xD?S|%Ow@8Q}SRr)!lhB6cdjTJCTTxeU zPCp=5l$2^rN{RGAu(VmdXd<>{KxC)vyN0fY3-zRn9cdayulrr0f>Z%}b_!HKB#2Ax zbCBnZ%5@DzvF53iGfv$H>{1b+F>gC&@X}YYyu;3MUb-)tkuE8l_w6;xGW|+q)80}J zh0Eqj-dn1Mlz)%BpBz@(zekW`bocuM_yYKHswz#_8R}KkswmqY=Nl@&UXF45>sVEZ=-iwC*ZbT)r~Sdg zM2>sQLVIB#e4p%>`|~2mA8sAKbGB`w#Rumjy-J8y{HeCksUV1pd97B*663p1wA>i! z7yebH~P zoG<6k163W8El}J+SGIqg`Fym=j4t2Z>$6$?m}AdJEocjdwg4Gz8*kb301B5IX_?k- zi)q+70Y8KzYR`Vm&jTKmkpm|&HPQ1g+O+>>Z{HU49o$#p~QT_3pg1OhH*lUTD7S zXY~krODs;QX8=LEJqIS<%gDB?mfB@%>{Sd!I=7lE)8`v?ld1Iu1y;&#Th0UK(rrNT zv4oY<_~&0!HZ%DdDIG4&2=N)f6irzWj%}Jjw3Yz5BgQsien%?3(7y@aC`|Iy9 z&WhX2P`o>h&WZdbYlHT~KENXt)g3T`$Rt&Vj{1~`6t@m^Hp2dKzI}i;SnGAOie@}x z_lF__X%-65HxAXKd3t^gvtPnAAX`FseYA)dNrBsH>n2)V{$UPMQ!jyfP83;JxsoJm zk$-yeyVBzr(Nv%u~#K(;NjFGMxTJaKgFv;qzHm>Oqz`Vmx1f zE`$HmA2KyY-*lj{376;hcQru#cM<+TOBtiRjD)ck@eB{F%d~jENp#XXCgg3??DUNQ zh%8Qa);k|WzOCmax<&_zqn$hFVs&%;jDW-8M@Ya+P-@39dID=XlDYAsU$$RYW#^;x zn1n=|8XAlLQA%EP*Yw9wl6%Hi!%>%d6m9*}5Epe`(BW;?B2Rq`n&Z3$36?pM-Wc6D z%|(pBqvf)_CnG+g&$%N!b`@;J>CInqpLOc1`Pc6k;nE5dMkcsrw_)(_&5q$}jeXC$ z7D1oDG&}8K4xY+eCR}WZ85scD#4@&AP@cO@$e(feh&B47NsX#vch^KgfI<6f_2y=A z`VP`*vAjsr4jYhfF-kaFR7nJZ+#Q^ah_8ONwYh32=S=GlB>uke8V8cPc(u9R&J3A| zr*9Dx?zI&y=IfSho+z(x$hH@%kiGpjN}M~Vgmeq>O5C!Cl44|aKN^!$J?`v*Ku-0XFAeYBpOtQWgUV zaJ8NQx^R_Y60%5kn-7RSwO>!cefP3 zgGlztOU1y%icAjhI%k%2`3}u|TI@}x*j}}$|5N7`tTo;)>-ye}im`m>IDoDFPVz^= znO&u9d+}P-ia*oz-J5;g;5zFYzPX9}oMR;eeij`{ln&Hs??8X2e;Iq998SF5Ba|;t z`J|XO4tlyrm3``B@(5DCN2T^c7?#_RUA(lA{FLTc`?+{97X}Ju!Q64>bJq82qljv}$cjKquHvg{ zX^=EE5FxC#^3~~zqTQJeYzSNp{WA3En6RLVD&n2|{WheNuFfCSq>Xy26i z_+P)MyTkEQO6;3Sr|a>b-Ik*M9B^QuYB%y@O8$m$_)M-Vjfjrmo?UZ0r~#usLbTL!X@(uQnTnIH1PpXc-U>mw07J4GpSPmjGZw{PWWxaV@r9fn?nT?4t~3_Mk<=)qRUNFR4k)_E5?PIWJzg=gF7sI+qf-!q$bVvHCqn@}>(9<^-JOoqJP@NoQ zVXPg;umv(se!9gGU)u@KQREk`y<%+GDUdqrWcOZM((0%>6YLs{^9>o`Q>_#El|Kj# zJk=69WuQyEWjC8~Q{*6y#JE6d67YqL9Qd^J<8xJz9Tv^4FS zT4?+6@RZ%fS_2y1KI5d2XTQvOc)}}U`Aw*0#Y_04VhAHAKyf2)Qsk&@__2wSO zfS~G;)5Bk(=1($9<7Uaj6p)2=n4ZNeCOs_K{mV;`v?=`fCQM)a z3$G%(P3M!R1QWORQlK}o42k4zB#p;3oHL*r{&wcWVQZpbd!ZF>atuox&KY6<$e|<4#o}GEn83qe#s?sw3kj(-#eN*Zl5-m0A)JJbP!18I8zj|M}+>QyW^P8 zS>52a;O>J2(6%9Sx#4EN)laQ(x~zI_cH{n9Kpo)`tGZ7)^bf)Nan9{Pk=NA^l``s- z82MpHo_7rb1yhu`sYANA?D1^@d^9}jx{jx1q>xn?iAm#R^`PmEkIoC>s;H}Z-oMLaO_9D>mCqNm@P%m z?SIUOazf};foes{eweAIPB|@bVFO20jEHI&lpxIn&JR>;nVwB!>Ogq71;h8a8M#<< zPh#)mswgz?Yv6Jj0MQ!m`o_h}5%5NOx%x+;JsU5TI&+uB z;=>-PY(14128sxlcl^<%-Dp72qmd)MO#-r6KLU-kvD^9tugz1uYL1=p2c_hEF5bN_ zS-~)n04fQd1E<}GWZYh~XNN?2TBTB%`-_UcDQ@%88TYi7k!{A)FXQ)D_)EYH=4=%e zrk8<~VGzwjZsPYv>1Aep#42DkJIvPWJ)}}LDZ;$Q`4S6g$eW^$7%I-n=4l5!xU+Le z&iDWDA1MALM2Qvb{_*NJI@^c`Rl!9Nv5De{A&{jc#OC7M{eUrB_s!xy+mBimO>!+A znOUAQ;u%Pkh^fTojOQ;+wZ_(}Mv!?M}U)xJ07JcO_*L z?|8K%euv!4b{Ms1+g3aDZ+VeaAbGr8OsJ<{v>x8r!!KY$bxqrG=!CS=^4~0-huZe! zATdCc!;^Gl<F^Bb;r>ibzMu>tHl1F*G|}wPmOj%kr+t>- zXX4R6^W-4%-*_3OL;FHI|NO+w=rNEh9K;+!Q8uC1MjmJ5dw<)#@`fNuytpSK5EaCT z6yW92p2Sn4M8oB{S|4)g^dPUJ$olwDz#|N+U-bRo++@O-AGw46rL;TT?Fl2Y%^13D?d-(T%Rj%LX zlH7nv)H;u=tvr7qvw!70Gtbn9vl(jNbAkW8HDD$AuxB0d1em1H#rf{AW)>o8LZ{h1 zGgarLm$oLk_Pf{6U*@vQWXMT9ynFJ{g#&1_wW?c;i{5~iAi;hf6qih{zoS>GF2@EE zE`&CRCQ*7*AF(sm$2R~`KRdPy_=_VxuJ8{rL`>~_S$XS~KhZ4_?%xxKK_puR4Y9}6 z`7^D@r8(+eWOma`=t9fEF%`XutNA+*mo^w~&-sZ{HZ@nLPd@~SslO{aq%OyeqJVjM zYJz$kY1ALi*PLF@fTy!5s@h|0Zq-0dQF{?2?Edq^@O7YMYwTuufWk)GMLsI|oGXbzOm>ld>yUzC2>B z5mj{Ef?6Gk&|zyV=+0b&-r8OW3)ifJFc!%kNFmUnsNn21l;-Yd!6%l#u7IzvC!RHO zF~dup3K1pTDwQiDz2qi)a7&GW$i)=MWz}fVM6-% z+it5tu|2IUqXre$^uafncxUHpz>TNb-@}?7B=qn*ae5;eue11qx!1UcUtRKG%q`Qb zUMT(~&A%ZD3ApLx>4jgPZ4F&B?iRSdd!+&yfEME~r-^Ubt2Zqd!ookINZ}~=E(M@% zICvy+V_F~J8Y&BtR?JFuE8?!<<|_*blh1kjpGhLNC8hjBPC53opFgT8`z%5N5CmL@ zJgZu8wD{epR#Z&Sd4tW{ViSbsLq@A6$&14=!VL;@eKtUDCUwJw5#_5o0nPm>&cId(1TL#jwBgfCuQvI>HTf!O9F6NNyS;xTatV zdkoLn2C3wz|7ck%Xjz(a1D_zXCns;V-A#r0;5OGx$?Bj`>$UR|$&qHxI@4M_Y>Z~- z4kaLB8(t@~SmHgmcqOh#0TsbYZKTQB(?4VH0l`c-i~I2nK~MP9?ja_2Ni}a85(_RY zugYh=v}Sdd58Ur#*5Uwn#KaJ_#uy6@cr(^pe#>iwSUry)pqg2AE(YQ#b@b#S(Fua+mp zdNr83^yfVa0VB9~idH1**!(3zm}4-;?c4{c9E`~3y{*(1*2I1jyT1unf`AaQ&A7qx zZAi-(f10SjNiyxa))J2OzKt#Nza7Dp`uKZ~zjCuyftH(D5kM+k&{s_Qmlj1cu8%_uW_gN<-lkLh*eZ z3sC}XIs7l(xAg?Ff1rqzow+76dEYUnbdWz4l;&;A=(hXU0`_4IFL&!0?znOj#o^3t zw=Kczs3_|g(SV1>gjYmA^Fh8YjRi=LvMe(sTUoUhCR1Z$byUCzw`-Ug>n*l4k>hhf zLRuuDWIrSb>?zcyuKDhh0}}dHLP+1m(oa(Uir=N{8Bpw~YS_mI^4?t2A?B8W7!7zg z*;YrMZv|S9VysOY)_RE_fgz1^R5d$Mnyza^f={$GZO&}Q!(Z07LGl}B<`D8>BOp4U zZJA&kzn<7TMV3RylMFkyfhhkK0jzllLxQn}(?YLfNyP4LXR%&4uY8io93%*?LuDdw zy%|UAox5Ns+MV}Ha+Eg2xV?tGU!xdc7_{|i=ZksA&TA-qmS$3-1A2SSZ_jGsomv2d>(~Fw=;{FUhL|3CxjDM2gg~9 z(kG5Sq$qI}^6z4c5ZaLgOHsYe<$5PNF8=F4q4*66kqL>mXFHIrqerp1D6%u$&m6fV zsUK%w)R2uLETF`ixB^Qp40|>X?i4-PpWL&lx7`$*;r6X=ZglC5x(5D{Uw@$(O)}^U z60WX-k$7sAW|0%IHnqlnEAIH%Oar?v0fNK~M<$uY;rOMef*6}EyKZw8!eJ8qnT*ku z?8}mq&s1YF+{n+g!@@1*_?fYqgfJC#f_y&)?&et4m$$ zasz*sUcqlJTk{i*d97xe!;eiB5YS|ay_+w#l2kjvkci|tDH)a(jcQnm6{EZ1=Ty$1 zFurK6zP`rfj4IVUb)}%@)_vlI;$`h~kOZGb)uk!|vy6*@4ETeE*#qaIP zVB@7<2Qe4!Yb>F1#CI z1{0xN9VgvCpLGla#%2B?Lr2;~MD;&uUA$9)o%blo{W=w^v>?Tu^PFnS|hY`T) z%Xy=Jzq@XW-H87C4Tk1$14T$~if*Cb{+JXp*Yp%}*BEZjrdRLud69-4P`d44Cyp3> zE;}v$E`$X@Qsmk(RYmxI)4*n`o=6uBg7^oUzgsz0WefVd|FTs1Hb^mY@uLgieKC9!8 zFsNld`$3)6Y7WdQ3AW`QRNk24zHAF!8WjX^XkJ!@SfRfX50F#Mm358#N|`4UW|o0b z?EusXm-gCkpVA5cQR35fN~G-3DIg{E=``l#py3Qo+}p6dw+ewFd5+njr10{0)5HH_ z@^+ljD+%G#M=&-Wa_WtGMEl|Le4@Ii=JU*hKXS^)Bhn2a(PlSG!!TGqO7ZIj46HE_ z@E8DL%Ow4Mg95OThCu@6gc!z2iK@i+rzH@y$V$~wl}tOL6F+4E*P{T}3u^Q(Oe7$d zwnq&&l}G;kLP6u0-*+JlvNG3Xd$Vrd!2a9jc#IINcr$r1eVZ zs>&Cq|BVC4%;hgwRYE?yFFPI25F*9@CDM&5 z9DEiOB>RzsBV(Wd3@ehKQ+m;~o+Nj*sBc7b0ZTzNuNszcfR(|9q1Y~hOX(9QpHrV) z)8A3Vjqrlwt8x<;-lrb7uMFc3*WAQJ+IxamBn^dO$?cRh7I)M_-|+?m7Yz%|owB<* zw0>1%*Hn!hlmH#za8&ZbMS8I@wxE2!MYFhp*|mT8wB|q%T>FveFCzX=3boEpX)yjG z@FruJB=_YHf(|VtJ4y=eauF3n}l!fKyBs_AraNIBtCVg`$56JBcqj*=|fS#b_`)9EQaDt!dxFH2J1ZQ{MzcZ z#)n+ot;pLTRDDJ&?LQWN%MUru_7k|*TYgz;;Jl&N|3+SY3|0Iyv-ImB`nNvD zH;pYqT11_(DV zH^OnI=--w>E^*1GwIO3) ze5{sD-mw&?X%*#Wm75^!xy1YNF{72&)al{mSE~H+D@0D<)-9 zgGkaAw-Vx9&kV4@xY&E{YWKepZ?u7IvL{XG(>8{|S9{o(#THC`4`yRmkb|oG4TeGf zX8IOB$$F%RV^hf)>n|=#d3NF}**{G#(<`=6X*H@GGvdnJm@gGUm z$m~DxF${RhZsZ!j5`N;4er<`cRcewt8`yb05>SwYs&N8%&om+Vm!0cY=Wj6FlTW#A zM@bYTH31FVm@K_lh32G3AQ@7$DTnukGt*y?^Y`F4*+xUNoiJc_oqNETMIT_bExfAY zKJC1g6YruDphD#})1)vU(&|Qts~AJ`7S-RbdDqXw2$%UsJfS1X2^h@RL`P;HIAy60 zC|t4$Cr#v{@p8uw`(f`H?o|K;RdL(R6-G(b6CxLLW^t@Qjf+_|D#I^u^ph0ws{l9o zE;(P9d>CPohIl37wwjFFY)M*snbYG^9lIxcdq9KbmJ8UsC+-#I4x*Z!xA{U}@NHqZ zrP_4FlZ(6A=+l5zGh={CbGNDTUNMyTX3+^-nco7hM2qgIcUv|KH%Gm2RRP(^xtBRq4@u0g=#oVQBeem1AbF6&@CutD;H7Vk0qN^60LJc=xzx!S35rUp;9cpdW7UAuvjb)S*ptNlFmuPuM zK+0^`R(B>3`k;aGh`-&OwLsV>@dL#V_!k9rY^@^zW&^A(V{@u_3^ z{&B;f9zlxCN(96c9l%b-QSDoo=bz*Yz%TkjT!On>(BKfl;O=fAFi3EBhv4o30t9!Lkl-2|LU0%)xVyW3&2!)9{qFDk zhqYL%r`PGOK2>$9cJ2L`YP~+g9zS3AY!(>w&8WT0Aowv*KsqrlR(nVhCOu#m-ZwqK z5sRZ?(EnS+xJ>_5+r8s#PqCrMH+1~dpBSE(7SAd8wTfpbW36$juG!kxsayCDhi}kb z$!SHr+>F{mm(3{3>FKNB03T=3wEp=fYI!6bj;DKfE-ev6VX-uKV>H$~uB#!^kJC-V zZa5cgHE=%77>T%@LD82q#tQ*9NLhrJ&G|+C2=CTzp1gfM4~L@9%DU|Q$P1ZuqDt{= zy@j4$7F#cFo667QH*$8A30i80FqrW1R0-yzNQ1Np5Ujoil_3`m6^1gGHc_o3VylWe z!F~xq7%HSfjpa#aiUx^`Vc{2=rM4%lGx99u+Hnp2+qsbesoQ4AZ7=iRj0&DWE7(Jp z`FCtGOpgU`GIwAw%LWBiT(jM8MoD|h%}Co?x)4eQ>O9}r=)%1NZFk<>7@GgQ8KT#E zDBl;Rq7>stc4Z%a+_hD6kTxK^eyc1#yg4@fX9?0(Q59eolsXk!O&ek)0)-`81d7zu z8eS&II-Q7z_}a->hY~%N@JC6`)xjSoI9SBo>+#Rdc^@{Vx>D0U|8Ctbs{~3wWSE^Y zRxfCcz8_F+`}gj2=*<0fgAWH(Kocnr%%2TDIPb_aY7 zMNpO*L`9Sivm*#i&D_|7`5a^rtZYz)l<<(pxO^5A5Mr=$_?&Sm*G1OA;;YF|K-^X6 zZ&v0orI5Irzx`y%-_04-al8~*qL1X1Z*gQGtGo09UtqHD*Hu!cL>>r?!qgtfzE$`T zh{8SkdGu1&uftKAjyW9B&b30PH`xhR;U;G6vl{uOPg^Z5{zLp&TS+eG`i5hi4Y})H zUT!CwbQwEI4#jE-6w$ONM?{CXw_3JZXwmp-49{a~FE{g8+%_emU z@#RDAcKZ*{)Ll}a5cz%t>9@R5Kym;Zq=M0jE~$dQ=KfP#g#pe_ut|*{j3&^kH9}UO z6w|P4veAFvryamZ6o2&xP^R_>@5c2XqoTylE;p)adq2^8A??JcX8U|zkMqew0iTEN-G?WuL(ol zmwB9^Gi4z=2Y8gvjUY5lOr&Z6XooU$>n9|wX5lQ%kwshc^gaVP-#ZBo?*v*yC**@X z|8^nd#Z=MAW*)E5ksy6!s_8d3#*A6LQo2S$JbBWSZe%0!c>uxoo?1mN({ zQF02%&^}&qV~WlLa1UzINz``+(G%g{VVzC=ojA2~KBGY=*E!<>hoz=&pHuh!ImeD~ zoWsP$oB05npJLN-yj#R4h4qQ*$_{CA{?Y*|mwCZUf>7yM)dhM~Q3tGji3LRolO(#l zurk&|h6NW3VI+SJp1+!pJN&+Z)LP0=kqY}KI)UL`JJnJ40JgxD`_YHu#)HeO`)^O} z`cXOuKV42H!~0EJJ9Ltc7UplBB(U01KOTUg9ANLD!=b+w$X zVpM8TasWs&h)FM}pr}akOGjHqztfC&oZZj6oUlEwny|FD?-n&gE?}V4QTESl8xhdR z#Is3Zzqjez>4ix$6FSYTG=}izxykFZr>A$%bWm&>8WMzeD(hEnmA(K7KVL({AY74g z4AWgsl`pOlkw=-#xA6sKDeS1fP79=i$D(SK3`O8)U)ypgO@e@}N{*`q5agW>hHFuF z=Q_4-V)u04LvUII235xA>^iMitd8nZk=o0J>k;HNZk=cfx{iVaR$2p^@V#e%CW(_7 z(sTQTC$QGhl4Z9LW6#iv;+t*1dgzUQgl_l71CYn(-SWg4#-F+ViTtx?+Y;$|&SLtN zQgkyo*AVs&H^D!!DKm1;`uCTK-N2NKY&UY_h&ZF}m_(p@{WpY|Rt&?1+(b56tg|?X z$*Alf!Qh7ULKOS>fkw}>dlwxw%Sgl z9Zr6G+YaFkQU+1DW-(!Yp*cCK;=*@zZE~WKY+pp9Fd4n%-TdJppe_jzA^bbr1m0$p zwfF%n@ChjxrmW7OH$R7q*%9CT#3==Uiv_lB?}b!ayb$+_5hsU8 zaQsYE<0P(j7;k#deHeWolJ8)t;Ake{4!Q^luxrSf3QphOWdd#@sZ<#CbHh|Oi#BY< z;scAC?(j$SdmU!am+lY&jC21M>t#+1Z=1S53pRBS zN}WV-etV<$Xxx1D3%d6V6E`zb1LilRtvBT|&$PJ{>oZ6Sj5#z#k@*{??dBSQS)|*n zs%g=qxq7p?^CSAtcvi@FAwGq`Ql@TZ)w+}c6h7=v6$43n{tU&7n?bq(4PIxow%+WB zY?#OVmt5k=KDiZY9Gsco4sc1aC)JT;X9Z`?tuG1bX;t0l@h(}pmYSscGDIX^(N6{> zzj(Dc5GtR|LvP@@v<1nAL#A_OFccbY#HpNBRN$YFyvR7|TWw#yAJ*xD_@U|YfdQQ^ zW0NCkw$K6J6%lx|>I21-2?m;A0z+8Os(m^mTfj`c8`=vD2p4|2I5m9EGps-t)yz)g zba=z^GK@F_(-$k2np}BmlL0PX4SnB#0QmfeoXP>TIPC(%)^6H4Ue;fKx-7w@(aprd zj0WlnK15@hT@@r{6@w$NwOSd)COF7tU&;i{xKl-|DG&X60*zV}7fIzdtE?&5MIt0&`Hi~9nnGXOS1DITBR`QLt0oty-Kid9p z=Ki_G7@x6(8FZ1bVQXNMmceC#I8=W3^Kb-nq%Ty105r{#paJ33!I3WQl1L~qmPL)M1k8u2e_ z(3FhM7x;0^uuKN|mNEkk&zA}hPfix2q0wsGH0KiD%SwCKEE-K20|C_A;sc_d>AuV) z0`v{HGyOmWs&hCfws>j+CFpG7z1ggTy#gh9EM|h0aE`09KTjj+ZnNcxxAXQGP~M9e zN3CpHx&aO9{3oX3VpCw)w(VYYJq#HU9(CC`AnR+! zh#pLYJ2MHjQRyi-JhfMKR2`nq3m=l6vIe>&DyX|8>}snG@B{@4a~Z*GzQMn`q4 zr3hbNK5?!W{F?D3(Sxq5ywF6Dc8^-BHjkExow;EAC;xJi_Cq>ea4)~Y4GZyhk-aNcJts7Tz3cSFR&lUY}07e+1Q5| zE;sa;PdBZDp3#l+OZ+B3fitRU^qP^%b+Bu~ZAfV~9kg*z)385HBqPa5?pSC%T&7TQ z>WB8-@jM>;RZGBE8~1Nh5dy9f9pb9^_u#iqt%HGhEhYR$*Tz6Fq>eTkDtl&cu!5Je zXHo~*a6djGSyn1R(R$q){g>|qT#fXMb>=S0230PYKR5jH++5sjcV+Z*R#+{NKP35d zyaN#LFqWw0F#oRU>+KWu_3@4LFodo7LRCxF*T_(#J(Pj7Eon&_z@MAPBcIV7o zf2XiXm{M>Dp1M@VVxT$ECo&a{iMvmL5A6ANshzn7mRzmVvH+be>gJt`c2u+nvl%fzTJARlSc{8Kl96Wa|JOlPGVw|9>J`Fnlqa8*@IuHK!I( zuNjvn@R&MqF7emD>^XTM{h>7uJFR6_2>Vm@W^VBiKcKJJ)-YB;nr%MSquC(`mzh_sE z#q4=)%qMJfP|v+aaGN@X&fMX$5Jw5>Zod9n%cERE`Wg}Y`U?zio+;|qpoA{1zC`8NFPJ>~!V7K*LA)a* zu*sNFeY3vc zbQ81Jt*=q%Y^RO=jj!|7a3v-I%m#39;GB~(@%gp{>k2P;gS+Z)t2kDtU^}BFh_XA{ z4$+}s6%EXz)ao$K;uTVG7(I}$x!21TyTOfUAv2KrAX0O!$H8u1@?tyG_9M8Oj<=Xm zQaMlf$_?m_IJ;W=B+rYsIYhOCbdYPjy@X{aFxgP%!UyAQMejbqA0D^AZ0u*DH)3fM2G_fbfoD*n9Iz>n_sRq_jD+Kx z)*fJMxF*u7y4EGC;VNa8SuAN}Zt1V=G#~>+4foUWVoAUgug>P_lZ}4E-iptQA+@8- zyQ$JZY%MiEs*}n;ZwKvs3iKR^**m)bWU$zyXd4r}%sF5E^K|oP{yXC156s@zaW<*! zBL|;FJTO#S2A~cfKP)1x&_9#s#ZmGUQO~lVE0)Q@@Tq3p1Mn6Jb)Ub*rZVmzq?R~! zjx}AbscaTcva1smDkXT!uPqIKKI`EK9iG8bgPS;VOqi14ua z(=mT=y9q-TYlfxFL0}R%98@?6AZ+Oawx;1KB*F)5=p_;l|a zxhO#{;wfc0+-iY!m7!ZXyVWhoY9w`Zws$n~)MMUh;X6pww<^N(50ZfwV~ zVX#yoD{3Dfu*0q!v&<;@FdDOXCqDPYUlbNsNaM? z=daG-jEB1BB=ifH>s!+ocK>XAMy+k?s{aelA7_DS^x(2W2Wp(p#lD z6T(b8;Bg@3Cy%K4832LEpv18M7K|C;5tfJSd#%C+#_Sm23fVN6ua5vz2(9|zx;2s9 zjrkTzdLt@bJjM#1(WfR0e;~UUu7YN6xf}SwIRZHO?)&wlApS+5+iM%POFmuHAqgg4 z!#87|9(!aeUjZlrV8|uf8_`atnP#{kp?x}Ox2} ztzm~T4x8#{nFHxB-UM93rY{Qn3kwQ`o(Q}35`I!?hL?Oe544}cC0I^zJBN0L>)THr zs0WSS%=u{5Hy3;Cc@(~T5sVFp8)6ee`UN}paqQTd#S<` z{-#dv)!hCY((^Z4(r@J}17;i;k_E~XAlWJp5#uu@4Y_P?Opbg2j{zAjoZs^{5J)o! zPvyqe`)cyqYgsP(zW_eTt?)I3?t!J(et~9)r;DC@Lx}ZSl`rBVt8$8I9*~nqek+V6 zC}Z4R4xb7a7G@Y0zbO48BbSC)kFy%Kp6SOkYZwQ|!a*4o7n(hEnddy|UT?IQZZwPrs4n`#CPy>(Y@)cJhe?FkojJjj#>=B;_O}8*uBN(l z0FQ>46Jl@-mh81d(O15%LPC1Zj;_gbBi-?XR0D!f4_(d3KlnPS~nj0sbN1UBcP z3(y$S7F6CDOI7!| zau0_5daW5GUuBckc3)gYWc`E`-A+%UPrW)Lp)29&%7;z4)%@?vW_NZsz$uQb-09Jg zGA3k2d-^V9h0v`-bkJ+M{&)}ESNocV-F~HcylLe4*RAxIy$J&wvreRAq^SBx%vz6O zii715Ulq4zKDDJmyRHuFQZL!|MYP#wmDNv2vUg+XUQ}^5{QKMZWXxR%-&q`21MU61Z)!+R6RxSwsv)r1 zgDD(7u#RJ4AQFXc>c27Qex{xi$B8)h(AiMb~!a0!Bv(b6fWEz5#RfAqguT{*Z7m zO|8TmCw+Sr?c~ZXJdb2YV=OG6bWPjWKDY6l{oi~3v?IBzGS&cZ->i_qwPagtH`<0j z+6dQAkWu;(qDY-9)U(H!YsNj(TBJ?I%w~a(7uQ@9f_O}C(BAg$Wlp(WD~M?*d<*p- zx;Os=dt30gZ;lHg(+2J2UvI&KI+uph1~NzZK+{ZY;JE)Q^t29-w6rlP%=hK zx7A3yoHN$a!-k)v9YtMm78WBD#LqeJ=qq!fY~HqY8tgBQN?zqB6iB5#Mz~I?L<`$N zm=SpJ{g%bc{tYk-1vL*DsXU-4K=yaKJb=wo%pk{i1aWk#_XWr{8%S=EPO(lIJD$Up zJiQ7S;$QO;=N6$|BFP(Aet>9DzLL{N_}hksP4~QcmH5(##pn5qY7YJo8P#Xld&X?x zz*{s~+vj~m6$5J^QCXt3!E!+Aec=Gu-Y8(|__jX&@@}L;`^d^_U`Z)jP~CA=QL4WN z?&nyN1mE?E${hO75kc)jV1yzvWi6(Zs)J!12;p<`e`%w)hYN<^S9v;D=65 z9YZws-`*8m)p|nHi%G=pA-|3=jSc*I%5trZ_o#`A|^ZktK zU|UVdzE4*uaF{myf+>J#hEN%+i{@Hz3HxL?CjRzGuO@#+qBjdoj-f|jzLd?4z){R{ zV<74>u;a(%%}wBxpW%XVn}g1YP^N_5(S~a- z8&|B%?nzknF~#k-SMn2KLlV%Q-;9NkMo&)cIZ?wC(VgymH%;tqRq)k?>5Acx*97W4 z=3AJ#^Uip_sK%c5LzIF5f^VAiWeQm;Go_?_P0ZDrzkYd+RNS&Uu^PbM3KC5Y)_^FH ztqwn9B~X5aJW0_vWQ)n1f6H$e@noOL2~JjkwGmi_55ZLpwFNxYeE@!Gb!({p9OBB# z<&i&=N=wJ{iWiG=iprITR60YsB)u9GYzZ(yjobbX?J@wMoLjzSZ3c(#&*Hlum_3sh zrhil?Rc-+J4iM;~2gf2VAOVib^Y$j36V#81Bh!6(qfR#GyjUzV-y;$+;h;&73rb*9 zZSGEASL23CgnF57-ijPP%fk zM#xb9BXaoaH! zEmrN4l}{xP=cle1o*7Qpy9X(vZ@r{MW{isK7>fNs{Ro574(hB#`v|k)<#`w%nA3#ud012-dZ108 zB_gA!XJSIB+s2I?6tF+xPuF^{c<(^BUP-c+vFaDwGM%IPXZ<@Xdw|WVI{YI-U zb77^i5YJlJnIS9IUI&o`j(-}ngZjMR^Pbxm;I)m6hKPE=e)Z%pRcJ>&htF48oq8E# zei0$HTR80IxtM{TLbMY6T*TLcQ zKg0|KP>7kh_JsvM{^KX#87}KZdxUnvi3d4`9A-~5 zwWAZ^2+#1mb2B;*#-c0BUq~v-uMF^-HB1Zp6^(8@D-w|XFRQEhqc(~h?l4QRTc^(+ zZ;34zGWS8*Ql2DR#FB=+HwJ?S4;F9@cQ`GMS8&?2IHhisY8v+^Nb;-RoUFSg?(dW_TIkeO(X#^SyGl+hgZt2Pswm>Ufjy$ zx<LO?EbW@)M34+D|galcA zluF{umdT9*XB&Q$=?MVnsiqxRHrKx9AYq8TsWs{BLDZ-%vt1*kOSx5kYYW2vZd z(W{h~fY}SLZ)762++yq3Q>xAQNz~A4ZgpVPF^2Hae>Jswp~m^m z`~|k6;0Ibtx>OF7U&ufA+*Z-a?E4EQvd6p|-Chk4&Rw!T2ix;I@elLJZ+s9+${`%m zeuaRT1RM;}6*#3yY8~5l{e3TOqDXd|+sEYk(A--4Q1@6#EOZGLfB0oqst8-D_k~|I zi$XdhXq~VY2Irh~_yFx@?6C8cp;{u1i{2N(bI2`bJ+bI^{ymflF-7i~c%}j}z|lwL zmzxxP2x~OL(H^yvbqwwmSSVqYxC7{G`qKvYbGe&y8M)1r4T8=s)wZKr%Z)3$K!ZHX zZyp8bIOv0_>60?a`@8H>Ka$6eiO%yl`5=sCC@LuM;~y$cyVC2xFu+bg67lF}Skk$L zDD+Y)pkI*s8VFLsGZ%7vFG~QEyDEQrTt6F>b2d?~A=tes&NwELtPh%l?Z5z^hbkXRbR!*X%^Bd=>P-r7rh3S9VDxt$6TfI`-w9 z(GZ2T0eTP%9)h(WnndYozO6BkbRYiMs}7i%@WoW!S1G8WNkV_=MK?LG-!F0zqXfhw zOsh-V^_cs##Z;5J>A`D3KOVE&q0i0g%j8ofy?mV6+MBI(CM zJmyA(O4kd+0RInvu`np}%KU8-II&$cF3^xgfcYuBa=VfAIzSM^82D%lYWCqnEC7m{ zK-FYA^4Ow|^$E!wP{}yw786wLT%C6x9bRB~IqiHlH0SBZ&w0P+RomkxWB^N-AU7&~ z=T2op?4ju|YjNK4S#iq~fq=?juc}-9i5D;>K*D1l%q#;@3!=pGo&dmdJ*`{4Ter;x zYRit~eR%Sp2I(9e0a~L~(B#mqFA?B75aPgE4ZM9)jnxzA+mPIH_wdbjPh|IiY1*xJ4#n`jx5xDCiACbE*> z^n0}jsTkfCpFOvvetrP`@xfHOAZB7dzL=tD$SZO|jI+emunVNhl&AIa-VT5zEC6GB zh$&W?;rFRcaoFg4J!2`O8OxcQA_0h02jD1T=D(^!&dd=l%Abd`ghknWa9CN-4q3li zwNNHXSMo)1Zf$CL^Y^(S!<0)G)tVoZej8ssK+aMUfqAl8-T3ZU`uugY*a6>X!4j{Q zOQ|tH5YVAn+Z+uN^p8bo zJyQZ}@~?>a%$H-C?B`^~3t~2kIq8A`gn)j(MF@dm=P{rRATjAb-UZbkU{qz#_UJgk z%GIr~+jf*g@-FEw;ae!gc`GlbA+^f)9(1Bh0HX4=?{$S|)aAG#fP1ni4qsL468eQ@X4hY*V zK&M3^E$J~HU?4%)1@j}_RvZu^G=;_W102lAO8&H`-jxj6A-ZSfR0b!pU4Yt0a>S0@ zCPwU$RVC?Hra^#h%O7H8rnf-CA;nCpAP z$dW56hfr`laa29yo5GCDYQ-33ShTJk#rt@lxD1`rPa5KWZHn`1FYi9zwKF(O{80hM zEFfSnmh2aPfh>+7&1cKL3ec68uvW81;0{D4vYA2cMb%uzXUHu<^`k>_Zd=sY8c;BE zC+|&z<(B1>@_1`s%mmQnYKKXQ<#RoP#ep2Ilnga_`{7gDJIN=BAvz0y*rN(Go4Fa> z^$zHsDvy!QMVToI0x7lRyUv?OL#bjN@i}nua`=FZ8if;iA53oU&Q~kJi6|X}{R>|p zEcB#;SCybnpZC9%JBKHvx6A=cd1~a7N2hF(?EXCc5_3ev0ko^G{r9FVDSc_=^G_;1 z3WU2|g|@w#rQgeQIk4+UDt)Xnd|4?aj`X%|SN|7~P1A_b88A4o=32WB1AZE{xS0~W zhHnR|);{MJkq_*!TmdJJ2y%ioZA@l{M0;U|1->OtPWcBP{Shl~PzpP4O^yV1F1cnb zIOy8yk3Iual;QTOs) zM(sK>CWKlAanc`1TV)t&_9U$GtS_;D@yVDI%3h*=%u*VIAr2GikII!4AhWNXAbTLZaPbss zAoCUasmB3DfYKYzgn?+Vhu5b7u586~hF>aCn%qyj__Z+Qmx5Fnv2SxM`hwD5)mDg( z1lZ~S;^dTgyZT1i1>zQQdr_>D!zEvT%d@;g!has1@sL;lXT~zis$G=i50e#_CH#wX z30XsyVJxa|Ka>LqW_=gJ#*5`hdETU2^Jiv1mzE`$GP)$eNu5sQrXOeNi(%uN!n z*u|b$^aqHN2Qh88XvYB`?5E{ZaP9+Dlh3#e%6e^Z^VCfsCj^#O__>Qa<`u_=WwrAg<@-R=$1?| z-*vD1=vCfymXHdFVj>x^+UE|cucDSqlS_iFSYVdl^UW(+OhM1wXvP7eB*`{JGKrSE z%sNP(MLr$UV^Dcw(T`ZSR|vx$Iv_QLSJG|@EN$R}F=Pjz@L2eQpf>zK#Ih`hITFfRLxf?~X*K^Ui29~1xYo)y5h^F6enYR?(={g!`QpBlnY=-Y%pNJ4&O+s)Xqx~eTJ%of0u9@dd6ta1qOYVU@ZR>v`JaxysbA3?c;c7z1Isu4%`#l z)=h{T>#&z*+(K&HwvT>zWLq3lO_H)Qq7cn`#cfY5ObVDwi6QxKWHSBmH(KB5qn}8L z>Guf-9C>NnX4Kg?Ov6`SSawR0X2V9ZuWwGx3N1IU_LFPD zRcNDdb8lnB!kY?bvlmoO6Zz@PP5*$H1EP~3A^Wz`2UtH6tbCJQ&(S==6t^V@AAaPu z!Da#ly5L<)-!8Yz^E_Il_QIk*h9zIl>_$pQhuic_jSzR7X3QkHY`co;Fh-z?!{SDn zKnYv4{8C1`C0H(fBv;I2eX=-F+)~;(kcXm*=}4U2NSZaY;y80@01XtMnV6Gm*&~u1 zrJ6kA6IWH+)Ih%AWw@i-E2h0a3FMW)qa18aLiiG$GZ(8kRaTf`GXID59-)$7!vMY+ zNZUO20y|b-<@0VY39E4{B|iKinZQt!xezMM5Qbg;@sZlbM7tmp=^06up{Dj|t&cvk zjUAxv4Rf9H97hbX==rmmuy0rTq6SV%N)tMI3&a8&`??IElw3A^g!&j%Dum9ek3omf z@=zO{xvvwRz#_~wq;T4nrTPTkg`1l#5Wj4*ks9FJwR~6b#Wmmc{Syv5A4VaMPDWMy zANu3FUCWMeR~K@AVR(ItB#l`KnrPKegI=v)mA|C$YkXenWc{8pbwiS~5c1a(WUK>K z3wS>{1VGmM@uXf03P7VeKv)#)#SyWjPms^%hTQF;N{2V z>Zkv^N=`r)5r1D}pum~p%-@h?B(E^Z--VpU;kDPt?4u%m#baavC?<3vJ;w~Am&+`x z%VSz>085kWwPPe3as5&j!IHyahoHd_R;rO)h4#dbtt!bfts|F3S0ZpsP{fZE9$?uO&DNH z;3(g`vv8_}uO7Hy%PW|O1F?rK*u0m*(GcxK^c{4|r2dHE{N^?lvEito+D#>a0YqH! zv0PdUZ!37N*G42}<;E+4Hc*1;&&}|gPiX&8z2R84%(v-VAxKe~8s?hkpJwEcMRGOK zbhG2)%^doHW{Z~zrnD(e`2egyQyktqFS5sZy2n{{NFR9)h(Hvw@4X6dalvwvAQ$KH zzLT@>y>Y*6TX|pCHipJykn6c(_(f`U*dLiG0cV z$r&j$)6&3rhTj7=0+h&+P~*IF-cETo0$HS$)mfUzwW@D=$nK9W_iEZV!I+P5MYzu7 zro}0bu=Hp5YXZjov$8Wu$crD+4eKKnFBBPENH>5sx%?`N@-{(sk@IxNY*!Pqb;@%4A7h@0HtbU#gDiQ+Oc zWwPjRKoj1{2=`@$;`T6X@ObqEqYmjQv?`D1_83tY zzJ+L$c$L%l#p(UtQ)87jBlYXG&CBgr^Q2}fMttK(qV7~LGoLC%4Pm`7W-JhuCo)y8 ztM2|?$7Y{)la{>po-dE%*juTW?Q;e4ZQs}`y|@?I1*l2M0>F5h8Jp5~;+}8Tbf^6@ z%a<7rceLPnMo7eN_S>m}c>a89AOpxK z-7{;pDZf7>0kR{(5cD&r;TZXEn#W>P);wnBszHA76cK$P0X*Z#nfjKS1!rzTn$0DQ9;8#cHmM;AWG2YOfj) zmn3(y;+@GjiN4KK#Ztc!j~`KCEB!BJ5lNgW*Se;-r_gcdoGi-)z>Vdj^l8= zx(#y~4XtKk{(1B>nWPKpBo#weH3aS47SM{GoZ08MODx)-&IFBsUd!JI?8Whnxo;tj zYhVRsog@cA}&S7HHwyz_`62+C{jF-Nd z6U~*1G712^y;wO3&V&Ozc`s`ecVD(JA2sGo+iUQ(e08I49jNN?W}w)*LAMHLrkoG& z^o>H9P5z@lIjrN0_OVoT>lt3mzI;w2sXV8P=`jh z^h_G00h3Z4VD6-VFcGT}OQST7f!{{WF@Qlqc@49ThdfU`%8r z&|vH86el&@92l```26F4?w$66{Q@HBk{^YkKr^#ln^o=SrH|`gl)lxu)^l`{H6i^! zD~*Y4o@6xotK?qrg?pUzbg=zWa~c0Mt-m)lMd3 zUy(VnEeiXLHs(3b*vKD1g;~u2*;SEx=+KHC0bKmwAAD5@BfJZEB6uWOi(5sU76>N& zlZLZ))(9ih`hC>YB4Fb`^0&bAF^Lsvby-`T?N!~Y8?i`g9;2C(naD@Vb1YhG9hbZD0jC-u z2g5SQ@P}y@X#z%)Gm%g|k1%EVUf$Ny{qJ=Kd%$(GbjonqM6)(pXmT#5#7RunmD+ymFh>DZ!RCM|(f{aK zzG|NWL~HfGZZ%_tvFAL4-OqOL84@_1-tTHJlJ43*8ms01k2zwPthTheK$bbb2t+1e z+FAlxT6CLF(?3gd(SHjWcz@n{QTEA%0pq_bK#{xy&gri|oIqiPP9FKpI$A~hdq-O_1wV?%(Wd#G(^&l9 z7Xv$X#Hs^r&65i0RUV1jt0eIuqtqXrk0Hm6o@*n1V>-9%u}|ibApcACwO}P%HiK(*FC#{(J5J?AibKf4L)GkfgAk Wu}9ak=K(o;7&$3r$!c+