diff --git a/.coveragerc b/.coveragerc index 05682c79744..693959684f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -6,7 +6,6 @@ omit = homeassistant/helpers/signal.py homeassistant/helpers/typing.py homeassistant/scripts/*.py - homeassistant/util/async.py # omit pieces of code that rely on external devices being present homeassistant/components/abode/__init__.py @@ -32,7 +31,6 @@ omit = homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py - homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarmdecoder/* homeassistant/components/alarmdotcom/alarm_control_panel.py homeassistant/components/alpha_vantage/sensor.py @@ -116,7 +114,6 @@ omit = homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py - homeassistant/components/ciscospark/notify.py homeassistant/components/citybikes/sensor.py homeassistant/components/clementine/media_player.py homeassistant/components/clickatell/notify.py @@ -256,13 +253,15 @@ omit = homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py + homeassistant/components/garmin_connect/__init__.py + homeassistant/components/garmin_connect/const.py + homeassistant/components/garmin_connect/sensor.py homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py homeassistant/components/gios/__init__.py homeassistant/components/gios/air_quality.py - homeassistant/components/gios/consts.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py @@ -307,7 +306,6 @@ omit = homeassistant/components/homematic/notify.py homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py - homeassistant/components/hook/switch.py homeassistant/components/horizon/media_player.py homeassistant/components/hp_ilo/sensor.py homeassistant/components/htu21d/sensor.py @@ -324,6 +322,7 @@ omit = homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/__init__.py + homeassistant/components/icloud/account.py homeassistant/components/icloud/device_tracker.py homeassistant/components/icloud/sensor.py homeassistant/components/izone/climate.py @@ -421,7 +420,8 @@ omit = homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py - homeassistant/components/mikrotik/* + homeassistant/components/mikrotik/hub.py + homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/minio/* @@ -455,8 +455,13 @@ omit = homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* - homeassistant/components/netatmo/* - homeassistant/components/netatmo_public/sensor.py + homeassistant/components/netatmo/__init__.py + homeassistant/components/netatmo/binary_sensor.py + homeassistant/components/netatmo/api.py + homeassistant/components/netatmo/camera.py + homeassistant/components/netatmo/climate.py + homeassistant/components/netatmo/const.py + homeassistant/components/netatmo/sensor.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear_lte/* @@ -504,13 +509,13 @@ omit = homeassistant/components/openuv/sensor.py homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py + homeassistant/components/opnsense/* homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py - homeassistant/components/owlet/* homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py @@ -530,12 +535,10 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py - homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* - homeassistant/components/postnl/sensor.py homeassistant/components/prezzibenzina/sensor.py homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* @@ -637,6 +640,7 @@ omit = homeassistant/components/smappee/* homeassistant/components/smarty/* homeassistant/components/smarthab/* + homeassistant/components/sms/* homeassistant/components/smtp/notify.py homeassistant/components/snapcast/media_player.py homeassistant/components/snmp/* @@ -659,6 +663,7 @@ omit = homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* homeassistant/components/spotcrime/sensor.py + homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/* homeassistant/components/starline/* @@ -724,7 +729,6 @@ omit = homeassistant/components/torque/sensor.py homeassistant/components/totalconnect/* homeassistant/components/touchline/climate.py - homeassistant/components/tplink/device_tracker.py homeassistant/components/tplink/switch.py homeassistant/components/tplink_lte/* homeassistant/components/traccar/device_tracker.py @@ -749,7 +753,6 @@ omit = homeassistant/components/twitch/sensor.py homeassistant/components/twitter/notify.py homeassistant/components/ubee/device_tracker.py - homeassistant/components/uber/sensor.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/unifiled/* @@ -779,7 +782,9 @@ omit = homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* homeassistant/components/vivotek/camera.py - homeassistant/components/vizio/* + homeassistant/components/vizio/__init__.py + homeassistant/components/vizio/const.py + homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py @@ -825,7 +830,6 @@ omit = homeassistant/components/zestimate/sensor.py homeassistant/components/zha/__init__.py homeassistant/components/zha/api.py - homeassistant/components/zha/const.py homeassistant/components/zha/core/channels/* homeassistant/components/zha/core/const.py homeassistant/components/zha/core/device.py @@ -833,7 +837,6 @@ omit = homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/patches.py homeassistant/components/zha/core/registries.py - homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index f68fbbc800c..00000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,48 +0,0 @@ - - -**Home Assistant release with the issue:** - - - -**Last working Home Assistant release (if known):** - - -**Operating environment (Hass.io/Docker/Windows/etc.):** - - -**Integration:** - - - -**Description of problem:** - - - -**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** -```yaml - -``` - -**Traceback (if applicable):** -``` - -``` - -**Additional information:** - diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 00000000000..977abc6ef6b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,53 @@ +--- +name: Report a bug with Home Assistant +about: Report an issue with Home Assistant +--- + +## The problem + + + +## Environment + + +- Home Assistant release with the issue: +- Last working Home Assistant release (if known): +- Operating environment (Hass.io/Docker/Windows/etc.): +- Integration causing this issue: +- Link to integration documentation on our website: + +## Problem-relevant `configuration.yaml` + + +```yaml + +``` + +## Traceback/Error logs + + +```txt + +``` + +## Additional information + diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md deleted file mode 100644 index 885164d7a34..00000000000 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - - - -**Home Assistant release with the issue:** - - - -**Last working Home Assistant release (if known):** - - -**Operating environment (Hass.io/Docker/Windows/etc.):** - - -**Integration:** - - - -**Description of problem:** - - - -**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** -```yaml - -``` - -**Traceback (if applicable):** -``` - -``` - -**Additional information:** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..5b0b8c46e96 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: false +contact_links: + - name: Report a bug with the UI, Frontend or Lovelace + url: https://github.com/home-assistant/home-assistant-polymer/issues + about: This is the issue tracker for our backend. Please report issues with the UI in the frontend repository. + - name: Report incorrect or missing information on our website + url: https://github.com/home-assistant/home-assistant.io/issues + about: Our documentation has its own issue tracker. Please report issues with the website there. + - name: I have a question or need support + url: https://www.home-assistant.io/help + about: We use GitHub for tracking bugs, check our website for resources on getting help. + - name: Feature Request + url: https://community.home-assistant.io/c/feature-requests + about: Please use our Community Forum for making feature requests. + - name: I'm unsure where to go + url: https://www.home-assistant.io/join-chat + about: If you are unsure where to go, then joining our chat is recommended; Just ask! diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 474dff86b3d..1ada6d3af86 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,35 +1,109 @@ -## Breaking Change: - - - -## Description: + +## Breaking change + -**Related issue (if applicable):** fixes # +## Proposed change + -**Pull request with documentation for [home-assistant.io](https://github.com/home-assistant/home-assistant.io) (if applicable):** home-assistant/home-assistant.io# -## Example entry for `configuration.yaml` (if applicable): +## Type of change + + +- [ ] Dependency upgrade +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New integration (thank you!) +- [ ] New feature (which adds functionality to an existing integration) +- [ ] Breaking change (fix/feature causing existing functionality to break) +- [ ] Code quality improvements to existing code or addition of tests + +## Example entry for `configuration.yaml`: + + ```yaml +# Example configuration.yaml ``` -## Checklist: - - [ ] The code change is tested and works locally. - - [ ] Local tests pass with `tox`. **Your PR cannot be merged unless tests pass** - - [ ] There is no commented out code in this PR. - - [ ] I have followed the [development checklist][dev-checklist] +## Additional information + + +- This PR fixes or closes issue: fixes # +- This PR is related to issue: +- Link to documentation pull request: + +## Checklist + + +- [ ] The code change is tested and works locally. +- [ ] Local tests pass. **Your PR cannot be merged unless tests pass** +- [ ] There is no commented out code in this PR. +- [ ] I have followed the [development checklist][dev-checklist] +- [ ] The code has been formatted using Black (`black --fast homeassistant tests`) +- [ ] Tests have been added to verify that the new code works. If user exposed functionality or configuration variables are added/changed: - - [ ] Documentation added/updated in [home-assistant.io](https://github.com/home-assistant/home-assistant.io) + +- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository] If the code communicates with devices, web services, or third-party tools: - - [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly. Update and include derived files by running `python3 -m script.hassfest`. - - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `python3 -m script.gen_requirements_all`. - - [ ] Untested files have been added to `.coveragerc`. -If the code does not interact with devices: - - [ ] Tests have been added to verify that the new code works. +- [ ] The [manifest file][manifest-docs] has all fields filled out correctly. + Updated and included derived files by running: `python3 -m script.hassfest`. +- [ ] New or updated dependencies have been added to `requirements_all.txt`. + Updated by running `python3 -m script.gen_requirements_all`. +- [ ] Untested files have been added to `.coveragerc`. +The integration reached or maintains the following [Integration Quality Scale][quality-scale]: + + +- [ ] No score or internal +- [ ] 🥈 Silver +- [ ] 🥇 Gold +- [ ] 🏆 Platinum + + [dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html [manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html +[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html +[docs-repository]: https://github.com/home-assistant/home-assistant.io diff --git a/.hound.yml b/.hound.yml deleted file mode 100644 index c5ab91614dc..00000000000 --- a/.hound.yml +++ /dev/null @@ -1,2 +0,0 @@ -python: - enabled: true diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 1eabfcb0017..a6b882e617b 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -24,7 +24,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==5.0.1 + - pydocstyle==5.0.2 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 226708bb947..1f27e82b6d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==5.0.1 + - pydocstyle==5.0.2 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/CODEOWNERS b/CODEOWNERS index 6e4ea0e8b77..6983d13fc8b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,6 +23,7 @@ homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya +homeassistant/components/amcrest/* @pnbruckner homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core @@ -59,7 +60,6 @@ homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl -homeassistant/components/ciscospark/* @fbradyirl homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus homeassistant/components/comfoconnect/* @michaelarnauts @@ -76,12 +76,14 @@ homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @kane610 homeassistant/components/delijn/* @bollewolle homeassistant/components/demo/* @home-assistant/core +homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff +homeassistant/components/dyson/* @etheralm homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt @@ -89,7 +91,6 @@ homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elgato/* @frenck homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 -homeassistant/components/emulated_hue/* @NobleKangaroo homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/entur_public_transport/* @hfurubotten @@ -115,6 +116,7 @@ homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend +homeassistant/components/garmin_connect/* @cyberjunky homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte @@ -129,6 +131,7 @@ homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 homeassistant/components/gpsd/* @fabaff +homeassistant/components/greeneye_monitor/* @jkeljo homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 @@ -168,7 +171,7 @@ homeassistant/components/intent/* @home-assistant/core homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 homeassistant/components/iperf3/* @rohankapoorcom -homeassistant/components/ipma/* @dgomes +homeassistant/components/ipma/* @dgomes @abmantis homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig @@ -206,6 +209,7 @@ homeassistant/components/met/* @danielhiversen homeassistant/components/meteo_france/* @victorcerutti @oncleben31 homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel +homeassistant/components/mikrotik/* @engrbm87 homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff homeassistant/components/minio/* @tkislan @@ -219,9 +223,11 @@ homeassistant/components/msteams/* @peroyvind homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/neato/* @dshokouhi @Santobert +homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan +homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/nextbus/* @vividboarder homeassistant/components/nilu/* @hfurubotten @@ -243,9 +249,9 @@ homeassistant/components/onewire/* @garbled1 homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff +homeassistant/components/opnsense/* @mtreinish homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/oru/* @bvlaicu -homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/pcal9535a/* @Shulyaka @@ -277,11 +283,13 @@ homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt +homeassistant/components/safe_mode/* @home-assistant/core homeassistant/components/saj/* @fredericvl homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core +homeassistant/components/search/* @home-assistant/core homeassistant/components/sense/* @kbickar homeassistant/components/sensibo/* @andrey-git homeassistant/components/sentry/* @dcramer @@ -290,14 +298,17 @@ homeassistant/components/seventeentrack/* @bachya homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff +homeassistant/components/sighthound/* @robmarkcole homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb +homeassistant/components/sisyphus/* @jkeljo homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess +homeassistant/components/sms/* @ocalvo homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solarlog/* @Ernst79 @@ -308,6 +319,7 @@ homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen +homeassistant/components/spotify/* @frenck homeassistant/components/sql/* @dgomes homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff @@ -351,6 +363,7 @@ homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 +homeassistant/components/ubee/* @mzdrale homeassistant/components/unifi/* @kane610 homeassistant/components/unifiled/* @florisvdk homeassistant/components/upc_connect/* @pvizeli @@ -360,7 +373,7 @@ homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes -homeassistant/components/velbus/* @cereal2nd +homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 5092010c49c..b537aa3bf53 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -30,7 +30,7 @@ jobs: - template: templates/azp-job-wheels.yaml@azure parameters: builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev' + builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' @@ -68,6 +68,7 @@ jobs: sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} + sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} if [[ "$(buildArch)" =~ arm ]]; then sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 5ebdc71680e..d1d59482e6d 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -6,13 +6,10 @@ import platform import subprocess import sys import threading -from typing import TYPE_CHECKING, Any, Dict, List +from typing import List from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ -if TYPE_CHECKING: - from homeassistant import core - def set_loop() -> None: """Attempt to use different loop.""" @@ -78,19 +75,6 @@ def ensure_config_path(config_dir: str) -> None: sys.exit(1) -async def ensure_config_file(hass: "core.HomeAssistant", config_dir: str) -> str: - """Ensure configuration file exists.""" - import homeassistant.config as config_util - - config_path = await config_util.async_ensure_config_exists(hass, config_dir) - - if config_path is None: - print("Error getting configuration path") - sys.exit(1) - - return config_path - - def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" import homeassistant.config as config_util @@ -107,7 +91,7 @@ def get_arguments() -> argparse.Namespace: help="Directory that contains the Home Assistant configuration", ) parser.add_argument( - "--demo-mode", action="store_true", help="Start Home Assistant in demo mode" + "--safe-mode", action="store_true", help="Start Home Assistant in safe mode" ) parser.add_argument( "--debug", action="store_true", help="Start Home Assistant in debug mode" @@ -253,34 +237,20 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up Home Assistant and run.""" - from homeassistant import bootstrap, core + from homeassistant import bootstrap - hass = core.HomeAssistant() + hass = await bootstrap.async_setup_hass( + config_dir=config_dir, + verbose=args.verbose, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + skip_pip=args.skip_pip, + safe_mode=args.safe_mode, + ) - if args.demo_mode: - config: Dict[str, Any] = {"frontend": {}, "demo": {}} - bootstrap.async_from_config_dict( - config, - hass, - config_dir=config_dir, - verbose=args.verbose, - skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - ) - else: - config_file = await ensure_config_file(hass, config_dir) - print("Config directory:", config_dir) - await bootstrap.async_from_config_file( - config_file, - hass, - verbose=args.verbose, - skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - ) + if hass is None: + return 1 if args.open_ui and hass.config.api is not None: import webbrowser @@ -358,7 +328,7 @@ def main() -> int: return scripts.run(args.script) - config_dir = os.path.join(os.getcwd(), args.config) + config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) ensure_config_path(config_dir) # Daemon functions diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7ceedba5bd5..3d8523bf9ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,6 +1,5 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio -from collections import OrderedDict import logging import logging.handlers import os @@ -11,6 +10,7 @@ from typing import Any, Dict, Optional, Set import voluptuous as vol from homeassistant import config as conf_util, config_entries, core, loader +from homeassistant.components import http from homeassistant.const import ( EVENT_HOMEASSISTANT_CLOSE, REQUIRED_NEXT_PYTHON_DATE, @@ -42,16 +42,68 @@ STAGE_1_INTEGRATIONS = { } +async def async_setup_hass( + *, + config_dir: str, + verbose: bool, + log_rotate_days: int, + log_file: str, + log_no_color: bool, + skip_pip: bool, + safe_mode: bool, +) -> Optional[core.HomeAssistant]: + """Set up Home Assistant.""" + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + + async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + + hass.config.skip_pip = skip_pip + if skip_pip: + _LOGGER.warning( + "Skipping pip installation of required modules. This may cause issues" + ) + + if not await conf_util.async_ensure_config_exists(hass): + _LOGGER.error("Error getting configuration path") + return None + + _LOGGER.info("Config directory: %s", config_dir) + + config_dict = None + + if not safe_mode: + await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) + + try: + config_dict = await conf_util.async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error( + "Failed to parse configuration.yaml: %s. Falling back to safe mode", + err, + ) + else: + if not is_virtual_env(): + await async_mount_local_lib_path(config_dir) + + await async_from_config_dict(config_dict, hass) + finally: + clear_secret_cache() + + if safe_mode or config_dict is None: + _LOGGER.info("Starting in safe mode") + + http_conf = (await http.async_get_last_config(hass)) or {} + + await async_from_config_dict( + {"safe_mode": {}, "http": http_conf}, hass, + ) + + return hass + + async def async_from_config_dict( - config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False, + config: Dict[str, Any], hass: core.HomeAssistant ) -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -60,15 +112,6 @@ async def async_from_config_dict( """ start = time() - if enable_log: - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) - - hass.config.skip_pip = skip_pip - if skip_pip: - _LOGGER.warning( - "Skipping pip installation of required modules. This may cause issues" - ) - core_config = config.get(core.DOMAIN, {}) try: @@ -83,14 +126,6 @@ async def async_from_config_dict( ) return None - # Make a copy because we are mutating it. - config = OrderedDict(config) - - # Merge packages - await conf_util.merge_packages_config( - hass, config, core_config.get(conf_util.CONF_PACKAGES, {}) - ) - hass.config_entries = config_entries.ConfigEntries(hass, config) await hass.config_entries.async_initialize() @@ -116,46 +151,6 @@ async def async_from_config_dict( return hass -async def async_from_config_file( - config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False, -) -> Optional[core.HomeAssistant]: - """Read the configuration file and try to start all the functionality. - - Will add functionality to 'hass' parameter. - This method is a coroutine. - """ - # Set config dir to directory holding config file - config_dir = os.path.abspath(os.path.dirname(config_path)) - hass.config.config_dir = config_dir - - if not is_virtual_env(): - await async_mount_local_lib_path(config_dir) - - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) - - await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) - - try: - config_dict = await hass.async_add_executor_job( - conf_util.load_yaml_config_file, config_path - ) - except HomeAssistantError as err: - _LOGGER.error("Error loading %s: %s", config_path, err) - return None - finally: - clear_secret_cache() - - return await async_from_config_dict( - config_dict, hass, enable_log=False, skip_pip=skip_pip - ) - - @core.callback def async_enable_logging( hass: core.HomeAssistant, @@ -269,7 +264,8 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - domains.update(hass.config_entries.async_domains()) + if "safe_mode" not in config: + domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded if "HASSIO" in os.environ: diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index 88a072bd79c..b9a0a8ce192 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -21,11 +21,6 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode alarm control panel device.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 56c7bbcc1ff..c27357ca076 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -13,11 +13,6 @@ from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode binary sensor devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index c6f366e0e51..1742a0a5d6c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -18,11 +18,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode camera devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index a4fce7e7b8a..ec4f54a985c 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -11,11 +11,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index c02019e6bcc..ad2df23ef9c 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -24,11 +24,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index e7ed40849de..b05a3e7f297 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -11,11 +11,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index ce71906dfcc..383320141e5 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,7 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==0.16.7"], + "requirements": ["abodepy==0.17.0"], "dependencies": [], "codeowners": ["@shred86"] } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 573df6d49b4..dc622cb1a38 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -22,11 +22,6 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode sensor devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index c092c1ef3f0..bbe3f01f488 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -12,11 +12,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode switch devices.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/adguard/.translations/cs.json b/homeassistant/components/adguard/.translations/cs.json new file mode 100644 index 00000000000..fc450c2e908 --- /dev/null +++ b/homeassistant/components/adguard/.translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?", + "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/de.json b/homeassistant/components/adguard/.translations/de.json index 3434b6feac6..c1ef5bb7926 100644 --- a/homeassistant/components/adguard/.translations/de.json +++ b/homeassistant/components/adguard/.translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, Sie haben {current_version}. Bitte aktualisieren Sie Ihr Hass.io AdGuard Home Add-on.", - "adguard_home_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, Sie haben {current_version}.", + "adguard_home_addon_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, du hast {current_version}. Bitte aktualisiere dein Hass.io AdGuard Home Add-on.", + "adguard_home_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, du hast {current_version}.", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 1f4d63d627b..6996a2b0d51 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -142,11 +142,14 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool class AdGuardHomeEntity(Entity): """Defines a base AdGuard Home entity.""" - def __init__(self, adguard, name: str, icon: str) -> None: + def __init__( + self, adguard, name: str, icon: str, enabled_default: bool = True + ) -> None: """Initialize the AdGuard Home entity.""" - self._name = name - self._icon = icon self._available = True + self._enabled_default = enabled_default + self._icon = icon + self._name = name self.adguard = adguard @property @@ -159,6 +162,11 @@ class AdGuardHomeEntity(Entity): """Return the mdi icon of the entity.""" return self._icon + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + @property def available(self) -> bool: """Return True if entity is available.""" @@ -166,6 +174,9 @@ class AdGuardHomeEntity(Entity): async def async_update(self) -> None: """Update AdGuard Home entity.""" + if not self.enabled: + return + try: await self._adguard_update() self._available = True diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index c818752ad2f..e5618282a97 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -51,14 +51,20 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity): """Defines a AdGuard Home sensor.""" def __init__( - self, adguard, name: str, icon: str, measurement: str, unit_of_measurement: str + self, + adguard, + name: str, + icon: str, + measurement: str, + unit_of_measurement: str, + enabled_default: bool = True, ) -> None: """Initialize AdGuard Home sensor.""" self._state = None self._unit_of_measurement = unit_of_measurement self.measurement = measurement - super().__init__(adguard, name, icon) + super().__init__(adguard, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -109,6 +115,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): "mdi:magnify-close", "blocked_filtering", "queries", + enabled_default=False, ) async def _adguard_update(self) -> None: @@ -214,7 +221,12 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): def __init__(self, adguard): """Initialize AdGuard Home sensor.""" super().__init__( - adguard, "AdGuard Rules Count", "mdi:counter", "rules_count", "rules" + adguard, + "AdGuard Rules Count", + "mdi:counter", + "rules_count", + "rules", + enabled_default=False, ) async def _adguard_update(self) -> None: diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 39cd1ef028d..1ddefb3367b 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -10,9 +10,9 @@ from homeassistant.components.adguard.const import ( DATA_ADGUARD_VERION, DOMAIN, ) +from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -45,14 +45,16 @@ async def async_setup_entry( async_add_entities(switches, True) -class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity): +class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchDevice): """Defines a AdGuard Home switch.""" - def __init__(self, adguard, name: str, icon: str, key: str): + def __init__( + self, adguard, name: str, icon: str, key: str, enabled_default: bool = True + ): """Initialize AdGuard Home switch.""" self._state = False self._key = key - super().__init__(adguard, name, icon) + super().__init__(adguard, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -204,7 +206,13 @@ class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): def __init__(self, adguard) -> None: """Initialize AdGuard Home switch.""" - super().__init__(adguard, "AdGuard Query Log", "mdi:shield-check", "querylog") + super().__init__( + adguard, + "AdGuard Query Log", + "mdi:shield-check", + "querylog", + enabled_default=False, + ) async def _adguard_turn_off(self) -> None: """Turn off the switch.""" diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json index c2c14d1d101..52bf903d5a8 100644 --- a/homeassistant/components/airly/.translations/da.json +++ b/homeassistant/components/airly/.translations/da.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly-integration for disse koordinater er allerede konfigureret." + }, "error": { "auth": "API-n\u00f8glen er ikke korrekt.", "name_exists": "Navnet findes allerede.", diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json index 83c23a90389..ef2b2d64a4e 100644 --- a/homeassistant/components/airly/.translations/de.json +++ b/homeassistant/components/airly/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Die Airly-Integration ist f\u00fcr diese Koordinaten bereits konfiguriert." + }, "error": { "auth": "Der API-Schl\u00fcssel ist nicht korrekt.", "name_exists": "Name existiert bereits", diff --git a/homeassistant/components/airly/.translations/en.json b/homeassistant/components/airly/.translations/en.json index 83284aaeb7b..cae6854d231 100644 --- a/homeassistant/components/airly/.translations/en.json +++ b/homeassistant/components/airly/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly integration for these coordinates is already configured." + }, "error": { "auth": "API key is not correct.", "name_exists": "Name already exists.", diff --git a/homeassistant/components/airly/.translations/es.json b/homeassistant/components/airly/.translations/es.json index 0c29ad0bc66..6fd18eb747c 100644 --- a/homeassistant/components/airly/.translations/es.json +++ b/homeassistant/components/airly/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3n a\u00e9rea para estas coordenadas ya est\u00e1 configurada." + }, "error": { "auth": "La clave de la API no es correcta.", "name_exists": "El nombre ya existe.", diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index 374e578eed2..f2fdbbd9754 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'int\u00e9gration des coordonn\u00e9es d'Airly est d\u00e9j\u00e0 configur\u00e9." + }, "error": { "auth": "La cl\u00e9 API n'est pas correcte.", "name_exists": "Le nom existe d\u00e9j\u00e0.", diff --git a/homeassistant/components/airly/.translations/it.json b/homeassistant/components/airly/.translations/it.json index e50f618575b..c52e77881c0 100644 --- a/homeassistant/components/airly/.translations/it.json +++ b/homeassistant/components/airly/.translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'integrazione Airly per queste coordinate \u00e8 gi\u00e0 configurata." + }, "error": { "auth": "La chiave API non \u00e8 corretta.", "name_exists": "Il nome \u00e8 gi\u00e0 esistente", diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json index eb20c9174b4..b64a16635a6 100644 --- a/homeassistant/components/airly/.translations/ko.json +++ b/homeassistant/components/airly/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uc88c\ud45c\uc5d0 \ub300\ud55c Airly \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.", diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json index 08aac57d162..8c2f5c615f3 100644 --- a/homeassistant/components/airly/.translations/lb.json +++ b/homeassistant/components/airly/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly Integratioun fir d\u00ebs Koordinaten ass scho konfigur\u00e9iert." + }, "error": { "auth": "Api Schl\u00ebssel ass net korrekt.", "name_exists": "Numm g\u00ebtt et schonn", diff --git a/homeassistant/components/airly/.translations/no.json b/homeassistant/components/airly/.translations/no.json index 70924bb7bf4..ada9955f9c5 100644 --- a/homeassistant/components/airly/.translations/no.json +++ b/homeassistant/components/airly/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Airly integrering for disse koordinatene er allerede konfigurert." + }, "error": { "auth": "API-n\u00f8kkelen er ikke korrekt.", "name_exists": "Navnet finnes allerede.", diff --git a/homeassistant/components/airly/.translations/pl.json b/homeassistant/components/airly/.translations/pl.json index 5d601b37591..5274a4383b6 100644 --- a/homeassistant/components/airly/.translations/pl.json +++ b/homeassistant/components/airly/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Integracja Airly dla tych wsp\u00f3\u0142rz\u0119dnych jest ju\u017c skonfigurowana." + }, "error": { "auth": "Klucz API jest nieprawid\u0142owy.", "name_exists": "Nazwa ju\u017c istnieje.", diff --git a/homeassistant/components/airly/.translations/ru.json b/homeassistant/components/airly/.translations/ru.json index 36080c9f372..5094d3f4d1e 100644 --- a/homeassistant/components/airly/.translations/ru.json +++ b/homeassistant/components/airly/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Airly \u0441 \u0442\u0430\u043a\u0438\u043c\u0438 \u0436\u0435 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." + }, "error": { "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", diff --git a/homeassistant/components/airly/.translations/zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json index bb38d2b9b8c..5bc0a52f394 100644 --- a/homeassistant/components/airly/.translations/zh-Hant.json +++ b/homeassistant/components/airly/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64 Airly \u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 17e1d27e571..bad5a48c05f 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -41,6 +41,12 @@ async def async_setup_entry(hass, config_entry): latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] + # For backwards compat, set unique ID + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=f"{latitude}-{longitude}" + ) + websession = async_get_clientsession(hass) airly = AirlyData(websession, api_key, latitude, longitude) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index b48a360da28..45b4dfa3a37 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -5,7 +5,7 @@ from homeassistant.components.air_quality import ( ATTR_PM_10, AirQualityEntity, ) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_NAME from .const import ( ATTR_API_ADVICE, @@ -35,13 +35,10 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] - unique_id = f"{latitude}-{longitude}" data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - async_add_entities([AirlyAirQuality(data, name, unique_id)], True) + async_add_entities([AirlyAirQuality(data, name, config_entry.unique_id)], True) def round_state(func): diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 31cfec7e7aa..84bad2d3719 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -6,19 +6,14 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS - - -@callback -def configured_instances(hass): - """Return a set of configured Airly instances.""" - return set( - entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) - ) +from .const import ( # pylint:disable=unused-import + DEFAULT_NAME, + DOMAIN, + NO_AIRLY_SENSORS, +) class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -38,8 +33,10 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): websession = async_get_clientsession(self.hass) if user_input is not None: - if user_input[CONF_NAME] in configured_instances(self.hass): - self._errors[CONF_NAME] = "name_exists" + await self.async_set_unique_id( + f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() api_key_valid = await self._test_api_key(websession, user_input["api_key"]) if not api_key_valid: self._errors["base"] = "auth" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index af0eac39cdc..ab83f711153 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -2,8 +2,6 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, - CONF_LATITUDE, - CONF_LONGITUDE, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -62,14 +60,12 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly sensor entities based on a config entry.""" name = config_entry.data[CONF_NAME] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: - unique_id = f"{latitude}-{longitude}-{sensor.lower()}" + unique_id = f"{config_entry.unique_id}-{sensor.lower()}" sensors.append(AirlySensor(data, name, sensor, unique_id)) async_add_entities(sensors, True) diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 116b6df83e6..d8047265415 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -14,9 +14,11 @@ } }, "error": { - "name_exists": "Name already exists.", "wrong_location": "No Airly measuring stations in this area.", "auth": "API key is not correct." + }, + "abort": { + "already_configured": "Airly integration for these coordinates is already configured." } } } diff --git a/homeassistant/components/alarm_control_panel/.translations/de.json b/homeassistant/components/alarm_control_panel/.translations/de.json index 3e94345138a..1787391c292 100644 --- a/homeassistant/components/alarm_control_panel/.translations/de.json +++ b/homeassistant/components/alarm_control_panel/.translations/de.json @@ -1,5 +1,12 @@ { "device_automation": { + "action_type": { + "arm_away": "Aktiviere {entity_name} Unterwegs", + "arm_home": "Aktiviere {entity_name} Zuhause", + "arm_night": "Aktiviere {entity_name} Nacht-Modus", + "disarm": "Deaktivere {entity_name}", + "trigger": "Ausl\u00f6ser {entity_name}" + }, "trigger_type": { "armed_away": "{entity_name} Unterwegs", "armed_home": "{entity_name} Zuhause", diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 5fb44a18a0b..67b0309e513 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -121,67 +121,49 @@ class AlarmControlPanel(Entity): """Send disarm command.""" raise NotImplementedError() - def async_alarm_disarm(self, code=None): - """Send disarm command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_disarm, code) + async def async_alarm_disarm(self, code=None): + """Send disarm command.""" + await self.hass.async_add_executor_job(self.alarm_disarm, code) def alarm_arm_home(self, code=None): """Send arm home command.""" raise NotImplementedError() - def async_alarm_arm_home(self, code=None): - """Send arm home command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_arm_home, code) + async def async_alarm_arm_home(self, code=None): + """Send arm home command.""" + await self.hass.async_add_executor_job(self.alarm_arm_home, code) def alarm_arm_away(self, code=None): """Send arm away command.""" raise NotImplementedError() - def async_alarm_arm_away(self, code=None): - """Send arm away command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_arm_away, code) + async def async_alarm_arm_away(self, code=None): + """Send arm away command.""" + await self.hass.async_add_executor_job(self.alarm_arm_away, code) def alarm_arm_night(self, code=None): """Send arm night command.""" raise NotImplementedError() - def async_alarm_arm_night(self, code=None): - """Send arm night command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_arm_night, code) + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + await self.hass.async_add_executor_job(self.alarm_arm_night, code) def alarm_trigger(self, code=None): """Send alarm trigger command.""" raise NotImplementedError() - def async_alarm_trigger(self, code=None): - """Send alarm trigger command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_trigger, code) + async def async_alarm_trigger(self, code=None): + """Send alarm trigger command.""" + await self.hass.async_add_executor_job(self.alarm_trigger, code) def alarm_arm_custom_bypass(self, code=None): """Send arm custom bypass command.""" raise NotImplementedError() - def async_alarm_arm_custom_bypass(self, code=None): - """Send arm custom bypass command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) + async def async_alarm_arm_custom_bypass(self, code=None): + """Send arm custom bypass command.""" + await self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) @property @abstractmethod diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 833156e98b2..a990de9bf98 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -118,11 +118,12 @@ def setup(hass, config): conf = config.get(DOMAIN) restart = False - device = conf.get(CONF_DEVICE) - display = conf.get(CONF_PANEL_DISPLAY) + device = conf[CONF_DEVICE] + display = conf[CONF_PANEL_DISPLAY] + auto_bypass = conf[CONF_AUTO_BYPASS] zones = conf.get(CONF_ZONES) - device_type = device.get(CONF_DEVICE_TYPE) + device_type = device[CONF_DEVICE_TYPE] host = DEFAULT_DEVICE_HOST port = DEFAULT_DEVICE_PORT path = DEFAULT_DEVICE_PATH @@ -204,7 +205,9 @@ def setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) - load_platform(hass, "alarm_control_panel", DOMAIN, conf, config) + load_platform( + hass, "alarm_control_panel", DOMAIN, {CONF_AUTO_BYPASS: auto_bypass}, config + ) if zones: load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 70f3e67e15b..e217bcb6cf9 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE +from . import CONF_AUTO_BYPASS, DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -35,13 +35,17 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up for AlarmDecoder alarm panels.""" - device = AlarmDecoderAlarmPanel(discovery_info["autobypass"]) - add_entities([device]) + if discovery_info is None: + return + + auto_bypass = discovery_info[CONF_AUTO_BYPASS] + entity = AlarmDecoderAlarmPanel(auto_bypass) + add_entities([entity]) def alarm_toggle_chime_handler(service): """Register toggle chime handler.""" code = service.data.get(ATTR_CODE) - device.alarm_toggle_chime(code) + entity.alarm_toggle_chime(code) hass.services.register( DOMAIN, @@ -53,7 +57,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def alarm_keypress_handler(service): """Register keypress handler.""" keypress = service.data[ATTR_KEYPRESS] - device.alarm_keypress(keypress) + entity.alarm_keypress(keypress) hass.services.register( DOMAIN, diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index fd0e79cef8a..f146f6f4a7e 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,7 +2,9 @@ "domain": "alarmdecoder", "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": ["alarmdecoder==1.13.9"], + "requirements": [ + "alarmdecoder==1.13.2" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index dd6b1272223..e5ff550df9a 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -115,7 +115,7 @@ class AlarmDotCom(alarm.AlarmControlPanel): await self._alarm.async_alarm_disarm() async def async_alarm_arm_home(self, code=None): - """Send arm hom command.""" + """Send arm home command.""" if self._validate_code(code): await self._alarm.async_alarm_arm_home() diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1dddc815d01..eb1474aed7e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,8 +1,20 @@ """Alexa capabilities.""" import logging -from homeassistant.components import cover, fan, image_processing, input_number, light +from homeassistant.components import ( + cover, + fan, + image_processing, + input_number, + light, + vacuum, +) from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player from homeassistant.const import ( @@ -31,7 +43,6 @@ from .const import ( API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, Inputs, ) from .errors import UnsupportedProperty @@ -503,6 +514,10 @@ class AlexaColorController(AlexaCapability): """Return what properties this entity supports.""" return [{"name": "color"}] + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + def properties_retrievable(self): """Return True if properties can be retrieved.""" return True @@ -548,6 +563,10 @@ class AlexaColorTemperatureController(AlexaCapability): """Return what properties this entity supports.""" return [{"name": "colorTemperatureInKelvin"}] + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + def properties_retrievable(self): """Return True if properties can be retrieved.""" return True @@ -590,6 +609,10 @@ class AlexaPercentageController(AlexaCapability): """Return what properties this entity supports.""" return [{"name": "percentage"}] + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + def properties_retrievable(self): """Return True if properties can be retrieved.""" return True @@ -1064,10 +1087,23 @@ class AlexaSecurityPanelController(AlexaCapability): def configuration(self): """Return configuration object with supported authorization types.""" code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) + supported = self.entity.attributes[ATTR_SUPPORTED_FEATURES] + configuration = {} + + supported_arm_states = [{"value": "DISARMED"}] + if supported & SUPPORT_ALARM_ARM_AWAY: + supported_arm_states.append({"value": "ARMED_AWAY"}) + if supported & SUPPORT_ALARM_ARM_HOME: + supported_arm_states.append({"value": "ARMED_STAY"}) + if supported & SUPPORT_ALARM_ARM_NIGHT: + supported_arm_states.append({"value": "ARMED_NIGHT"}) + + configuration["supportedArmStates"] = supported_arm_states if code_format == FORMAT_NUMBER: - return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} - return None + configuration["supportedAuthorizationTypes"] = [{"type": "FOUR_DIGIT_PIN"}] + + return configuration class AlexaModeController(AlexaCapability): @@ -1185,7 +1221,10 @@ class AlexaModeController(AlexaCapability): f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", [AlexaGlobalCatalog.VALUE_CLOSE], ) - self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"]) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.custom", + ["Custom", AlexaGlobalCatalog.SETTING_PRESET], + ) return self._resource.serialize_capability_resources() return None @@ -1287,10 +1326,20 @@ class AlexaRangeController(AlexaCapability): if name != "rangeValue": raise UnsupportedProperty(name) + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST) speed = self.entity.attributes.get(fan.ATTR_SPEED) - return RANGE_FAN_MAP.get(speed, 0) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1304,6 +1353,16 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": return float(self.entity.state) + # Vacuum Fan Speed + if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) + speed = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED) + if speed_list is not None and speed is not None: + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index + return None def configuration(self): @@ -1318,24 +1377,26 @@ class AlexaRangeController(AlexaCapability): # Fan Speed Resources if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] + max_value = len(speed_list) - 1 self._resource = AlexaPresetResource( labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], - min_value=1, - max_value=3, + min_value=0, + max_value=max_value, precision=1, ) - self._resource.add_preset( - value=1, - labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM], - ) - self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM]) - self._resource.add_preset( - value=3, - labels=[ - AlexaGlobalCatalog.VALUE_HIGH, - AlexaGlobalCatalog.VALUE_MAXIMUM, - ], - ) + for index, speed in enumerate(speed_list): + labels = [] + if isinstance(speed, str): + labels.append(speed.replace("_", " ")) + if index == 1: + labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) + if index == max_value: + labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) + + if len(labels) > 0: + self._resource.add_preset(value=index, labels=labels) + return self._resource.serialize_capability_resources() # Cover Position Resources @@ -1368,7 +1429,7 @@ class AlexaRangeController(AlexaCapability): unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT) self._resource = AlexaPresetResource( - ["Value"], + ["Value", AlexaGlobalCatalog.SETTING_PRESET], min_value=min_value, max_value=max_value, precision=precision, @@ -1382,6 +1443,26 @@ class AlexaRangeController(AlexaCapability): ) return self._resource.serialize_capability_resources() + # Vacuum Fan Speed Resources + if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + max_value = len(speed_list) - 1 + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=0, + max_value=max_value, + precision=1, + ) + for index, speed in enumerate(speed_list): + labels = [speed.replace("_", " ")] + if index == 1: + labels.append(AlexaGlobalCatalog.VALUE_MINIMUM) + if index == max_value: + labels.append(AlexaGlobalCatalog.VALUE_MAXIMUM) + self._resource.add_preset(value=index, labels=labels) + + return self._resource.serialize_capability_resources() + return None def semantics(self): @@ -1701,3 +1782,29 @@ class AlexaEqualizerController(AlexaCapability): configurations = {"modes": {"supported": supported_sound_modes}} return configurations + + +class AlexaTimeHoldController(AlexaCapability): + """Implements Alexa.TimeHoldController. + + https://developer.amazon.com/docs/device-apis/alexa-timeholdcontroller.html + """ + + supported_locales = {"en-US"} + + def __init__(self, entity, allow_remote_resume=False): + """Initialize the entity.""" + super().__init__(entity) + self._allow_remote_resume = allow_remote_resume + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.TimeHoldController" + + def configuration(self): + """Return configuration object. + + Set allowRemoteResume to True if Alexa can restart the operation on the device. + When false, Alexa does not send the Resume directive. + """ + return {"allowRemoteResume": self._allow_remote_resume} diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index f5f19bbf955..e45bcf824bc 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -84,20 +84,6 @@ PERCENTAGE_FAN_MAP = { fan.SPEED_HIGH: 100, } -RANGE_FAN_MAP = { - fan.SPEED_OFF: 0, - fan.SPEED_LOW: 1, - fan.SPEED_MEDIUM: 2, - fan.SPEED_HIGH: 3, -} - -SPEED_FAN_MAP = { - 0: fan.SPEED_OFF, - 1: fan.SPEED_LOW, - 2: fan.SPEED_MEDIUM, - 3: fan.SPEED_HIGH, -} - class Cause: """Possible causes for property changes. diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 084231f0090..254cec44553 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -19,6 +19,8 @@ from homeassistant.components import ( script, sensor, switch, + timer, + vacuum, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -61,6 +63,7 @@ from .capabilities import ( AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaTimeHoldController, AlexaToggleController, ) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES @@ -255,6 +258,9 @@ class AlexaEntity: def serialize_properties(self): """Yield each supported property in API format.""" for interface in self.interfaces(): + if not interface.properties_proactively_reported(): + continue + for prop in interface.serialize_properties(): yield prop @@ -394,6 +400,7 @@ class CoverCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" + yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: yield AlexaRangeController( @@ -703,3 +710,48 @@ class InputNumberCapabilities(AlexaEntity): ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(timer.DOMAIN) +class TimerCapabilities(AlexaEntity): + """Class to represent Timer capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaTimeHoldController(self.entity, allow_remote_resume=True) + yield Alexa(self.entity) + + +@ENTITY_ADAPTERS.register(vacuum.DOMAIN) +class VacuumCapabilities(AlexaEntity): + """Class to represent vacuum capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if (supported & vacuum.SUPPORT_TURN_ON) and ( + supported & vacuum.SUPPORT_TURN_OFF + ): + yield AlexaPowerController(self.entity) + + if supported & vacuum.SUPPORT_FAN_SPEED: + yield AlexaRangeController( + self.entity, instance=f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}" + ) + + if supported & vacuum.SUPPORT_PAUSE: + support_resume = bool(supported & vacuum.SUPPORT_START) + yield AlexaTimeHoldController( + self.entity, allow_remote_resume=support_resume + ) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 1cb8980b0b1..03c5acd42fa 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -10,6 +10,8 @@ from homeassistant.components import ( input_number, light, media_player, + timer, + vacuum, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -50,8 +52,6 @@ from .const import ( API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, - SPEED_FAN_MAP, Cause, Inputs, ) @@ -119,7 +119,9 @@ async def async_api_turn_on(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_ON - if domain == media_player.DOMAIN: + if domain == cover.DOMAIN: + service = cover.SERVICE_OPEN_COVER + elif domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -145,7 +147,9 @@ async def async_api_turn_off(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_OFF - if domain == media_player.DOMAIN: + if entity.domain == cover.DOMAIN: + service = cover.SERVICE_CLOSE_COVER + elif domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -908,8 +912,11 @@ async def async_api_arm(hass, config, directive, context): entity.domain, service, data, blocking=False, context=context ) + # return 0 until alarm integration supports an exit delay + payload = {"exitDelayInSeconds": 0} + response = directive.response( - name="Arm.Response", namespace="Alexa.SecurityPanelController" + name="Arm.Response", namespace="Alexa.SecurityPanelController", payload=payload ) response.add_context_property( @@ -928,6 +935,12 @@ async def async_api_disarm(hass, config, directive, context): """Process a Security Panel Disarm request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} + response = directive.response() + + # Per Alexa Documentation: If you receive a Disarm directive, and the system is already disarmed, + # respond with a success response, not an error response. + if entity.state == STATE_ALARM_DISARMED: + return response payload = directive.payload if "authorization" in payload: @@ -941,7 +954,6 @@ async def async_api_disarm(hass, config, directive, context): msg = "Invalid Code" raise AlexaSecurityPanelUnauthorizedError(msg) - response = directive.response() response.add_context_property( { "name": "armState", @@ -1095,8 +1107,10 @@ async def async_api_set_range(hass, config, directive, context): # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_value = int(range_value) service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(int(range_value)) + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + speed = next((v for i, v in enumerate(speed_list) if i == range_value), None) if not speed: msg = "Entity does not support value" @@ -1127,7 +1141,7 @@ async def async_api_set_range(hass, config, directive, context): service = cover.SERVICE_OPEN_COVER_TILT else: service = cover.SERVICE_SET_COVER_TILT_POSITION - data[cover.ATTR_POSITION] = range_value + data[cover.ATTR_TILT_POSITION] = range_value # Input Number Value elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": @@ -1137,6 +1151,20 @@ async def async_api_set_range(hass, config, directive, context): max_value = float(entity.attributes[input_number.ATTR_MAX]) data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + # Vacuum Fan Speed + elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + service = vacuum.SERVICE_SET_FAN_SPEED + speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + speed = next( + (v for i, v in enumerate(speed_list) if i == int(range_value)), None + ) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + data[vacuum.ATTR_FAN_SPEED] = speed + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) @@ -1167,15 +1195,23 @@ async def async_api_adjust_range(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} range_delta = directive.payload["rangeValueDelta"] + range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) response_value = 0 # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED - current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) - speed = SPEED_FAN_MAP.get( - min(3, max(0, range_delta + current_range)), fan.SPEED_OFF + speed_list = entity.attributes[fan.ATTR_SPEED_LIST] + current_speed = entity.attributes[fan.ATTR_SPEED] + current_speed_index = next( + (i for i, v in enumerate(speed_list) if v == current_speed), 0 + ) + new_speed_index = min( + len(speed_list) - 1, max(0, current_speed_index + range_delta) + ) + speed = next( + (v for i, v in enumerate(speed_list) if i == new_speed_index), None ) if speed == fan.SPEED_OFF: @@ -1185,21 +1221,37 @@ async def async_api_adjust_range(hass, config, directive, context): # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - range_delta = int(range_delta) + range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) service = SERVICE_SET_COVER_POSITION current = entity.attributes.get(cover.ATTR_POSITION) - data[cover.ATTR_POSITION] = response_value = min( - 100, max(0, range_delta + current) - ) + if not current: + msg = "Unable to determine {} current position".format(entity.entity_id) + raise AlexaInvalidValueError(msg) + position = response_value = min(100, max(0, range_delta + current)) + if position == 100: + service = cover.SERVICE_OPEN_COVER + elif position == 0: + service = cover.SERVICE_CLOSE_COVER + else: + data[cover.ATTR_POSITION] = position # Cover Tilt elif instance == f"{cover.DOMAIN}.tilt": - range_delta = int(range_delta) + range_delta = int(range_delta * 20) if range_delta_default else int(range_delta) service = SERVICE_SET_COVER_TILT_POSITION current = entity.attributes.get(cover.ATTR_TILT_POSITION) - data[cover.ATTR_TILT_POSITION] = response_value = min( - 100, max(0, range_delta + current) - ) + if not current: + msg = "Unable to determine {} current tilt position".format( + entity.entity_id + ) + raise AlexaInvalidValueError(msg) + tilt_position = response_value = min(100, max(0, range_delta + current)) + if tilt_position == 100: + service = cover.SERVICE_OPEN_COVER_TILT + elif tilt_position == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + else: + data[cover.ATTR_TILT_POSITION] = tilt_position # Input Number Value elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": @@ -1212,6 +1264,24 @@ async def async_api_adjust_range(hass, config, directive, context): max_value, max(min_value, range_delta + current) ) + # Vacuum Fan Speed + elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": + range_delta = int(range_delta) + service = vacuum.SERVICE_SET_FAN_SPEED + speed_list = entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] + current_speed = entity.attributes[vacuum.ATTR_FAN_SPEED] + current_speed_index = next( + (i for i, v in enumerate(speed_list) if v == current_speed), 0 + ) + new_speed_index = min( + len(speed_list) - 1, max(0, current_speed_index + range_delta) + ) + speed = next( + (v for i, v in enumerate(speed_list) if i == new_speed_index), None + ) + + data[vacuum.ATTR_FAN_SPEED] = response_value = speed + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) @@ -1396,3 +1466,49 @@ async def async_api_bands_directive(hass, config, directive, context): # Currently bands directives are not supported. msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) + + +@HANDLERS.register(("Alexa.TimeHoldController", "Hold")) +async def async_api_hold(hass, config, directive, context): + """Process a TimeHoldController Hold request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == timer.DOMAIN: + service = timer.SERVICE_PAUSE + + elif entity.domain == vacuum.DOMAIN: + service = vacuum.SERVICE_START_PAUSE + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.TimeHoldController", "Resume")) +async def async_api_resume(hass, config, directive, context): + """Process a TimeHoldController Resume request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == timer.DOMAIN: + service = timer.SERVICE_START + + elif entity.domain == vacuum.DOMAIN: + service = vacuum.SERVICE_START_PAUSE + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json index c626e2795ea..6f7df114774 100644 --- a/homeassistant/components/almond/.translations/ca.json +++ b/homeassistant/components/almond/.translations/ca.json @@ -6,6 +6,9 @@ "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond." }, "step": { + "hassio_confirm": { + "title": "Almond (complement de Hass.io)" + }, "pick_implementation": { "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" } diff --git a/homeassistant/components/almond/.translations/cs.json b/homeassistant/components/almond/.translations/cs.json new file mode 100644 index 00000000000..f103fcc2727 --- /dev/null +++ b/homeassistant/components/almond/.translations/cs.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed hass.io {addon}?", + "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/da.json b/homeassistant/components/almond/.translations/da.json index 93158cee94f..a752b791988 100644 --- a/homeassistant/components/almond/.translations/da.json +++ b/homeassistant/components/almond/.translations/da.json @@ -6,6 +6,10 @@ "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." }, "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Hass.io-tilf\u00f8jelsen: {addon}?", + "title": "Almond via Hass.io-tilf\u00f8jelse" + }, "pick_implementation": { "title": "V\u00e6lg godkendelsesmetode" } diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json index 1495cabf9c9..b4e5f168f7c 100644 --- a/homeassistant/components/almond/.translations/de.json +++ b/homeassistant/components/almond/.translations/de.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", + "already_setup": "Du kannst nur ein Almond-Konto konfigurieren.", "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", - "missing_configuration": "Bitte \u00fcberpr\u00fcfen Sie die Dokumentation zur Einrichtung von Almond." + "missing_configuration": "Bitte \u00fcberpr\u00fcfe die Dokumentation zur Einrichtung von Almond." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index 3b7b5b9aa63..96638ef08fb 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -6,6 +6,10 @@ "missing_configuration": "Please check the documentation on how to set up Almond." }, "step": { + "hassio_confirm": { + "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?", + "title": "Almond via Hass.io add-on" + }, "pick_implementation": { "title": "Pick Authentication Method" } diff --git a/homeassistant/components/almond/.translations/es.json b/homeassistant/components/almond/.translations/es.json index 26eacb834b0..41e1fad4126 100644 --- a/homeassistant/components/almond/.translations/es.json +++ b/homeassistant/components/almond/.translations/es.json @@ -6,6 +6,10 @@ "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." }, "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Hass.io: {addon} ?", + "title": "Almond a trav\u00e9s del complemento Hass.io" + }, "pick_implementation": { "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" } diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index 9ae881d332c..30a4cbec6bd 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -6,6 +6,10 @@ "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, "step": { + "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour se connecter \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", + "title": "Almonf via le module compl\u00e9mentaire Hass.io" + }, "pick_implementation": { "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index 9d529e5e5c8..dd722907c6a 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -6,6 +6,10 @@ "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant a connettersi ad Almond tramite il componente aggiuntivo Hass.io: {addon} ?", + "title": "Almond tramite il componente aggiuntivo di Hass.io" + }, "pick_implementation": { "title": "Seleziona metodo di autenticazione" } diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 9f1e71163d6..ec484ffc0d4 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -6,6 +6,10 @@ "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "step": { + "hassio_confirm": { + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c Almond \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hass.io \uc560\ub4dc\uc628\uc758 Almond" + }, "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" } diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json index ca836267d46..b47ddca4a26 100644 --- a/homeassistant/components/almond/.translations/lb.json +++ b/homeassistant/components/almond/.translations/lb.json @@ -6,6 +6,10 @@ "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." }, "step": { + "hassio_confirm": { + "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der hass.io Erweiderung {addon} bereet gestallt g\u00ebtt?", + "title": "Almond via Hass.io Erweiderung" + }, "pick_implementation": { "title": "Wielt Authentifikatiouns Method aus" } diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 0272a120f21..47e32db0abe 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -6,6 +6,10 @@ "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." }, "step": { + "hassio_confirm": { + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io add-on: {addon}?", + "title": "Almond via Hass.io add-on" + }, "pick_implementation": { "title": "Velg autentiseringsmetode" } diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json index 56aa629e015..dc5717539a6 100644 --- a/homeassistant/components/almond/.translations/pl.json +++ b/homeassistant/components/almond/.translations/pl.json @@ -6,6 +6,10 @@ "missing_configuration": "Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105 konfiguracji Almond." }, "step": { + "hassio_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon}?", + "title": "Almond poprzez dodatek Hass.io" + }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" } diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json index 39dc41a3995..02162980894 100644 --- a/homeassistant/components/almond/.translations/ru.json +++ b/homeassistant/components/almond/.translations/ru.json @@ -6,6 +6,10 @@ "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." }, "step": { + "hassio_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "Almond (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } diff --git a/homeassistant/components/almond/.translations/sv.json b/homeassistant/components/almond/.translations/sv.json new file mode 100644 index 00000000000..61af3a04e47 --- /dev/null +++ b/homeassistant/components/almond/.translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "hassio_confirm": { + "title": "Almond via Hass.io-till\u00e4gget" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json index 4db6e0c936e..9522e350eea 100644 --- a/homeassistant/components/almond/.translations/zh-Hant.json +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -6,6 +6,10 @@ "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" }, "step": { + "hassio_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 Almond\uff1f", + "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 Almond" + }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 872367eb862..2ae4e632d6b 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -3,6 +3,10 @@ "step": { "pick_implementation": { "title": "Pick Authentication Method" + }, + "hassio_confirm": { + "title": "Almond via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to Almond provided by the Hass.io add-on: {addon}?" } }, "abort": { diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 1ed6dbd0db4..e4c1c8ccdac 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -30,11 +30,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Ambient PWS binary sensors based on the old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Ambient PWS binary sensors based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 0120799d6f2..6dc79cec326 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -20,11 +20,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Ambient PWS sensors based on existing config.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Ambient PWS sensors based on a config entry.""" ambient = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 63daeb04731..f7814939e3a 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -256,7 +256,7 @@ def setup(hass, config): async_dispatcher_send(hass, service_signal(call.service, entity_id), *args) for service, params in CAMERA_SERVICES.items(): - hass.services.async_register(DOMAIN, service, async_service_handler, params[0]) + hass.services.register(DOMAIN, service, async_service_handler, params[0]) return True diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index ee5b97b8579..8b2d72effa6 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": ["amcrest==1.5.3"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": ["@pnbruckner"] } diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index d81e7863503..5fea6c3f2e2 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell==0.1.1", - "androidtv==0.0.38", + "androidtv==0.0.39", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 63b27f17bb2..93666958919 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -26,6 +26,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( @@ -59,6 +60,7 @@ SUPPORT_ANDROIDTV = ( | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP ) @@ -80,6 +82,7 @@ CONF_ADBKEY = "adbkey" CONF_ADB_SERVER_IP = "adb_server_ip" CONF_ADB_SERVER_PORT = "adb_server_port" CONF_APPS = "apps" +CONF_EXCLUDE_UNNAMED_APPS = "exclude_unnamed_apps" CONF_GET_SOURCES = "get_sources" CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_ON_COMMAND = "turn_on_command" @@ -132,12 +135,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ADB_SERVER_IP): cv.string, vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port, vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean, - vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_APPS, default=dict()): vol.Schema( + {cv.string: vol.Any(cv.string, None)} + ), vol.Optional(CONF_TURN_ON_COMMAND): cv.string, vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( {cv.string: ha_state_detection_rules_validator(vol.Invalid)} ), + vol.Optional(CONF_EXCLUDE_UNNAMED_APPS, default=False): cv.boolean, } ) @@ -230,6 +236,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_GET_SOURCES], config.get(CONF_TURN_ON_COMMAND), config.get(CONF_TURN_OFF_COMMAND), + config[CONF_EXCLUDE_UNNAMED_APPS], ] if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: @@ -365,7 +372,14 @@ class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv @@ -373,7 +387,7 @@ class ADBDevice(MediaPlayerDevice): self._app_id_to_name = APPS.copy() self._app_id_to_name.update(apps) self._app_name_to_id = { - value: key for key, value in self._app_id_to_name.items() + value: key for key, value in self._app_id_to_name.items() if value } self._get_sources = get_sources self._keys = KEYS @@ -384,12 +398,15 @@ class ADBDevice(MediaPlayerDevice): self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command + self._exclude_unnamed_apps = exclude_unnamed_apps + # ADB exceptions to catch if not self.aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AttributeError, BrokenPipeError, + ConnectionResetError, TypeError, ValueError, InvalidChecksumError, @@ -558,11 +575,24 @@ class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + self, + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ): """Initialize the Android TV device.""" super().__init__( - aftv, name, apps, get_sources, turn_on_command, turn_off_command + aftv, + name, + apps, + get_sources, + turn_on_command, + turn_off_command, + exclude_unnamed_apps, ) self._is_volume_muted = None @@ -600,9 +630,13 @@ class AndroidTVDevice(ADBDevice): self._available = False if running_apps: - self._sources = [ - self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps ] + self._sources = [source for source in sources if source] else: self._sources = None @@ -631,6 +665,11 @@ class AndroidTVDevice(ADBDevice): """Mute the volume.""" self.aftv.mute_volume() + @adb_decorator() + def set_volume_level(self, volume): + """Set the volume level.""" + self.aftv.set_volume_level(volume) + @adb_decorator() def volume_down(self): """Send volume down command.""" @@ -670,9 +709,13 @@ class FireTVDevice(ADBDevice): self._available = False if running_apps: - self._sources = [ - self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + sources = [ + self._app_id_to_name.get( + app_id, app_id if not self._exclude_unnamed_apps else None + ) + for app_id in running_apps ] + self._sources = [source for source in sources if source] else: self._sources = None diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index f7b385d80a2..f4efd0de355 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,9 +56,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.info("Provisioning Anthem AVR device at %s:%d", host, port) + @callback def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" - _LOGGER.info("Received update callback from AVR: %s", message) + _LOGGER.debug("Received update callback from AVR: %s", message) hass.async_create_task(device.async_update_ha_state()) avr = await anthemav.Connection.create( diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index fc2f01d418d..b9638d44d2b 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -411,6 +411,7 @@ async def async_services_json(hass): return [{"domain": key, "services": value} for key, value in descriptions.items()] +@ha.callback def async_events_json(hass): """Generate event data to JSONify.""" return [ diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index c816be52259..c34a46a8b82 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -229,62 +229,42 @@ class AppleTvDevice(MediaPlayerDevice): self._playing = None self._power.set_power_on(False) - def async_media_play_pause(self): - """Pause media on media player. + async def async_media_play_pause(self): + """Pause media on media player.""" + if not self._playing: + return + state = self.state + if state == STATE_PAUSED: + await self.atv.remote_control.play() + elif state == STATE_PLAYING: + await self.atv.remote_control.pause() - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_play(self): + """Play media.""" if self._playing: - state = self.state - if state == STATE_PAUSED: - return self.atv.remote_control.play() - if state == STATE_PLAYING: - return self.atv.remote_control.pause() + await self.atv.remote_control.play() - def async_media_play(self): - """Play media. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_stop(self): + """Stop the media player.""" if self._playing: - return self.atv.remote_control.play() + await self.atv.remote_control.stop() - def async_media_stop(self): - """Stop the media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_pause(self): + """Pause the media player.""" if self._playing: - return self.atv.remote_control.stop() + await self.atv.remote_control.pause() - def async_media_pause(self): - """Pause the media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_next_track(self): + """Send next track command.""" if self._playing: - return self.atv.remote_control.pause() + await self.atv.remote_control.next() - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_previous_track(self): + """Send previous track command.""" if self._playing: - return self.atv.remote_control.next() + await self.atv.remote_control.previous() - def async_media_previous_track(self): - """Send previous track command. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_seek(self, position): + """Send seek command.""" if self._playing: - return self.atv.remote_control.previous() - - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ - if self._playing: - return self.atv.remote_control.set_position(position) + await self.atv.remote_control.set_position(position) diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 1229b756e72..dd784cc449d 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -61,17 +61,10 @@ class AppleTVRemote(remote.RemoteDevice): """ self._power.set_power_on(False) - def async_send_command(self, command, **kwargs): - """Send a command to one device. + async def async_send_command(self, command, **kwargs): + """Send a command to one device.""" + for single_command in command: + if not hasattr(self._atv.remote_control, single_command): + continue - This method must be run in the event loop and returns a coroutine. - """ - # Send commands in specified order but schedule only one coroutine - async def _send_commands(): - for single_command in command: - if not hasattr(self._atv.remote_control, single_command): - continue - - await getattr(self._atv.remote_control, single_command)() - - return _send_commands() + await getattr(self._atv.remote_control, single_command)() diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 6e8a0567d06..1f41d5a24e2 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -1,8 +1,8 @@ { "domain": "apprise", "name": "Apprise", - "documentation": "https://www.home-assistant.io/components/apprise", - "requirements": ["apprise==0.8.2"], + "documentation": "https://www.home-assistant.io/integrations/apprise", + "requirements": ["apprise==0.8.3"], "dependencies": [], "codeowners": ["@caronc"] } diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 3cd6fe059b6..b3863eeb13f 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,4 +1,5 @@ """Support for the Asterisk Voicemail interface.""" +from functools import partial import logging from asterisk_mbox import ServerError @@ -55,7 +56,9 @@ class AsteriskMailbox(Mailbox): client = self.hass.data[ASTERISK_DOMAIN].client try: - return client.mp3(msgid, sync=True) + return await self.hass.async_add_executor_job( + partial(client.mp3, msgid, sync=True) + ) except ServerError as err: raise StreamError(err) @@ -63,9 +66,9 @@ class AsteriskMailbox(Mailbox): """Return a list of the current messages.""" return self.hass.data[ASTERISK_DOMAIN].messages - def async_delete(self, msgid): + async def async_delete(self, msgid): """Delete the specified messages.""" client = self.hass.data[ASTERISK_DOMAIN].client _LOGGER.info("Deleting: %s", msgid) - client.delete(msgid) + await self.hass.async_add_executor_job(client.delete, msgid) return True diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 454c3ad2405..6963d836685 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,6 +1,7 @@ """Support for aurora forecast data sensor.""" from datetime import timedelta import logging +from math import floor from aiohttp.hdrs import USER_AGENT import requests @@ -99,8 +100,6 @@ class AuroraData: """Initialize the data object.""" self.latitude = latitude self.longitude = longitude - self.number_of_latitude_intervals = 513 - self.number_of_longitude_intervals = 1024 self.headers = {USER_AGENT: HA_USER_AGENT} self.threshold = int(threshold) self.is_visible = None @@ -126,18 +125,22 @@ class AuroraData: def get_aurora_forecast(self): """Get forecast data and parse for given long/lat.""" raw_data = requests.get(URL, headers=self.headers, timeout=5).text + # We discard comment rows (#) + # We split the raw text by line (\n) + # For each line we trim leading spaces and split by spaces forecast_table = [ - row.strip(" ").split(" ") + row.strip().split() for row in raw_data.split("\n") if not row.startswith("#") ] # Convert lat and long for data points in table - converted_latitude = round( - (self.latitude / 180) * self.number_of_latitude_intervals - ) - converted_longitude = round( - (self.longitude / 360) * self.number_of_longitude_intervals + # Assumes self.latitude belongs to [-90;90[ (South to North) + # Assumes self.longitude belongs to [-180;180[ (West to East) + # No assumptions made regarding the number of rows and columns + converted_latitude = floor((self.latitude + 90) * len(forecast_table) / 180) + converted_longitude = floor( + (self.longitude + 180) * len(forecast_table[converted_latitude]) / 360 ) return forecast_table[converted_latitude][converted_longitude] diff --git a/homeassistant/components/auth/.translations/zh-Hant.json b/homeassistant/components/auth/.translations/zh-Hant.json index b7a26f5079c..96e7f21ac99 100644 --- a/homeassistant/components/auth/.translations/zh-Hant.json +++ b/homeassistant/components/auth/.translations/zh-Hant.json @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u5169\u6b65\u9a5f\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", - "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u5169\u6b65\u9a5f\u9a57\u8b49" + "description": "\u6b32\u555f\u7528\u4e00\u6b21\u6027\u4e14\u5177\u6642\u6548\u6027\u7684\u5bc6\u78bc\u4e4b\u96d9\u91cd\u9a57\u8b49\u529f\u80fd\uff0c\u8acb\u4f7f\u7528\u60a8\u7684\u9a57\u8b49 App \u6383\u7784\u4e0b\u65b9\u7684 QR code \u3002\u5018\u82e5\u60a8\u5c1a\u672a\u5b89\u88dd\u4efb\u4f55 App\uff0c\u63a8\u85a6\u60a8\u4f7f\u7528 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u6216 [Authy](https://authy.com/)\u3002\n\n{qr_code}\n\n\u65bc\u641c\u5c0b\u4e4b\u5f8c\uff0c\u8f38\u5165 App \u4e2d\u7684\u516d\u4f4d\u6578\u5b57\u9032\u884c\u8a2d\u5b9a\u9a57\u8b49\u3002\u5047\u5982\u641c\u5c0b\u51fa\u73fe\u554f\u984c\uff0c\u8acb\u624b\u52d5\u8f38\u5165\u4ee5\u4e0b\u9a57\u8b49\u78bc **`{code}`**\u3002", + "title": "\u4f7f\u7528 TOTP \u8a2d\u5b9a\u96d9\u91cd\u9a57\u8b49" } }, "title": "TOTP" diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6175646778f..528a314dd7b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,17 +1,19 @@ """Allow to set up simple automation rules via the config file.""" -import asyncio -from functools import partial import importlib import logging -from typing import Any, Awaitable, Callable +from typing import Any, Awaitable, Callable, List, Optional, Set import voluptuous as vol +from homeassistant.components import sun from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + CONF_DEVICE_ID, + CONF_ENTITY_ID, CONF_ID, CONF_PLATFORM, + CONF_ZONE, EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, SERVICE_RELOAD, @@ -20,11 +22,10 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.core import Context, CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -93,29 +94,23 @@ _TRIGGER_SCHEMA = vol.All( _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) -PLATFORM_SCHEMA = vol.Schema( - { - # str on purpose - CONF_ID: str, - CONF_ALIAS: cv.string, - vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(CONF_INITIAL_STATE): cv.boolean, - vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, - vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, - vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, - vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, - } +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.107"), + vol.Schema( + { + # str on purpose + CONF_ID: str, + CONF_ALIAS: cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_INITIAL_STATE): cv.boolean, + vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, + vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, + vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, + } + ), ) -TRIGGER_SERVICE_SCHEMA = make_entity_service_schema( - { - vol.Optional(ATTR_VARIABLES, default={}): dict, - vol.Optional(CONF_SKIP_CONDITION, default=True): bool, - } -) - -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - @bind_hass def is_on(hass, entity_id): @@ -127,48 +122,97 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) +@callback +def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all automations that reference the entity.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + results = [] + + for automation_entity in component.entities: + if entity_id in automation_entity.referenced_entities: + results.append(automation_entity.entity_id) + + return results + + +@callback +def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all entities in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_entities) + + +@callback +def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: + """Return all automations that reference the device.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + results = [] + + for automation_entity in component.entities: + if device_id in automation_entity.referenced_devices: + results.append(automation_entity.entity_id) + + return results + + +@callback +def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all devices in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + automation_entity = component.get_entity(entity_id) + + if automation_entity is None: + return [] + + return list(automation_entity.referenced_devices) + + async def async_setup(hass, config): """Set up the automation.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) await _async_process_config(hass, config, component) - async def trigger_service_handler(service_call): + async def trigger_service_handler(entity, service_call): """Handle automation triggers.""" - tasks = [] - for entity in await component.async_extract_from_service(service_call): - tasks.append( - entity.async_trigger( - service_call.data[ATTR_VARIABLES], - skip_condition=service_call.data[CONF_SKIP_CONDITION], - context=service_call.context, - ) - ) + await entity.async_trigger( + service_call.data[ATTR_VARIABLES], + skip_condition=service_call.data[CONF_SKIP_CONDITION], + context=service_call.context, + ) - if tasks: - await asyncio.wait(tasks) - - async def turn_onoff_service_handler(service_call): - """Handle automation turn on/off service calls.""" - tasks = [] - method = f"async_{service_call.service}" - for entity in await component.async_extract_from_service(service_call): - tasks.append(getattr(entity, method)()) - - if tasks: - await asyncio.wait(tasks) - - async def toggle_service_handler(service_call): - """Handle automation toggle service calls.""" - tasks = [] - for entity in await component.async_extract_from_service(service_call): - if entity.is_on: - tasks.append(entity.async_turn_off()) - else: - tasks.append(entity.async_turn_on()) - - if tasks: - await asyncio.wait(tasks) + component.async_register_entity_service( + SERVICE_TRIGGER, + { + vol.Optional(ATTR_VARIABLES, default={}): dict, + vol.Optional(CONF_SKIP_CONDITION, default=True): bool, + }, + trigger_service_handler, + ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") async def reload_service_handler(service_call): """Remove all automations and load new ones from config.""" @@ -177,33 +221,10 @@ async def async_setup(hass, config): return await _async_process_config(hass, conf, component) - hass.services.async_register( - DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA - ) - async_register_admin_service( - hass, - DOMAIN, - SERVICE_RELOAD, - reload_service_handler, - schema=RELOAD_SERVICE_SCHEMA, + hass, DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}), ) - hass.services.async_register( - DOMAIN, - SERVICE_TOGGLE, - toggle_service_handler, - schema=make_entity_service_schema({}), - ) - - for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): - hass.services.async_register( - DOMAIN, - service, - turn_onoff_service_handler, - schema=make_entity_service_schema({}), - ) - return True @@ -214,29 +235,36 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self, automation_id, name, - async_attach_triggers, + trigger_config, cond_func, - async_action, + action_script, hidden, initial_state, ): """Initialize an automation entity.""" self._id = automation_id self._name = name - self._async_attach_triggers = async_attach_triggers + self._trigger_config = trigger_config self._async_detach_triggers = None self._cond_func = cond_func - self._async_action = async_action + self.action_script = action_script self._last_triggered = None self._hidden = hidden self._initial_state = initial_state self._is_enabled = False + self._referenced_entities: Optional[Set[str]] = None + self._referenced_devices: Optional[Set[str]] = None @property def name(self): """Name of the automation.""" return self._name + @property + def unique_id(self): + """Return unique ID.""" + return self._id + @property def should_poll(self): """No polling needed for automation entities.""" @@ -257,6 +285,45 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """Return True if entity is on.""" return self._async_detach_triggers is not None or self._is_enabled + @property + def referenced_devices(self): + """Return a set of referenced devices.""" + if self._referenced_devices is not None: + return self._referenced_devices + + referenced = self.action_script.referenced_devices + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_devices(conf) + + for conf in self._trigger_config: + device = _trigger_extract_device(conf) + if device is not None: + referenced.add(device) + + self._referenced_devices = referenced + return referenced + + @property + def referenced_entities(self): + """Return a set of referenced entities.""" + if self._referenced_entities is not None: + return self._referenced_entities + + referenced = self.action_script.referenced_entities + + if self._cond_func is not None: + for conf in self._cond_func.config: + referenced |= condition.async_extract_entities(conf) + + for conf in self._trigger_config: + for entity_id in _trigger_extract_entities(conf): + referenced.add(entity_id) + + self._referenced_entities = referenced + return referenced + async def async_added_to_hass(self) -> None: """Startup with initial state or previous state.""" await super().async_added_to_hass() @@ -307,7 +374,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): This method is a coroutine. """ - if not skip_condition and not self._cond_func(variables): + if ( + not skip_condition + and self._cond_func is not None + and not self._cond_func(variables) + ): return # Create a new context referring to the old context. @@ -320,7 +391,16 @@ class AutomationEntity(ToggleEntity, RestoreEntity): {ATTR_NAME: self._name, ATTR_ENTITY_ID: self.entity_id}, context=trigger_context, ) - await self._async_action(self.entity_id, variables, trigger_context) + + _LOGGER.info("Executing %s", self._name) + + try: + await self.action_script.async_run(variables, trigger_context) + except Exception as err: # pylint: disable=broad-except + self.action_script.async_log_exception( + _LOGGER, f"Error while executing automation {self.entity_id}", err + ) + self._last_triggered = utcnow() await self.async_update_ha_state() @@ -341,9 +421,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): # HomeAssistant is starting up if self.hass.state != CoreState.not_running: - self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger - ) + self._async_detach_triggers = await self._async_attach_triggers() self.async_write_ha_state() return @@ -353,9 +431,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): if not self._is_enabled or self._async_detach_triggers is not None: return - self._async_detach_triggers = await self._async_attach_triggers( - self.async_trigger - ) + self._async_detach_triggers = await self._async_attach_triggers() self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, async_enable_automation @@ -375,6 +451,38 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_write_ha_state() + async def _async_attach_triggers(self): + """Set up the triggers.""" + removes = [] + info = {"name": self._name} + + for conf in self._trigger_config: + platform = importlib.import_module( + ".{}".format(conf[CONF_PLATFORM]), __name__ + ) + + remove = await platform.async_attach_trigger( + self.hass, conf, self.async_trigger, info + ) + + if not remove: + _LOGGER.error("Error setting up trigger %s", self._name) + continue + + _LOGGER.info("Initialized trigger %s", self._name) + removes.append(remove) + + if not removes: + return None + + @callback + def remove_triggers(): + """Remove attached triggers.""" + for remove in removes: + remove() + + return remove_triggers + @property def device_state_attributes(self): """Return automation attributes.""" @@ -401,7 +509,7 @@ async def _async_process_config(hass, config, component): hidden = config_block[CONF_HIDE_ENTITY] initial_state = config_block.get(CONF_INITIAL_STATE) - action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) + action_script = script.Script(hass, config_block.get(CONF_ACTION, {}), name) if CONF_CONDITION in config_block: cond_func = await _async_process_if(hass, config, config_block) @@ -409,24 +517,14 @@ async def _async_process_config(hass, config, component): if cond_func is None: continue else: + cond_func = None - def cond_func(variables): - """Condition will always pass.""" - return True - - async_attach_triggers = partial( - _async_process_trigger, - hass, - config, - config_block.get(CONF_TRIGGER, []), - name, - ) entity = AutomationEntity( automation_id, name, - async_attach_triggers, + config_block[CONF_TRIGGER], cond_func, - action, + action_script, hidden, initial_state, ) @@ -437,27 +535,9 @@ async def _async_process_config(hass, config, component): await component.async_add_entities(entities) -def _async_get_action(hass, config, name): - """Return an action based on a configuration.""" - script_obj = script.Script(hass, config, name) - - async def action(entity_id, variables, context): - """Execute an action.""" - _LOGGER.info("Executing %s", name) - - try: - await script_obj.async_run(variables, context) - except Exception as err: # pylint: disable=broad-except - script_obj.async_log_exception( - _LOGGER, f"Error while executing automation {entity_id}", err - ) - - return action - - async def _async_process_if(hass, config, p_config): """Process if checks.""" - if_configs = p_config.get(CONF_CONDITION) + if_configs = p_config[CONF_CONDITION] checks = [] for if_config in if_configs: @@ -471,35 +551,33 @@ async def _async_process_if(hass, config, p_config): """AND all conditions.""" return all(check(hass, variables) for check in checks) + if_action.config = if_configs + return if_action -async def _async_process_trigger(hass, config, trigger_configs, name, action): - """Set up the triggers. - - This method is a coroutine. - """ - removes = [] - info = {"name": name} - - for conf in trigger_configs: - platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - - remove = await platform.async_attach_trigger(hass, conf, action, info) - - if not remove: - _LOGGER.error("Error setting up trigger %s", name) - continue - - _LOGGER.info("Initialized trigger %s", name) - removes.append(remove) - - if not removes: +@callback +def _trigger_extract_device(trigger_conf: dict) -> Optional[str]: + """Extract devices from a trigger config.""" + if trigger_conf[CONF_PLATFORM] != "device": return None - def remove_triggers(): - """Remove attached triggers.""" - for remove in removes: - remove() + return trigger_conf[CONF_DEVICE_ID] - return remove_triggers + +@callback +def _trigger_extract_entities(trigger_conf: dict) -> List[str]: + """Extract entities from a trigger config.""" + if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): + return trigger_conf[CONF_ENTITY_ID] + + if trigger_conf[CONF_PLATFORM] == "zone": + return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "geo_location": + return [trigger_conf[CONF_ZONE]] + + if trigger_conf[CONF_PLATFORM] == "sun": + return [sun.ENTITY_ID] + + return [] diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 466fc941a9a..12ffa29b962 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -92,6 +92,7 @@ async def async_attach_trigger(hass, config, action, automation_info): hass.data["litejet_system"].on_switch_pressed(number, pressed) hass.data["litejet_system"].on_switch_released(number, released) + @callback def async_remove(): """Remove all subscriptions used for this trigger.""" return diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index 7b706eb1bfa..3f9c0043a3e 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -2,7 +2,7 @@ "domain": "aws", "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", - "requirements": ["aiobotocore==0.10.4"], + "requirements": ["aiobotocore==0.11.1"], "dependencies": [], "codeowners": ["@awarecan", "@robbiet480"] } diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 608e12d020a..07cfbd46504 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "bad_config_file": "Mauvaises donn\u00e9es du fichier de configuration", "link_local_address": "Les adresses locales ne sont pas prises en charge", - "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis" + "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis", + "updated_configuration": "Mise \u00e0 jour de la configuration du dispositif avec la nouvelle adresse de l'h\u00f4te" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json index c69f5a07a41..74d295f3055 100644 --- a/homeassistant/components/binary_sensor/.translations/it.json +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -59,11 +59,11 @@ "moving": "{entity_name} ha iniziato a muoversi", "no_gas": "{entity_name} ha smesso la rilevazione di gas", "no_light": "{entity_name} smesso il rilevamento di luce", - "no_motion": "{nome_entit\u00e0} ha smesso di rilevare il movimento", - "no_problem": "{nome_entit\u00e0} ha smesso di rilevare un problema", + "no_motion": "{entity_name} ha smesso di rilevare il movimento", + "no_problem": "{entity_name} ha smesso di rilevare un problema", "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", - "no_sound": "{nome_entit\u00e0} ha smesso di rilevare il suono", - "no_vibration": "{nome_entit\u00e0} ha smesso di rilevare le vibrazioni", + "no_sound": "{entity_name} ha smesso di rilevare il suono", + "no_vibration": "{entity_name} ha smesso di rilevare le vibrazioni", "not_bat_low": "{entity_name} batteria normale", "not_cold": "{entity_name} non \u00e8 diventato freddo", "not_connected": "{entity_name} \u00e8 disconnesso", diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index aa9a9d25e72..63f84b657c1 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -232,6 +232,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 3f9b5cd4597..dd7c02b82ad 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -10,6 +10,7 @@ import socket import voluptuous as vol from homeassistant.const import CONF_HOST +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -64,67 +65,67 @@ SERVICE_SEND_SCHEMA = vol.Schema( SERVICE_LEARN_SCHEMA = vol.Schema({vol.Required(CONF_HOST): cv.string}) +@callback def async_setup_service(hass, host, device): """Register a device for given host for use in services.""" hass.data.setdefault(DOMAIN, {})[host] = device - if not hass.services.has_service(DOMAIN, SERVICE_LEARN): + if hass.services.has_service(DOMAIN, SERVICE_LEARN): + return - async def _learn_command(call): - """Learn a packet from remote.""" - device = hass.data[DOMAIN][call.data[CONF_HOST]] + async def _learn_command(call): + """Learn a packet from remote.""" + device = hass.data[DOMAIN][call.data[CONF_HOST]] - try: - auth = await hass.async_add_executor_job(device.auth) - except socket.timeout: - _LOGGER.error("Failed to connect to device, timeout") + try: + auth = await hass.async_add_executor_job(device.auth) + except socket.timeout: + _LOGGER.error("Failed to connect to device, timeout") + return + if not auth: + _LOGGER.error("Failed to connect to device") + return + + await hass.async_add_executor_job(device.enter_learning) + + _LOGGER.info("Press the key you want Home Assistant to learn") + start_time = utcnow() + while (utcnow() - start_time) < timedelta(seconds=20): + packet = await hass.async_add_executor_job(device.check_data) + if packet: + data = b64encode(packet).decode("utf8") + log_msg = f"Received packet is: {data}" + _LOGGER.info(log_msg) + hass.components.persistent_notification.async_create( + log_msg, title="Broadlink switch" + ) return - if not auth: - _LOGGER.error("Failed to connect to device") - return - - await hass.async_add_executor_job(device.enter_learning) - - _LOGGER.info("Press the key you want Home Assistant to learn") - start_time = utcnow() - while (utcnow() - start_time) < timedelta(seconds=20): - packet = await hass.async_add_executor_job(device.check_data) - if packet: - data = b64encode(packet).decode("utf8") - log_msg = f"Received packet is: {data}" - _LOGGER.info(log_msg) - hass.components.persistent_notification.async_create( - log_msg, title="Broadlink switch" - ) - return - await asyncio.sleep(1) - _LOGGER.error("No signal was received") - hass.components.persistent_notification.async_create( - "No signal was received", title="Broadlink switch" - ) - - hass.services.async_register( - DOMAIN, SERVICE_LEARN, _learn_command, schema=SERVICE_LEARN_SCHEMA + await asyncio.sleep(1) + _LOGGER.error("No signal was received") + hass.components.persistent_notification.async_create( + "No signal was received", title="Broadlink switch" ) - if not hass.services.has_service(DOMAIN, SERVICE_SEND): + hass.services.async_register( + DOMAIN, SERVICE_LEARN, _learn_command, schema=SERVICE_LEARN_SCHEMA + ) - async def _send_packet(call): - """Send a packet.""" - device = hass.data[DOMAIN][call.data[CONF_HOST]] - packets = call.data[CONF_PACKET] - for packet in packets: - for retry in range(DEFAULT_RETRY): + async def _send_packet(call): + """Send a packet.""" + device = hass.data[DOMAIN][call.data[CONF_HOST]] + packets = call.data[CONF_PACKET] + for packet in packets: + for retry in range(DEFAULT_RETRY): + try: + await hass.async_add_executor_job(device.send_data, packet) + break + except (socket.timeout, ValueError): try: - await hass.async_add_executor_job(device.send_data, packet) - break - except (socket.timeout, ValueError): - try: - await hass.async_add_executor_job(device.auth) - except socket.timeout: - if retry == DEFAULT_RETRY - 1: - _LOGGER.error("Failed to send packet to device") + await hass.async_add_executor_job(device.auth) + except socket.timeout: + if retry == DEFAULT_RETRY - 1: + _LOGGER.error("Failed to send packet to device") - hass.services.async_register( - DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA - ) + hass.services.async_register( + DOMAIN, SERVICE_SEND, _send_packet, schema=SERVICE_SEND_SCHEMA + ) diff --git a/homeassistant/components/brother/.translations/cs.json b/homeassistant/components/brother/.translations/cs.json new file mode 100644 index 00000000000..716b62c6c70 --- /dev/null +++ b/homeassistant/components/brother/.translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "Tisk\u00e1rna Brother: {model} {serial_number}", + "step": { + "zeroconf_confirm": { + "data": { + "type": "Typ tisk\u00e1rny" + }, + "description": "Chcete p\u0159idat tisk\u00e1rnu Brother {model} se s\u00e9riov\u00fdm \u010d\u00edslem \"{serial_number}\" do Home Assistant?", + "title": "Objeven\u00e1 tisk\u00e1rna Brother" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/da.json b/homeassistant/components/brother/.translations/da.json index 2ec79228194..7a8f754bd9f 100644 --- a/homeassistant/components/brother/.translations/da.json +++ b/homeassistant/components/brother/.translations/da.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP-server er sl\u00e5et fra, eller printeren underst\u00f8ttes ikke.", "wrong_host": "Ugyldigt v\u00e6rtsnavn eller IP-adresse." }, + "flow_title": "Brother-printer: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Konfigurer Brother-printerintegration. Hvis du har problemer med konfiguration, kan du g\u00e5 til: https://www.home-assistant.io/integrations/brother", "title": "Brother-printer" + }, + "zeroconf_confirm": { + "data": { + "type": "Type af printer" + }, + "description": "Vil du tilf\u00f8je Brother-printeren {model} med serienummeret `{serial_number}` til Home Assistant?", + "title": "Fandt Brother-printer" } }, "title": "Brother-printer" diff --git a/homeassistant/components/brother/.translations/de.json b/homeassistant/components/brother/.translations/de.json new file mode 100644 index 00000000000..92c8d22148f --- /dev/null +++ b/homeassistant/components/brother/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dieser Drucker ist bereits konfiguriert", + "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." + }, + "error": { + "connection_error": "Verbindungsfehler", + "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", + "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" + }, + "step": { + "user": { + "data": { + "host": "Drucker Hostname oder IP-Adresse", + "type": "Typ des Druckers" + }, + "description": "Einrichten der Brother-Drucker-Integration. Wenn Du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/brother", + "title": "Brother Drucker" + } + }, + "title": "Brother Drucker" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/en.json b/homeassistant/components/brother/.translations/en.json index d586bcea1f8..928b6bf3530 100644 --- a/homeassistant/components/brother/.translations/en.json +++ b/homeassistant/components/brother/.translations/en.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP server turned off or printer not supported.", "wrong_host": "Invalid hostname or IP address." }, + "flow_title": "Brother Printer: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", "title": "Brother Printer" + }, + "zeroconf_confirm": { + "data": { + "type": "Type of the printer" + }, + "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Brother Printer" } }, "title": "Brother Printer" diff --git a/homeassistant/components/brother/.translations/es.json b/homeassistant/components/brother/.translations/es.json index f4e53e20793..d41d09634d8 100644 --- a/homeassistant/components/brother/.translations/es.json +++ b/homeassistant/components/brother/.translations/es.json @@ -9,6 +9,7 @@ "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", "wrong_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos." }, + "flow_title": "Impresora Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Configure la integraci\u00f3n de impresoras Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", "title": "Impresora Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo de impresora" + }, + "description": "\u00bfQuiere a\u00f1adir la Impresora Brother {model} con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Impresora Brother encontrada" } }, "title": "Impresora Brother" diff --git a/homeassistant/components/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json new file mode 100644 index 00000000000..99d49cc3bd8 --- /dev/null +++ b/homeassistant/components/brother/.translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cette imprimante est d\u00e9j\u00e0 configur\u00e9e.", + "unsupported_model": "Ce mod\u00e8le d'imprimante n'est pas pris en charge." + }, + "error": { + "connection_error": "Erreur de connexion.", + "snmp_error": "Serveur SNMP d\u00e9sactiv\u00e9 ou imprimante non prise en charge.", + "wrong_host": "Nom d'h\u00f4te ou adresse IP invalide." + }, + "step": { + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP de l'imprimante", + "type": "Type d'imprimante" + }, + "description": "Configurez l'int\u00e9gration de l'imprimante Brother. Si vous avez des probl\u00e8mes avec la configuration, allez \u00e0 : https://www.home-assistant.io/integrations/brother", + "title": "Imprimante Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Type d'imprimante" + } + } + }, + "title": "Imprimante Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/hu.json b/homeassistant/components/brother/.translations/hu.json new file mode 100644 index 00000000000..a0e83450b37 --- /dev/null +++ b/homeassistant/components/brother/.translations/hu.json @@ -0,0 +1,15 @@ +{ + "config": { + "flow_title": "Brother nyomtat\u00f3: {model} {serial_number}", + "step": { + "zeroconf_confirm": { + "data": { + "type": "A nyomtat\u00f3 t\u00edpusa" + }, + "description": "Hozz\u00e1 akarja adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: {serial_number} `, a Home Assistant-hoz?", + "title": "Felfedezett Brother nyomtat\u00f3" + } + }, + "title": "Brother nyomtat\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/it.json b/homeassistant/components/brother/.translations/it.json index 43bdb7aec7b..838598f24f7 100644 --- a/homeassistant/components/brother/.translations/it.json +++ b/homeassistant/components/brother/.translations/it.json @@ -9,6 +9,7 @@ "snmp_error": "Server SNMP spento o stampante non supportata.", "wrong_host": "Nome host o indirizzo IP non valido." }, + "flow_title": "Stampante Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Configurare l'integrazione della stampante Brother. In caso di problemi con la configurazione, visitare: https://www.home-assistant.io/integrations/brother", "title": "Stampante Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo di stampante" + }, + "description": "Vuoi aggiungere la stampante Brother {model} con il numero seriale `{serial_number}` a Home Assistant?", + "title": "Trovata stampante Brother" } }, "title": "Stampante Brother" diff --git a/homeassistant/components/brother/.translations/ko.json b/homeassistant/components/brother/.translations/ko.json index 4d2e213cbee..ec0f0d2453f 100644 --- a/homeassistant/components/brother/.translations/ko.json +++ b/homeassistant/components/brother/.translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "unsupported_model": "\uc774 \ud504\ub9b0\ud130 \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, "error": { @@ -8,6 +9,7 @@ "snmp_error": "SNMP \uc11c\ubc84\uac00 \uaebc\uc838 \uc788\uac70\ub098 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud504\ub9b0\ud130\uc785\ub2c8\ub2e4.", "wrong_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "flow_title": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130: {model} {serial_number}", "step": { "user": { "data": { @@ -16,6 +18,13 @@ }, "description": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00\uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/brother \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130" + }, + "zeroconf_confirm": { + "data": { + "type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958" + }, + "description": "\uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \ub85c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130 {model} \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c \ube0c\ub77c\ub354 \ud504\ub9b0\ud130" } }, "title": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130" diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json index dd051b1bb0c..7553933b66e 100644 --- a/homeassistant/components/brother/.translations/lb.json +++ b/homeassistant/components/brother/.translations/lb.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP Server ausgeschalt oder Printer net \u00ebnnerst\u00ebtzt.", "wrong_host": "Ong\u00ebltege Numm oder IP Adresse" }, + "flow_title": "Brother Printer: {model {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Brother Printer Integratioun ariichten. Am Fall vun Problemer kuckt op: https://www.home-assistant.io/integrations/brother", "title": "Brother Printer" + }, + "zeroconf_confirm": { + "data": { + "type": "Typ vum Printer" + }, + "description": "W\u00ebllt dir den Brother Printer {model} mat der Seriennummer `{serial_number}` am Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten Brother Printer" } }, "title": "Brother Printer" diff --git a/homeassistant/components/brother/.translations/nl.json b/homeassistant/components/brother/.translations/nl.json new file mode 100644 index 00000000000..ed7d3980f47 --- /dev/null +++ b/homeassistant/components/brother/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "unsupported_model": "Dit printermodel wordt niet ondersteund." + }, + "error": { + "connection_error": "Verbindingsfout.", + "wrong_host": "Ongeldige hostnaam of IP-adres." + }, + "step": { + "user": { + "data": { + "host": "Printerhostnaam of IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/no.json b/homeassistant/components/brother/.translations/no.json index d4cf935f156..46bfe618176 100644 --- a/homeassistant/components/brother/.translations/no.json +++ b/homeassistant/components/brother/.translations/no.json @@ -9,6 +9,7 @@ "snmp_error": "SNMP verten er skrudd av eller printeren er ikke st\u00f8ttet.", "wrong_host": "Ugyldig vertsnavn eller IP-adresse." }, + "flow_title": "Brother Printer: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Konfigurer Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: https://www.home-assistant.io/integrations/brother", "title": "Brother skriver" + }, + "zeroconf_confirm": { + "data": { + "type": "Type skriver" + }, + "description": "Vil du legge til Brother-skriveren {Model} med serienummeret {serial_number} til Home Assistant?", + "title": "Oppdaget Brother-Skriveren" } }, "title": "Brother skriver" diff --git a/homeassistant/components/brother/.translations/pl.json b/homeassistant/components/brother/.translations/pl.json index 14fe4024f34..1417720714e 100644 --- a/homeassistant/components/brother/.translations/pl.json +++ b/homeassistant/components/brother/.translations/pl.json @@ -9,6 +9,7 @@ "snmp_error": "Serwer SNMP wy\u0142\u0105czony lub drukarka nie jest obs\u0142ugiwana.", "wrong_host": "Niepoprawna nazwa hosta lub adres IP drukarki." }, + "flow_title": "Drukarka Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "Konfiguracja integracji drukarek Brother. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/brother", "title": "Drukarka Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "Typ drukarki" + }, + "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistant'a?", + "title": "Wykryto drukark\u0119 Brother" } }, "title": "Drukarka Brother" diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json index eb12f2f1225..0faf059c8b9 100644 --- a/homeassistant/components/brother/.translations/ru.json +++ b/homeassistant/components/brother/.translations/ru.json @@ -9,6 +9,7 @@ "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother: {model} {serial_number}", "step": { "user": { "data": { @@ -17,6 +18,13 @@ }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/brother.", "title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" + }, + "zeroconf_confirm": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother {model} \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" } }, "title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" diff --git a/homeassistant/components/brother/.translations/sv.json b/homeassistant/components/brother/.translations/sv.json new file mode 100644 index 00000000000..8661c3278bc --- /dev/null +++ b/homeassistant/components/brother/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "zeroconf_confirm": { + "data": { + "type": "Typ av skrivare" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index b95469977a7..e50105e0b27 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -34,6 +34,11 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize.""" + self.brother = None + self.host = None + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -64,6 +69,58 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + async def async_step_zeroconf(self, user_input=None): + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + if not user_input.get("name") or not user_input["name"].startswith("Brother"): + return self.async_abort(reason="not_brother_printer") + + # Hostname is format: brother.local. + self.host = user_input["hostname"].rstrip(".") + + self.brother = Brother(self.host) + try: + await self.brother.async_update() + except (ConnectionError, SnmpError, UnsupportedModel): + return self.async_abort(reason="connection_error") + + # Check if already configured + await self.async_set_unique_id(self.brother.serial.lower()) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + { + "title_placeholders": { + "serial_number": self.brother.serial, + "model": self.brother.model, + } + } + ) + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle a flow initiated by zeroconf.""" + if user_input is not None: + title = f"{self.brother.model} {self.brother.serial}" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + return self.async_create_entry( + title=title, + data={CONF_HOST: self.host, CONF_TYPE: user_input[CONF_TYPE]}, + ) + return self.async_show_form( + step_id="zeroconf_confirm", + data_schema=vol.Schema( + {vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES)} + ), + description_placeholders={ + "serial_number": self.brother.serial, + "model": self.brother.model, + }, + ) + class InvalidHost(exceptions.HomeAssistantError): """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index d080ee4fd6c..e63fb9b0d7c 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -5,5 +5,6 @@ "dependencies": [], "codeowners": ["@bieniu"], "requirements": ["brother==0.1.4"], + "zeroconf": ["_printer._tcp.local."], "config_flow": true } diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json index b636b7c0202..c14903df950 100644 --- a/homeassistant/components/brother/strings.json +++ b/homeassistant/components/brother/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "Brother Printer", + "flow_title": "Brother Printer: {model} {serial_number}", "step": { "user": { "title": "Brother Printer", @@ -9,6 +10,13 @@ "host": "Printer hostname or IP address", "type": "Type of the printer" } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Brother Printer", + "data": { + "type": "Type of the printer" + } } }, "error": { diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 6928879d405..b41b3220b40 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -15,14 +15,18 @@ from homeassistant.util import dt as dt_util CONF_DIMENSION = "dimension" CONF_DELTA = "delta" +CONF_COUNTRY = "country_code" -RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/RadarMapNL?w={w}&h={h}" +RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/RadarMap{c}?w={w}&h={h}" _LOG = logging.getLogger(__name__) # Maximum range according to docs DIM_RANGE = vol.All(vol.Coerce(int), vol.Range(min=120, max=700)) +# Multiple choice for available Radar Map URL +SUPPORTED_COUNTRY_CODES = ["NL", "BE"] + PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { @@ -31,6 +35,9 @@ PLATFORM_SCHEMA = vol.All( vol.Coerce(float), vol.Range(min=0) ), vol.Optional(CONF_NAME, default="Buienradar loop"): cv.string, + vol.Optional(CONF_COUNTRY, default="NL"): vol.All( + vol.Coerce(str), vol.In(SUPPORTED_COUNTRY_CODES) + ), } ) ) @@ -41,8 +48,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= dimension = config[CONF_DIMENSION] delta = config[CONF_DELTA] name = config[CONF_NAME] + country = config[CONF_COUNTRY] - async_add_entities([BuienradarCam(name, dimension, delta)]) + async_add_entities([BuienradarCam(name, dimension, delta, country)]) class BuienradarCam(Camera): @@ -54,7 +62,7 @@ class BuienradarCam(Camera): [0]: https://www.buienradar.nl/overbuienradar/gratis-weerdata """ - def __init__(self, name: str, dimension: int, delta: float): + def __init__(self, name: str, dimension: int, delta: float, country: str): """ Initialize the component. @@ -70,6 +78,9 @@ class BuienradarCam(Camera): # time a cached image stays valid for self._delta = delta + # country location + self._country = country + # Condition that guards the loading indicator. # # Ensures that only one reader can cause an http request at the same @@ -101,7 +112,9 @@ class BuienradarCam(Camera): """Retrieve new radar image and return whether this succeeded.""" session = async_get_clientsession(self.hass) - url = RADAR_MAP_URL_TEMPLATE.format(w=self._dimension, h=self._dimension) + url = RADAR_MAP_URL_TEMPLATE.format( + c=self._country, w=self._dimension, h=self._dimension + ) if self._last_modified: headers = {"If-Modified-Since": self._last_modified} diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4fe52a7d164..b02874780e5 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -364,19 +364,12 @@ class Camera(Entity): """Return bytes of camera image.""" raise NotImplementedError() - @callback - def async_camera_image(self): - """Return bytes of camera image. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.camera_image) + async def async_camera_image(self): + """Return bytes of camera image.""" + return await self.hass.async_add_job(self.camera_image) async def handle_async_still_stream(self, request, interval): - """Generate an HTTP MJPEG stream from camera images. - - This method must be run in the event loop. - """ + """Generate an HTTP MJPEG stream from camera images.""" return await async_get_still_stream( request, self.async_camera_image, self.content_type, interval ) @@ -386,7 +379,6 @@ class Camera(Entity): This method can be overridden by camera plaforms to proxy a direct stream from the camera. - This method must be run in the event loop. """ return await self.handle_async_still_stream(request, self.frame_interval) @@ -408,19 +400,17 @@ class Camera(Entity): """Turn off camera.""" raise NotImplementedError() - @callback - def async_turn_off(self): + async def async_turn_off(self): """Turn off camera.""" - return self.hass.async_add_job(self.turn_off) + await self.hass.async_add_job(self.turn_off) def turn_on(self): """Turn off camera.""" raise NotImplementedError() - @callback - def async_turn_on(self): + async def async_turn_on(self): """Turn off camera.""" - return self.hass.async_add_job(self.turn_on) + await self.hass.async_add_job(self.turn_on) def enable_motion_detection(self): """Enable motion detection in the camera.""" diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index c6db2d897e4..51558e78266 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==4.0.1"], + "requirements": ["pychromecast==4.1.1"], "dependencies": [], "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json index 4df2ebe4fd9..e344e2dfd29 100644 --- a/homeassistant/components/cert_expiry/.translations/de.json +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -18,7 +18,7 @@ "name": "Der Name des Zertifikats", "port": "Der Port des Zertifikats" }, - "title": "Definieren Sie das zu testende Zertifikat" + "title": "Definiere das zu testende Zertifikat" } }, "title": "Zertifikatsablauf" diff --git a/homeassistant/components/ciscospark/__init__.py b/homeassistant/components/ciscospark/__init__.py deleted file mode 100644 index f872a0257f7..00000000000 --- a/homeassistant/components/ciscospark/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ciscospark component.""" diff --git a/homeassistant/components/ciscospark/manifest.json b/homeassistant/components/ciscospark/manifest.json deleted file mode 100644 index 4fd87a8a5e4..00000000000 --- a/homeassistant/components/ciscospark/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "ciscospark", - "name": "Cisco Spark", - "documentation": "https://www.home-assistant.io/integrations/ciscospark", - "requirements": ["ciscosparkapi==0.4.2"], - "dependencies": [], - "codeowners": ["@fbradyirl"] -} diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py deleted file mode 100644 index e765aff05f6..00000000000 --- a/homeassistant/components/ciscospark/notify.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Cisco Spark platform for notify component.""" -import logging - -from ciscosparkapi import CiscoSparkAPI, SparkApiError -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, - PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_ROOMID = "roomid" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_TOKEN): cv.string, vol.Required(CONF_ROOMID): cv.string} -) - - -def get_service(hass, config, discovery_info=None): - """Get the CiscoSpark notification service.""" - return CiscoSparkNotificationService( - config.get(CONF_TOKEN), config.get(CONF_ROOMID) - ) - - -class CiscoSparkNotificationService(BaseNotificationService): - """The Cisco Spark Notification Service.""" - - def __init__(self, token, default_room): - """Initialize the service.""" - - self._default_room = default_room - self._token = token - self._spark = CiscoSparkAPI(access_token=self._token) - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - - try: - title = "" - if kwargs.get(ATTR_TITLE) is not None: - title = kwargs.get(ATTR_TITLE) + ": " - self._spark.messages.create(roomId=self._default_room, text=title + message) - except SparkApiError as api_error: - _LOGGER.error( - "Could not send CiscoSpark notification. Error: %s", api_error - ) diff --git a/homeassistant/components/climate/.translations/zh-Hans.json b/homeassistant/components/climate/.translations/zh-Hans.json new file mode 100644 index 00000000000..3459ef3b798 --- /dev/null +++ b/homeassistant/components/climate/.translations/zh-Hans.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u66f4\u6539 {entity_name} \u7a7a\u8c03\u6a21\u5f0f", + "set_preset_mode": "\u66f4\u6539 {entity_name} \u9884\u8bbe\u6a21\u5f0f" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u7a7a\u8c03\u6a21\u5f0f", + "is_preset_mode": "{entity_name} \u88ab\u8bbe\u4e3a\u6307\u5b9a\u7684\u9884\u8bbe\u6a21\u5f0f" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u6d4b\u91cf\u7684\u5ba4\u5185\u6e7f\u5ea6\u53d8\u5316", + "current_temperature_changed": "{entity_name} \u6d4b\u91cf\u7684\u5ba4\u5185\u6e29\u5ea6\u53d8\u5316", + "hvac_mode_changed": "{entity_name} \u7684\u8fd0\u884c\u6a21\u5f0f\u53d8\u5316" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 26cec7efbeb..b489071db57 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -56,7 +56,6 @@ PRESET_SLEEP = "sleep" # Device is reacting to activity (e.g. movement sensors) PRESET_ACTIVITY = "activity" - # Possible fan state FAN_ON = "on" FAN_OFF = "off" diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index cf393a035ec..8a5b9ceede8 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -11,7 +11,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -77,6 +77,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 24947ed7952..ef73d4356d5 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -11,7 +11,7 @@ from homeassistant.components.alexa import ( errors as alexa_errors, smart_home as alexa_sh, ) -from homeassistant.components.google_assistant import smart_home as ga +from homeassistant.components.google_assistant import const as gc, smart_home as ga from homeassistant.core import Context, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType @@ -160,7 +160,7 @@ class CloudClient(Interface): gconf = await self.get_google_config() return await ga.async_handle_message( - self._hass, gconf, gconf.cloud_user, payload + self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD ) async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 1d996614caa..1edf141604f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -91,7 +91,7 @@ class CommandCover(CoverDevice): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = subprocess.call(command, shell=True) == 0 + success = subprocess.call(command, shell=True) == 0 # nosec # shell by design if not success: _LOGGER.error("Command failed: %s", command) @@ -104,7 +104,9 @@ class CommandCover(CoverDevice): _LOGGER.info("Running state command: %s", command) try: - return_value = subprocess.check_output(command, shell=True) + return_value = subprocess.check_output( + command, shell=True # nosec # shell by design + ) return return_value.strip().decode("utf-8") except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 21653171f34..50b0bec74ee 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -33,7 +33,10 @@ class CommandLineNotificationService(BaseNotificationService): """Send a message to a command line.""" try: proc = subprocess.Popen( - self.command, universal_newlines=True, stdin=subprocess.PIPE, shell=True + self.command, + universal_newlines=True, + stdin=subprocess.PIPE, + shell=True, # nosec # shell by design ) proc.communicate(input=message) if proc.returncode != 0: diff --git a/homeassistant/components/command_line/sensor.py b/homeassistant/components/command_line/sensor.py index 85ba78ecd98..c1fb5f1d21e 100644 --- a/homeassistant/components/command_line/sensor.py +++ b/homeassistant/components/command_line/sensor.py @@ -168,15 +168,14 @@ class CommandSensorData: if rendered_args == args: # No template used. default behavior - shell = True + pass else: # Template used. Construct the string used in the shell command = str(" ".join([prog] + shlex.split(rendered_args))) - shell = True try: _LOGGER.debug("Running command: %s", command) return_value = subprocess.check_output( - command, shell=shell, timeout=self.timeout + command, shell=True, timeout=self.timeout # nosec # shell by design ) self.value = return_value.strip().decode("utf-8") except subprocess.CalledProcessError: diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 62dcbe2f15a..f89ac6f5b92 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -94,7 +94,7 @@ class CommandSwitch(SwitchDevice): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = subprocess.call(command, shell=True) == 0 + success = subprocess.call(command, shell=True) == 0 # nosec # shell by design if not success: _LOGGER.error("Command failed: %s", command) @@ -107,7 +107,9 @@ class CommandSwitch(SwitchDevice): _LOGGER.info("Running state command: %s", command) try: - return_value = subprocess.check_output(command, shell=True) + return_value = subprocess.check_output( + command, shell=True # nosec # shell by design + ) return return_value.strip().decode("utf-8") except subprocess.CalledProcessError: _LOGGER.error("Command failed: %s", command) @@ -116,7 +118,7 @@ class CommandSwitch(SwitchDevice): def _query_state_code(command): """Execute state command for return code.""" _LOGGER.info("Running state command: %s", command) - return subprocess.call(command, shell=True) == 0 + return subprocess.call(command, shell=True) == 0 # nosec # shell by design @property def should_poll(self): diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5873cdc3271..ad7ae14ecb7 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -28,6 +28,8 @@ SECTIONS = ( "scene", ) ON_DEMAND = ("zwave",) +ACTION_CREATE_UPDATE = "create_update" +ACTION_DELETE = "delete" async def async_setup(hass, config): @@ -152,7 +154,9 @@ class BaseEditConfigView(HomeAssistantView): await hass.async_add_executor_job(_write, path, current) if self.post_write_hook is not None: - hass.async_create_task(self.post_write_hook(hass)) + hass.async_create_task( + self.post_write_hook(ACTION_CREATE_UPDATE, config_key) + ) return self.json({"result": "ok"}) @@ -170,7 +174,7 @@ class BaseEditConfigView(HomeAssistantView): await hass.async_add_executor_job(_write, path, current) if self.post_write_hook is not None: - hass.async_create_task(self.post_write_hook(hass)) + hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key)) return self.json({"result": "ok"}) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index d7bb1ef9883..6216a52fc13 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -6,18 +6,30 @@ from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item from homeassistant.config import AUTOMATION_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry -from . import EditIdBasedConfigView +from . import ACTION_DELETE, EditIdBasedConfigView async def async_setup(hass): """Set up the Automation config API.""" - async def hook(hass): + async def hook(action, config_key): """post_write_hook for Config View that reloads automations.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + if action != ACTION_DELETE: + return + + ent_reg = await entity_registry.async_get_registry(hass) + + entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key) + + if entity_id is None: + return + + ent_reg.async_remove(entity_id) + hass.http.register_view( EditAutomationConfigView( DOMAIN, diff --git a/homeassistant/components/config/customize.py b/homeassistant/components/config/customize.py index ed75a8a04a6..3b1122fc3a5 100644 --- a/homeassistant/components/config/customize.py +++ b/homeassistant/components/config/customize.py @@ -12,7 +12,7 @@ CONFIG_PATH = "customize.yaml" async def async_setup(hass): """Set up the Customize config API.""" - async def hook(hass): + async def hook(action, config_key): """post_write_hook for Config View that reloads groups.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD_CORE_CONFIG) diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index d95891af655..e26b2b80bc1 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -10,7 +10,7 @@ from . import EditKeyBasedConfigView async def async_setup(hass): """Set up the Group config API.""" - async def hook(hass): + async def hook(action, config_key): """post_write_hook for Config View that reloads groups.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 79a30177e47..b380656c541 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -5,18 +5,31 @@ import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA from homeassistant.config import SCENE_CONFIG_PATH from homeassistant.const import CONF_ID, SERVICE_RELOAD -import homeassistant.helpers.config_validation as cv +from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.helpers import config_validation as cv, entity_registry -from . import EditIdBasedConfigView +from . import ACTION_DELETE, EditIdBasedConfigView async def async_setup(hass): """Set up the Scene config API.""" - async def hook(hass): + async def hook(action, config_key): """post_write_hook for Config View that reloads scenes.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + if action != ACTION_DELETE: + return + + ent_reg = await entity_registry.async_get_registry(hass) + + entity_id = ent_reg.async_get_entity_id(DOMAIN, HA_DOMAIN, config_key) + + if entity_id is None: + return + + ent_reg.async_remove(entity_id) + hass.http.register_view( EditSceneConfigView( DOMAIN, diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 032774de473..de9c25b223f 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -10,7 +10,7 @@ from . import EditKeyBasedConfigView async def async_setup(hass): """Set up the script config API.""" - async def hook(hass): + async def hook(action, config_key): """post_write_hook for Config View that reloads scripts.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) diff --git a/homeassistant/components/coolmaster/.translations/de.json b/homeassistant/components/coolmaster/.translations/de.json index c312de14935..5359f92b138 100644 --- a/homeassistant/components/coolmaster/.translations/de.json +++ b/homeassistant/components/coolmaster/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "connection_error": "Verbindung zur CoolMasterNet-Instanz fehlgeschlagen. Bitte \u00fcberpr\u00fcfen Sie Ihren Host.", + "connection_error": "Verbindung zur CoolMasterNet-Instanz fehlgeschlagen. Bitte \u00fcberpr\u00fcfe deinen Host.", "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." }, "step": { @@ -15,7 +15,7 @@ "host": "Host", "off": "Kann ausgeschaltet werden" }, - "title": "Richten Sie Ihre CoolMasterNet-Verbindungsdaten ein." + "title": "Richte deine CoolMasterNet-Verbindungsdaten ein." } }, "title": "CoolMasterNet" diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 2fe4022fb39..abefd3263bc 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -236,23 +236,17 @@ class CoverDevice(Entity): """Open the cover.""" raise NotImplementedError() - def async_open_cover(self, **kwargs): - """Open the cover. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) def close_cover(self, **kwargs: Any) -> None: """Close cover.""" raise NotImplementedError() - def async_close_cover(self, **kwargs): - """Close cover. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" @@ -261,69 +255,52 @@ class CoverDevice(Entity): else: self.close_cover(**kwargs) - def async_toggle(self, **kwargs): - """Toggle the entity. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_toggle(self, **kwargs): + """Toggle the entity.""" if self.is_closed: - return self.async_open_cover(**kwargs) - return self.async_close_cover(**kwargs) + await self.async_open_cover(**kwargs) + else: + await self.async_close_cover(**kwargs) def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" pass - def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.set_cover_position, **kwargs)) + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + await self.hass.async_add_job(ft.partial(self.set_cover_position, **kwargs)) def stop_cover(self, **kwargs): """Stop the cover.""" pass - def async_stop_cover(self, **kwargs): - """Stop the cover. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" pass - def async_open_cover_tilt(self, **kwargs): - """Open the cover tilt. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) + async def async_open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + await self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" pass - def async_close_cover_tilt(self, **kwargs): - """Close the cover tilt. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.close_cover_tilt, **kwargs)) + async def async_close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + await self.hass.async_add_job(ft.partial(self.close_cover_tilt, **kwargs)) def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" pass - def async_set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job( + async def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + await self.hass.async_add_job( ft.partial(self.set_cover_tilt_position, **kwargs) ) @@ -331,12 +308,9 @@ class CoverDevice(Entity): """Stop the cover.""" pass - def async_stop_cover_tilt(self, **kwargs): - """Stop the cover. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover.""" + await self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) def toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" @@ -345,11 +319,9 @@ class CoverDevice(Entity): else: self.close_cover_tilt(**kwargs) - def async_toggle_tilt(self, **kwargs): - """Toggle the entity. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_toggle_tilt(self, **kwargs): + """Toggle the entity.""" if self.current_cover_tilt_position == 0: - return self.async_open_cover_tilt(**kwargs) - return self.async_close_cover_tilt(**kwargs) + await self.async_open_cover_tilt(**kwargs) + else: + await self.async_close_cover_tilt(**kwargs) diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index ec6da84e5f6..7c6dc5fed72 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( condition, config_validation as cv, @@ -163,6 +163,7 @@ async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> } +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: @@ -196,6 +197,7 @@ def async_condition_from_config( f"{{{{ state.attributes.{position} }}}}" ) + @callback def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: """Validate template based if-condition.""" value_template.hass = hass diff --git a/homeassistant/components/daikin/.translations/de.json b/homeassistant/components/daikin/.translations/de.json index 0a09c7b5cfa..b3e775fadf4 100644 --- a/homeassistant/components/daikin/.translations/de.json +++ b/homeassistant/components/daikin/.translations/de.json @@ -10,7 +10,7 @@ "data": { "host": "Host" }, - "description": "Geben Sie die IP-Adresse Ihrer Daikin AC ein.", + "description": "Gib die IP-Adresse deiner Daikin AC ein.", "title": "Daikin AC konfigurieren" } }, diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 52d1b516d32..a752642335f 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==1.6.1"], + "requirements": ["pydaikin==1.6.2"], "dependencies": [], "codeowners": ["@fredrike", "@rofrantz"], "quality_scale": "platinum" diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index c665690796d..954d1c8eb6e 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -10,6 +10,10 @@ }, "flow_title": "Br\u00e1na deCONZ ZigBee ({host})", "step": { + "hassio_confirm": { + "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k deCONZ br\u00e1n\u011b pomoc\u00ed hass.io {addon}?", + "title": "deCONZ Zigbee br\u00e1na prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + }, "init": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index d177448f4fd..479e645173b 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "Import virtueller Sensoren zulassen", "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" }, - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" }, "init": { @@ -77,14 +77,21 @@ "remote_button_short_release": "\"{subtype}\" Taste losgelassen", "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", "remote_double_tap": "Ger\u00e4t \"{subtype}\" doppelt getippt", + "remote_double_tap_any_side": "Ger\u00e4t auf beliebiger Seite doppelt angetippt", "remote_falling": "Ger\u00e4t im freien Fall", + "remote_flip_180_degrees": "Ger\u00e4t um 180 Grad gekippt", + "remote_flip_90_degrees": "Ger\u00e4t um 90 Grad gekippt", "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert", + "remote_moved": "Ger\u00e4t mit \"{subtype}\" nach oben bewegt", + "remote_moved_any_side": "Ger\u00e4t mit beliebiger Seite nach oben bewegt", "remote_rotate_from_side_1": "Ger\u00e4t von \"Seite 1\" auf \"{subtype}\" gedreht", "remote_rotate_from_side_2": "Ger\u00e4t von \"Seite 2\" auf \"{subtype}\" gedreht", "remote_rotate_from_side_3": "Ger\u00e4t von \"Seite 3\" auf \"{subtype}\" gedreht", "remote_rotate_from_side_4": "Ger\u00e4t von \"Seite 4\" auf \"{subtype}\" gedreht", "remote_rotate_from_side_5": "Ger\u00e4t von \"Seite 5\" auf \"{subtype}\" gedreht", - "remote_rotate_from_side_6": "Ger\u00e4t von \"Seite 6\" auf \"{subtype}\" gedreht" + "remote_rotate_from_side_6": "Ger\u00e4t von \"Seite 6\" auf \"{subtype}\" gedreht", + "remote_turned_clockwise": "Ger\u00e4t im Uhrzeigersinn gedreht", + "remote_turned_counter_clockwise": "Ger\u00e4t gegen den Uhrzeigersinn gedreht" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 1a4232e0817..c900bdab6ab 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -78,14 +78,19 @@ "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", "remote_falling": "Appareil en chute libre", + "remote_flip_180_degrees": "Dispositif retourn\u00e9 \u00e0 180 degr\u00e9s", + "remote_flip_90_degrees": "Dispositif retourn\u00e9 \u00e0 90 degr\u00e9s", "remote_gyro_activated": "Appareil secou\u00e9", "remote_moved": "Appareil d\u00e9plac\u00e9 avec \"{subtype}\" vers le haut", + "remote_moved_any_side": "Dispositif d\u00e9plac\u00e9 avec un c\u00f4t\u00e9 quelconque vers le haut", "remote_rotate_from_side_1": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 1\" \u00e0 \"{subtype}\"", "remote_rotate_from_side_2": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 2\" \u00e0 \"{subtype}\"", "remote_rotate_from_side_3": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 3\" \u00e0 \"{subtype}\"", "remote_rotate_from_side_4": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 4\" \u00e0 \"{subtype}\"", "remote_rotate_from_side_5": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 5\" \u00e0 \"{subtype}\"", - "remote_rotate_from_side_6": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 6\" \u00e0 \"{subtype}\"" + "remote_rotate_from_side_6": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 6\" \u00e0 \"{subtype}\"", + "remote_turned_clockwise": "Appareil tourn\u00e9 dans le sens horaire", + "remote_turned_counter_clockwise": "Appareil tourn\u00e9 dans le sens antihoraire" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 9e810910743..f162130680c 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -33,6 +33,11 @@ "device_automation": { "trigger_subtype": { "close": "Bez\u00e1r\u00e1s" + }, + "trigger_type": { + "remote_double_tap_any_side": "A k\u00e9sz\u00fcl\u00e9k b\u00e1rmelyik oldal\u00e1n dupl\u00e1n koppint.", + "remote_flip_180_degrees": "180 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z", + "remote_flip_90_degrees": "90 fokkal megd\u00f6nt\u00f6tt eszk\u00f6z" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 5cf1cb32ca2..d526d706a8b 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c", "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c", + "remote_double_tap_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774 \ub354\ube14 \ud0ed \ub420 \ub54c", "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c", + "remote_flip_180_degrees": "\uae30\uae30\uac00 180\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", + "remote_flip_90_degrees": "\uae30\uae30\uac00 90\ub3c4\ub85c \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", + "remote_moved_any_side": "\uae30\uae30\uc758 \uc544\ubb34 \uba74\uc774\ub098 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", - "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c" + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_turned_clockwise": "\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_turned_counter_clockwise": "\ubc18\uc2dc\uacc4 \ubc29\ud5a5\uc73c\ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 29b584fb9bb..3c61e447bca 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -37,7 +37,7 @@ "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432", "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" }, - "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f deCONZ" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 deCONZ" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index a7b5160e8a3..02869dcf76e 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -17,7 +17,7 @@ "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" }, - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till deCONZ gateway som tillhandah\u00e5lls av hass.io till\u00e4gg {addon}?", + "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Hass.io-till\u00e4gget {addon}?", "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg" }, "init": { diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index 2e5a216c77d..37b82cff29c 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -29,5 +29,28 @@ } }, "title": "deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "side_1": "\u7b2c 1 \u9762", + "side_2": "\u7b2c 2 \u9762", + "side_3": "\u7b2c 3 \u9762", + "side_4": "\u7b2c 4 \u9762", + "side_5": "\u7b2c 5 \u9762", + "side_6": "\u7b2c 6 \u9762" + }, + "trigger_type": { + "remote_awakened": "\u8bbe\u5907\u5524\u9192", + "remote_double_tap": "\u8bbe\u5907\u7684\u201c{subtype}\u201d\u88ab\u8f7b\u6572\u4e24\u6b21", + "remote_falling": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53", + "remote_gyro_activated": "\u8bbe\u5907\u6447\u6643", + "remote_moved": "\u8bbe\u5907\u6c34\u5e73\u79fb\u52a8\u4e14\u201c{subtype}\u201d\u671d\u4e0a", + "remote_rotate_from_side_1": "\u8bbe\u5907\u4ece\u201c\u7b2c 1 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", + "remote_rotate_from_side_2": "\u8bbe\u5907\u4ece\u201c\u7b2c 2 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", + "remote_rotate_from_side_3": "\u8bbe\u5907\u4ece\u201c\u7b2c 3 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", + "remote_rotate_from_side_4": "\u8bbe\u5907\u4ece\u201c\u7b2c 4 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", + "remote_rotate_from_side_5": "\u8bbe\u5907\u4ece\u201c\u7b2c 5 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d", + "remote_rotate_from_side_6": "\u8bbe\u5907\u4ece\u201c\u7b2c 6 \u9762\u201d\u7ffb\u8f6c\u5230\u201c{subtype}\u201d" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 225a28f52f8..667eb6db075 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -89,8 +89,10 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): if self._device.secondary_temperature is not None: attr[ATTR_TEMPERATURE] = self._device.secondary_temperature - if self._device.type in Presence.ZHATYPE and self._device.dark is not None: - attr[ATTR_DARK] = self._device.dark + if self._device.type in Presence.ZHATYPE: + + if self._device.dark is not None: + attr[ATTR_DARK] = self._device.dark elif self._device.type in Vibration.ZHATYPE: attr[ATTR_ORIENTATION] = self._device.orientation diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 43c6cee9193..614d2378c88 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -147,41 +147,21 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge_id = await async_get_bridge_id( session, **self.deconz_config ) - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if self.bridge_id == entry.unique_id: - return self._update_entry( - entry, - host=self.deconz_config[CONF_HOST], - port=self.deconz_config[CONF_PORT], - api_key=self.deconz_config[CONF_API_KEY], - ) - await self.async_set_unique_id(self.bridge_id) + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: self.deconz_config[CONF_HOST], + CONF_PORT: self.deconz_config[CONF_PORT], + CONF_API_KEY: self.deconz_config[CONF_API_KEY], + } + ) + except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) - def _update_entry(self, entry, host, port, api_key=None): - """Update existing entry.""" - if ( - entry.data[CONF_HOST] == host - and entry.data[CONF_PORT] == port - and (api_key is None or entry.data[CONF_API_KEY] == api_key) - ): - return self.async_abort(reason="already_configured") - - entry.data[CONF_HOST] = host - entry.data[CONF_PORT] = port - - if api_key is not None: - entry.data[CONF_API_KEY] = api_key - - self.hass.config_entries.async_update_entry(entry) - return self.async_abort(reason="updated_instance") - async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" if ( @@ -193,13 +173,14 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if self.bridge_id == entry.unique_id: - if entry.source == "hassio": - return self.async_abort(reason="already_configured") - return self._update_entry(entry, parsed_url.hostname, parsed_url.port) + entry = await self.async_set_unique_id(self.bridge_id) + if entry and entry.source == "hassio": + return self.async_abort(reason="already_configured") + + self._abort_if_unique_id_configured( + updates={CONF_HOST: parsed_url.hostname, CONF_PORT: parsed_url.port} + ) - await self.async_set_unique_id(self.bridge_id) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = {"host": parsed_url.hostname} @@ -216,17 +197,16 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the discovery component. """ self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL]) - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if self.bridge_id == entry.unique_id: - return self._update_entry( - entry, - user_input[CONF_HOST], - user_input[CONF_PORT], - user_input[CONF_API_KEY], - ) - await self.async_set_unique_id(self.bridge_id) + + self._abort_if_unique_id_configured( + updates={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) + self._hassio_discovery = user_input return await self.async_step_hassio_confirm() diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e951e61fde7..293e0d9719c 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -47,7 +47,7 @@ DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device"] COVER_TYPES = DAMPERS + WINDOW_COVERS -POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] +POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 4ac3e6cd379..06756bb49f6 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -59,7 +59,10 @@ class DeconzDevice(DeconzBase, Entity): @property def entity_registry_enabled_default(self): - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added to the entity registry. + + Daylight is a virtual sensor from deCONZ that should never be enabled by default. + """ if not self.gateway.option_allow_clip_sensor and self._device.type.startswith( "CLIP" ): @@ -71,6 +74,9 @@ class DeconzDevice(DeconzBase, Entity): ): return False + if self._device.type == "Daylight": + return False + return True async def async_added_to_hass(self): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 527e8d2ab7a..98a85a707bd 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -50,7 +50,7 @@ class DeconzEvent(DeconzBase): CONF_EVENT: self._device.state, } - if self._device.gesture: + if self._device.gesture is not None: data[CONF_GESTURE] = self._device.gesture self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 15d3b828741..ee22c86c44a 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -90,9 +90,12 @@ class DeconzLight(DeconzDevice, Light): """Set up light.""" super().__init__(device, gateway) - self._features = SUPPORT_BRIGHTNESS - self._features |= SUPPORT_FLASH - self._features |= SUPPORT_TRANSITION + self._features = 0 + + if self._device.brightness is not None: + self._features |= SUPPORT_BRIGHTNESS + self._features |= SUPPORT_FLASH + self._features |= SUPPORT_TRANSITION if self._device.ct is not None: self._features |= SUPPORT_COLOR_TEMP diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index f448e9105c8..adac6f54493 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==68" + "pydeconz==69" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8261f03e902..81804dfb9f6 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -143,8 +143,13 @@ class DeconzSensor(DeconzDevice): elif self._device.type in Daylight.ZHATYPE: attr[ATTR_DAYLIGHT] = self._device.daylight - elif self._device.type in LightLevel.ZHATYPE and self._device.dark is not None: - attr[ATTR_DARK] = self._device.dark + elif self._device.type in LightLevel.ZHATYPE: + + if self._device.dark is not None: + attr[ATTR_DARK] = self._device.dark + + if self._device.daylight is not None: + attr[ATTR_DAYLIGHT] = self._device.daylight elif self._device.type in Power.ZHATYPE: attr[ATTR_CURRENT] = self._device.current diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f893b9880fd..f1b19c79fce 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -1,4 +1,5 @@ """deCONZ services.""" +from pydeconz.utils import normalize_bridge_id import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -97,15 +98,14 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ - bridgeid = data.get(CONF_BRIDGE_ID) + gateway = get_master_gateway(hass) + if CONF_BRIDGE_ID in data: + gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] + field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] - gateway = get_master_gateway(hass) - if bridgeid: - gateway = hass.data[DOMAIN][bridgeid] - if entity_id: try: field = gateway.deconz_ids[entity_id] + field @@ -120,7 +120,7 @@ async def async_refresh_devices_service(hass, data): """Refresh available devices from deCONZ.""" gateway = get_master_gateway(hass) if CONF_BRIDGE_ID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]] + gateway = hass.data[DOMAIN][normalize_bridge_id(data[CONF_BRIDGE_ID])] groups = set(gateway.api.groups.keys()) lights = set(gateway.api.lights.keys()) diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index c0a27b667c5..e19b1262b74 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -18,7 +18,8 @@ "sun", "system_health", "updater", - "zeroconf" + "zeroconf", + "zone" ], "codeowners": [] } diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 20e3a52aa8d..ab95cc978b3 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -6,7 +6,8 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverDevice, ) -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_utc_time_change from . import DOMAIN @@ -131,21 +132,21 @@ class DemoCover(CoverDevice): return self._supported_features return super().supported_features - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the cover.""" if self._position == 0: return if self._position is None: self._closed = True - self.schedule_update_ha_state() + self.async_write_ha_state() return self._is_closing = True self._listen_cover() self._requested_closing = True - self.schedule_update_ha_state() + self.async_write_ha_state() - def close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Close the cover tilt.""" if self._tilt_position in (0, None): return @@ -153,21 +154,21 @@ class DemoCover(CoverDevice): self._listen_cover_tilt() self._requested_closing_tilt = True - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the cover.""" if self._position == 100: return if self._position is None: self._closed = False - self.schedule_update_ha_state() + self.async_write_ha_state() return self._is_opening = True self._listen_cover() self._requested_closing = False - self.schedule_update_ha_state() + self.async_write_ha_state() - def open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Open the cover tilt.""" if self._tilt_position in (100, None): return @@ -175,7 +176,7 @@ class DemoCover(CoverDevice): self._listen_cover_tilt() self._requested_closing_tilt = False - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) self._set_position = round(position, -1) @@ -185,7 +186,7 @@ class DemoCover(CoverDevice): self._listen_cover() self._requested_closing = position < self._position - def set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs): """Move the cover til to a specific position.""" tilt_position = kwargs.get(ATTR_TILT_POSITION) self._set_tilt_position = round(tilt_position, -1) @@ -195,7 +196,7 @@ class DemoCover(CoverDevice): self._listen_cover_tilt() self._requested_closing_tilt = tilt_position < self._tilt_position - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the cover.""" self._is_closing = False self._is_opening = False @@ -206,7 +207,7 @@ class DemoCover(CoverDevice): self._unsub_listener_cover = None self._set_position = None - def stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs): """Stop the cover tilt.""" if self._tilt_position is None: return @@ -216,14 +217,15 @@ class DemoCover(CoverDevice): self._unsub_listener_cover_tilt = None self._set_tilt_position = None + @callback def _listen_cover(self): """Listen for changes in cover.""" if self._unsub_listener_cover is None: - self._unsub_listener_cover = track_utc_time_change( + self._unsub_listener_cover = async_track_utc_time_change( self.hass, self._time_changed_cover ) - def _time_changed_cover(self, now): + async def _time_changed_cover(self, now): """Track time changes.""" if self._requested_closing: self._position -= 10 @@ -231,20 +233,20 @@ class DemoCover(CoverDevice): self._position += 10 if self._position in (100, 0, self._set_position): - self.stop_cover() + await self.async_stop_cover() self._closed = self.current_cover_position <= 0 + self.async_write_ha_state() - self.schedule_update_ha_state() - + @callback def _listen_cover_tilt(self): """Listen for changes in cover tilt.""" if self._unsub_listener_cover_tilt is None: - self._unsub_listener_cover_tilt = track_utc_time_change( + self._unsub_listener_cover_tilt = async_track_utc_time_change( self.hass, self._time_changed_cover_tilt ) - def _time_changed_cover_tilt(self, now): + async def _time_changed_cover_tilt(self, now): """Track time changes.""" if self._requested_closing_tilt: self._tilt_position -= 10 @@ -252,6 +254,6 @@ class DemoCover(CoverDevice): self._tilt_position += 10 if self._tilt_position in (100, 0, self._set_tilt_position): - self.stop_cover_tilt() + await self.async_stop_cover_tilt() - self.schedule_update_ha_state() + self.async_write_ha_state() diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index 77030623c9d..ce9c5cc0ea6 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -71,7 +71,7 @@ class DemoMailbox(Mailbox): reverse=True, ) - def async_delete(self, msgid): + async def async_delete(self, msgid): """Delete the specified messages.""" if msgid in self._messages: _LOGGER.info("Deleting: %s", msgid) diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py new file mode 100644 index 00000000000..afee8d5d175 --- /dev/null +++ b/homeassistant/components/derivative/__init__.py @@ -0,0 +1 @@ +"""The derivative component.""" diff --git a/homeassistant/components/derivative/manifest.json b/homeassistant/components/derivative/manifest.json new file mode 100644 index 00000000000..ae7eb4234b0 --- /dev/null +++ b/homeassistant/components/derivative/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "derivative", + "name": "Derivative", + "documentation": "https://www.home-assistant.io/integrations/derivative", + "requirements": [], + "dependencies": [], + "codeowners": [ + "@afaucogney" + ] +} \ No newline at end of file diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py new file mode 100644 index 00000000000..5e68b268685 --- /dev/null +++ b/homeassistant/components/derivative/sensor.py @@ -0,0 +1,209 @@ +"""Numeric derivative of data coming from a source sensor over time.""" +from decimal import Decimal, DecimalException +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_SOURCE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import RestoreEntity + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + +ATTR_SOURCE_ID = "source" + +CONF_ROUND_DIGITS = "round" +CONF_UNIT_PREFIX = "unit_prefix" +CONF_UNIT_TIME = "unit_time" +CONF_UNIT = "unit" +CONF_TIME_WINDOW = "time_window" + +# SI Metric prefixes +UNIT_PREFIXES = { + None: 1, + "n": 1e-9, + "µ": 1e-6, + "m": 1e-3, + "k": 1e3, + "M": 1e6, + "G": 1e9, + "T": 1e12, +} + +# SI Time prefixes +UNIT_TIME = {"s": 1, "min": 60, "h": 60 * 60, "d": 24 * 60 * 60} + +ICON = "mdi:chart-line" + +DEFAULT_ROUND = 3 +DEFAULT_TIME_WINDOW = 0 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_SOURCE): cv.entity_id, + vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int), + vol.Optional(CONF_UNIT_PREFIX, default=None): vol.In(UNIT_PREFIXES), + vol.Optional(CONF_UNIT_TIME, default="h"): vol.In(UNIT_TIME), + vol.Optional(CONF_UNIT): cv.string, + vol.Optional(CONF_TIME_WINDOW, default=DEFAULT_TIME_WINDOW): cv.time_period, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the derivative sensor.""" + derivative = DerivativeSensor( + source_entity=config[CONF_SOURCE], + name=config.get(CONF_NAME), + round_digits=config[CONF_ROUND_DIGITS], + unit_prefix=config[CONF_UNIT_PREFIX], + unit_time=config[CONF_UNIT_TIME], + unit_of_measurement=config.get(CONF_UNIT), + time_window=config[CONF_TIME_WINDOW], + ) + + async_add_entities([derivative]) + + +class DerivativeSensor(RestoreEntity): + """Representation of an derivative sensor.""" + + def __init__( + self, + source_entity, + name, + round_digits, + unit_prefix, + unit_time, + unit_of_measurement, + time_window, + ): + """Initialize the derivative sensor.""" + self._sensor_source_id = source_entity + self._round_digits = round_digits + self._state = 0 + self._state_list = [] # List of tuples with (timestamp, sensor_value) + + self._name = name if name is not None else f"{source_entity} derivative" + + if unit_of_measurement is None: + final_unit_prefix = "" if unit_prefix is None else unit_prefix + self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}" + # we postpone the definition of unit_of_measurement to later + self._unit_of_measurement = None + else: + self._unit_of_measurement = unit_of_measurement + + self._unit_prefix = UNIT_PREFIXES[unit_prefix] + self._unit_time = UNIT_TIME[unit_time] + self._time_window = time_window.total_seconds() + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state is not None: + try: + self._state = Decimal(state.state) + except SyntaxError as err: + _LOGGER.warning("Could not restore last state: %s", err) + + @callback + def calc_derivative(entity, old_state, new_state): + """Handle the sensor state changes.""" + if ( + old_state is None + or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ): + return + + now = new_state.last_updated + # Filter out the tuples that are older than (and outside of the) `time_window` + self._state_list = [ + (timestamp, state) + for timestamp, state in self._state_list + if (now - timestamp).total_seconds() < self._time_window + ] + # It can happen that the list is now empty, in that case + # we use the old_state, because we cannot do anything better. + if len(self._state_list) == 0: + self._state_list.append((old_state.last_updated, old_state.state)) + self._state_list.append((new_state.last_updated, new_state.state)) + + if self._unit_of_measurement is None: + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._unit_of_measurement = self._unit_template.format( + "" if unit is None else unit + ) + + try: + # derivative of previous measures. + last_time, last_value = self._state_list[-1] + first_time, first_value = self._state_list[0] + + elapsed_time = (last_time - first_time).total_seconds() + delta_value = Decimal(last_value) - Decimal(first_value) + derivative = ( + delta_value + / Decimal(elapsed_time) + / Decimal(self._unit_prefix) + * Decimal(self._unit_time) + ) + assert isinstance(derivative, Decimal) + except ValueError as err: + _LOGGER.warning("While calculating derivative: %s", err) + except DecimalException as err: + _LOGGER.warning( + "Invalid state (%s > %s): %s", old_state.state, new_state.state, err + ) + except AssertionError as err: + _LOGGER.error("Could not calculate derivative: %s", err) + else: + self._state = derivative + self.async_schedule_update_ha_state() + + async_track_state_change(self.hass, self._sensor_source_id, calc_derivative) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return round(self._state, self._round_digits) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit_of_measurement + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + state_attr = {ATTR_SOURCE_ID: self._sensor_source_id} + return state_attr + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return ICON diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 7d84eb921e9..f6bb74edbec 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -24,7 +24,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -121,6 +121,7 @@ async def async_call_action_from_config( ) +@callback def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index af6abf544c6..d7986fbb5b4 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -113,29 +113,27 @@ async def async_setup(hass, config): return None return next_setting - LIGHT_TRANSITION_TIME * len(light_ids) - def async_turn_on_before_sunset(light_id): + async def async_turn_on_before_sunset(light_id): """Turn on lights.""" if not anyone_home() or light.is_on(light_id): return - hass.async_create_task( - hass.services.async_call( - DOMAIN_LIGHT, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: light_id, - ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, - ATTR_PROFILE: light_profile, - }, - ) + await hass.services.async_call( + DOMAIN_LIGHT, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: light_id, + ATTR_TRANSITION: LIGHT_TRANSITION_TIME.seconds, + ATTR_PROFILE: light_profile, + }, ) + @callback def async_turn_on_factory(light_id): """Generate turn on callbacks as factory.""" - @callback - def async_turn_on_light(now): + async def async_turn_on_light(now): """Turn on specific light.""" - async_turn_on_before_sunset(light_id) + await async_turn_on_before_sunset(light_id) return async_turn_on_light diff --git a/homeassistant/components/device_tracker/.translations/zh-Hans.json b/homeassistant/components/device_tracker/.translations/zh-Hans.json new file mode 100644 index 00000000000..456e09ebf0e --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u5728\u5bb6", + "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 7b42554b4c1..c66bb621ad4 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -53,11 +53,14 @@ SOURCE_TYPES = ( NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( None, - vol.Schema( - { - vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - } + vol.All( + cv.deprecated(CONF_AWAY_HIDE, invalidation_version="0.107.0"), + vol.Schema( + { + vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + } + ), ), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 9bdfc12db39..9c102bfa745 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -65,6 +65,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: @@ -76,6 +77,7 @@ def async_condition_from_config( else: state = STATE_NOT_HOME + @callback def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" return condition.state(hass, config[ATTR_ENTITY_ID], state) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index b7d529f18ac..da3c945bc86 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -491,34 +491,25 @@ class DeviceScanner: """Scan for devices.""" raise NotImplementedError() - def async_scan_devices(self) -> Any: - """Scan for devices. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.scan_devices) + async def async_scan_devices(self) -> Any: + """Scan for devices.""" + return await self.hass.async_add_job(self.scan_devices) def get_device_name(self, device: str) -> str: """Get the name of a device.""" raise NotImplementedError() - def async_get_device_name(self, device: str) -> Any: - """Get the name of a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.get_device_name, device) + async def async_get_device_name(self, device: str) -> Any: + """Get the name of a device.""" + return await self.hass.async_add_job(self.get_device_name, device) def get_extra_attributes(self, device: str) -> dict: """Get the extra attributes of a device.""" raise NotImplementedError() - def async_get_extra_attributes(self, device: str) -> Any: - """Get the extra attributes of a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.get_extra_attributes, device) + async def async_get_extra_attributes(self, device: str) -> Any: + """Get the extra attributes of a device.""" + return await self.hass.async_add_job(self.get_extra_attributes, device) async def async_load_config( diff --git a/homeassistant/components/dialogflow/.translations/de.json b/homeassistant/components/dialogflow/.translations/de.json index f585799391e..1dbf1fa0c8a 100644 --- a/homeassistant/components/dialogflow/.translations/de.json +++ b/homeassistant/components/dialogflow/.translations/de.json @@ -5,11 +5,11 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen finden Sie in der [Dokumentation]({docs_url})." + "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen findest du in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "M\u00f6chten Sie Dialogflow wirklich einrichten?", + "description": "M\u00f6chtest du Dialogflow wirklich einrichten?", "title": "Dialogflow Webhook einrichten" } }, diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 965782d1228..1e29d066f2d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -75,7 +75,6 @@ SERVICE_HANDLERS = { "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), "denonavr": ("media_player", "denonavr"), - "samsung_tv": ("media_player", "samsungtv"), "frontier_silicon": ("media_player", "frontier_silicon"), "openhome": ("media_player", "openhome"), "harmony": ("remote", "harmony"), diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 28843aacbe4..fa6b60d0c19 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -77,7 +77,7 @@ HOME_ASSISTANT_UPNP_CLASS_MAPPING = { MEDIA_TYPE_EPISODE: "object.item.videoItem", MEDIA_TYPE_CHANNEL: "object.item.videoItem", MEDIA_TYPE_IMAGE: "object.item.imageItem", - MEDIA_TYPE_PLAYLIST: "object.item.playlist", + MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", } UPNP_CLASS_DEFAULT = "object.item" HOME_ASSISTANT_UPNP_MIME_TYPE_MAPPING = { diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 680ee1354eb..d82e27f0f9a 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -6,6 +6,7 @@ from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView +from homeassistant.components.logbook import log_entry from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -296,4 +297,6 @@ class DoorBirdRequestView(HomeAssistantView): hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) + log_entry(hass, "Doorbird {}".format(event), "event was fired.", DOMAIN) + return web.Response(status=200, text="OK") diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 97b54adb4ab..1703557cc9e 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,6 +3,6 @@ "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.0.8"], - "dependencies": ["http"], + "dependencies": ["http", "logbook"], "codeowners": ["@oblogic7"] } diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 8f607dc299e..743bad148f0 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.12"], + "requirements": ["dsmr_parser==0.18"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 1fdbed0d204..2d41e6b828a 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -530,6 +530,8 @@ class DysonPureCoolDevice(FanEntity): @property def carbon_filter(self): """Return the carbon filter state.""" + if self._device.state.carbon_filter_state == "INV": + return self._device.state.carbon_filter_state return int(self._device.state.carbon_filter_state) @property diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 915c6aa3b79..f6c0c187c8c 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -2,7 +2,7 @@ "domain": "dyson", "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", - "requirements": ["libpurecool==0.6.0"], + "requirements": ["libpurecool==0.6.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@etheralm"] } diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json index 33d493f6db0..818783813fe 100644 --- a/homeassistant/components/ecobee/.translations/de.json +++ b/homeassistant/components/ecobee/.translations/de.json @@ -16,7 +16,7 @@ "data": { "api_key": "API Key" }, - "description": "Bitte geben Sie den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", + "description": "Bitte gib den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", "title": "ecobee API-Schl\u00fcssel" } }, diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index f7a24886b84..a4062905eaa 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -7,11 +7,6 @@ from homeassistant.components.binary_sensor import ( from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up ecobee binary sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up ecobee binary (occupancy) sensors.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 5915e64334f..6746192b840 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -156,11 +156,6 @@ SUPPORT_FLAGS = ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up ecobee thermostat.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ecobee thermostat.""" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 37201ec2121..c2c34d148e3 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -16,11 +16,6 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up ecobee sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up ecobee (temperature and humidity) sensors.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index a571e854f73..b8d23b3e379 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -23,11 +23,6 @@ from .const import ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up the ecobee weather platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ecobee weather platform.""" data = hass.data[DOMAIN] diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 157e4f1fe8c..4f02d6fdde0 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -64,12 +64,6 @@ class EgardiaBinarySensor(BinarySensorDevice): """Whether the device is switched on.""" return self._state == STATE_ON - @property - def hidden(self): - """Whether the device is hidden by default.""" - # these type of sensors are probably mainly used for automations - return True - @property def device_class(self): """Return the device class.""" diff --git a/homeassistant/components/elgato/.translations/de.json b/homeassistant/components/elgato/.translations/de.json index dd6344916de..f5bca5e8416 100644 --- a/homeassistant/components/elgato/.translations/de.json +++ b/homeassistant/components/elgato/.translations/de.json @@ -14,11 +14,11 @@ "host": "Host oder IP-Adresse", "port": "Port-Nummer" }, - "description": "Richten Sie Ihr Elgato Key Light f\u00fcr die Integration mit Home Assistant ein.", - "title": "Verkn\u00fcpfen Sie Ihr Elgato Key Light" + "description": "Richten dein Elgato Key Light f\u00fcr die Integration mit Home Assistant ein.", + "title": "Verkn\u00fcpfe dein Elgato Key Light" }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?", "title": "Elgato Key Light Ger\u00e4t entdeckt" } }, diff --git a/homeassistant/components/elgato/.translations/lb.json b/homeassistant/components/elgato/.translations/lb.json index d53eea87c4c..e46fc4364d2 100644 --- a/homeassistant/components/elgato/.translations/lb.json +++ b/homeassistant/components/elgato/.translations/lb.json @@ -18,7 +18,7 @@ "title": "\u00c4ren Elgato Key Light verbannen" }, "zeroconf_confirm": { - "description": "W\u00ebllt dir den Elgato Key Light mat der Seriennummer `{serial_number}` am 'Home Assistant dob\u00e4isetzen?", + "description": "W\u00ebllt dir den Elgato Key Light mat der Seriennummer `{serial_number}` am Home Assistant dob\u00e4isetzen?", "title": "Entdeckten Elgato Key Light Apparat" } }, diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json index 2b5fb72c507..454e6e78d84 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -19,9 +19,9 @@ }, "zeroconf_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", - "title": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" } }, - "title": "\u041e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c Elgado Key Light" + "title": "Elgado Key Light" } } \ No newline at end of file diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 57f781deceb..b4b05fd6c12 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -36,8 +36,6 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_AUTO_HIDE = "auto_hide" - MEDIA_TYPE_TRAILER = "trailer" MEDIA_TYPE_GENERIC_VIDEO = "video" @@ -45,7 +43,6 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 8096 DEFAULT_SSL_PORT = 8920 DEFAULT_SSL = False -DEFAULT_AUTO_HIDE = False _LOGGER = logging.getLogger(__name__) @@ -61,7 +58,6 @@ SUPPORT_EMBY = ( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, @@ -76,7 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= key = config.get(CONF_API_KEY) port = config.get(CONF_PORT) ssl = config.get(CONF_SSL) - auto_hide = config.get(CONF_AUTO_HIDE) if port is None: port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT @@ -109,7 +104,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= active_emby_devices[dev_id] = add _LOGGER.debug("Showing %s, item: %s", dev_id, add) add.set_available(True) - add.set_hidden(False) if new_devices: _LOGGER.debug("Adding new devices: %s", new_devices) @@ -123,8 +117,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= inactive_emby_devices[data] = rem _LOGGER.debug("Inactive %s, item: %s", data, rem) rem.set_available(False) - if auto_hide: - rem.set_hidden(True) @callback def start_emby(event): @@ -152,7 +144,6 @@ class EmbyDevice(MediaPlayerDevice): self.device_id = device_id self.device = self.emby.devices[self.device_id] - self._hidden = False self._available = True self.media_status_last_position = None @@ -177,15 +168,6 @@ class EmbyDevice(MediaPlayerDevice): self.async_schedule_update_ha_state() - @property - def hidden(self): - """Return True if entity should be hidden from UI.""" - return self._hidden - - def set_hidden(self, value): - """Set hidden property.""" - self._hidden = value - @property def available(self): """Return True if entity is available.""" @@ -327,44 +309,26 @@ class EmbyDevice(MediaPlayerDevice): return SUPPORT_EMBY return None - def async_media_play(self): - """Play media. + async def async_media_play(self): + """Play media.""" + await self.device.media_play() - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_play() + async def async_media_pause(self): + """Pause the media player.""" + await self.device.media_pause() - def async_media_pause(self): - """Pause the media player. + async def async_media_stop(self): + """Stop the media player.""" + await self.device.media_stop() - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_pause() + async def async_media_next_track(self): + """Send next track command.""" + await self.device.media_next() - def async_media_stop(self): - """Stop the media player. + async def async_media_previous_track(self): + """Send next track command.""" + await self.device.media_previous() - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_stop() - - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_next() - - def async_media_previous_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_previous() - - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.device.media_seek(position) + async def async_media_seek(self, position): + """Send seek command.""" + await self.device.media_seek(position) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b054d69e7a4..56e76b1d499 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -351,8 +351,9 @@ class HueOneLightChangeView(HomeAssistantView): if HUE_API_STATE_BRI in request_json: if entity.domain == light.DOMAIN: - parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 - if not entity_features & SUPPORT_BRIGHTNESS: + if entity_features & SUPPORT_BRIGHTNESS: + parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 + else: parsed[STATE_BRIGHTNESS] = None elif entity.domain == scene.DOMAIN: @@ -687,26 +688,25 @@ def entity_to_json(config, entity): retval["state"].update( {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} ) - elif ( - entity_features - & ( - SUPPORT_BRIGHTNESS - | SUPPORT_SET_POSITION - | SUPPORT_SET_SPEED - | SUPPORT_VOLUME_SET - | SUPPORT_TARGET_TEMPERATURE - ) - ) or entity.domain == script.DOMAIN: + elif entity_features & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ): # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) else: - # On/off light (Zigbee Device ID: 0x0000) - # Supports groups, scenes and on/off control - retval["type"] = "On/off light" - retval["modelid"] = "HASS321" + # Dimmable light (Zigbee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + # Reports fixed brightness for compatibility with Alexa. + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index fff85572477..c3c0302dbc3 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "requirements": ["aiohttp_cors==0.7.0"], "dependencies": [], - "codeowners": ["@NobleKangaroo"], + "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 8b5925fd12f..39b8d40737d 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -3,7 +3,7 @@ "name": "Emulated Roku", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emulated_roku", - "requirements": ["emulated_roku==0.2.0"], + "requirements": ["emulated_roku==0.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index fa243f09bbb..a9d97dc6271 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.0.31"], + "requirements": ["env_canada==0.0.34"], "dependencies": [], "codeowners": ["@michaeldavie"] } diff --git a/homeassistant/components/esphome/.translations/de.json b/homeassistant/components/esphome/.translations/de.json index 80111f34984..c9852632cdd 100644 --- a/homeassistant/components/esphome/.translations/de.json +++ b/homeassistant/components/esphome/.translations/de.json @@ -4,9 +4,9 @@ "already_configured": "ESP ist bereits konfiguriert" }, "error": { - "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achten Sie darauf, dass Ihre YAML-Datei eine Zeile 'api:' enth\u00e4lt.", + "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", "invalid_password": "Ung\u00fcltiges Passwort!", - "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, legen Sie eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", "step": { @@ -14,7 +14,7 @@ "data": { "password": "Passwort" }, - "description": "Bitte geben Sie das Passwort der ESPHome-Konfiguration f\u00fcr {name} ein:", + "description": "Bitte gebe das Passwort der ESPHome-Konfiguration f\u00fcr {name} ein:", "title": "Passwort eingeben" }, "discovery_confirm": { @@ -26,7 +26,7 @@ "host": "Host", "port": "Port" }, - "description": "Bitte geben Sie die Verbindungseinstellungen Ihres [ESPHome](https://esphomelib.com/)-Knotens ein.", + "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome](https://esphomelib.com/)-Knotens ein.", "title": "ESPHome" } }, diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 882b67823ba..8302d8b38c2 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -18,7 +18,7 @@ "title": "Passwuert aginn" }, "discovery_confirm": { - "description": "W\u00ebllt dir den ESPHome Provider `{name}` am 'Home Assistant dob\u00e4isetzen?", + "description": "W\u00ebllt dir den ESPHome Provider `{name}` am Home Assistant dob\u00e4isetzen?", "title": "Entdeckten ESPHome Provider" }, "user": { diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 48f1aea2c2d..c56760e952f 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -21,6 +21,7 @@ from aioesphomeapi import ( import attr from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType @@ -71,6 +72,7 @@ class RuntimeEntryData: loaded_platforms = attr.ib(type=Set[str], factory=set) platform_load_lock = attr.ib(type=asyncio.Lock, factory=asyncio.Lock) + @callback def async_update_entity( self, hass: HomeAssistantType, component_key: str, key: int ) -> None: @@ -80,6 +82,7 @@ class RuntimeEntryData: ) async_dispatcher_send(hass, signal) + @callback def async_remove_entity( self, hass: HomeAssistantType, component_key: str, key: int ) -> None: @@ -120,11 +123,13 @@ class RuntimeEntryData: signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal, infos) + @callback def async_update_state(self, hass: HomeAssistantType, state: EntityState) -> None: """Distribute an update of state information to all platforms.""" signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id) async_dispatcher_send(hass, signal, state) + @callback def async_update_device_state(self, hass: HomeAssistantType) -> None: """Distribute an update of a core device state like availability.""" signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 3d903e86e30..949471d64d0 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -1,8 +1,8 @@ """Support for (EMEA/EU-based) Honeywell TCC climate systems. -Such systems include evohome (multi-zone), and Round Thermostat (single zone). +Such systems include evohome, Round Thermostat, and others. """ -from datetime import datetime, timedelta +from datetime import datetime as dt, timedelta import logging import re from typing import Any, Dict, Optional, Tuple @@ -13,6 +13,7 @@ import evohomeasync2 import voluptuous as vol from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -24,7 +25,12 @@ from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util @@ -58,16 +64,44 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +ATTR_SYSTEM_MODE = "mode" +ATTR_DURATION_DAYS = "period" +ATTR_DURATION_HOURS = "duration" -def _local_dt_to_aware(dt_naive: datetime) -> datetime: - dt_aware = dt_util.now() + (dt_naive - datetime.now()) +ATTR_ZONE_TEMP = "setpoint" +ATTR_DURATION_UNTIL = "duration" + +SVC_REFRESH_SYSTEM = "refresh_system" +SVC_SET_SYSTEM_MODE = "set_system_mode" +SVC_RESET_SYSTEM = "reset_system" +SVC_SET_ZONE_OVERRIDE = "set_zone_override" +SVC_RESET_ZONE_OVERRIDE = "clear_zone_override" + + +RESET_ZONE_OVERRIDE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_id}) +SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ZONE_TEMP): vol.All( + vol.Coerce(float), vol.Range(min=4.0, max=35.0) + ), + vol.Optional(ATTR_DURATION_UNTIL): vol.All( + cv.time_period, vol.Range(min=timedelta(days=0), max=timedelta(days=1)), + ), + } +) +# system mode schemas are built dynamically, below + + +def _local_dt_to_aware(dt_naive: dt) -> dt: + dt_aware = dt_util.now() + (dt_naive - dt.now()) if dt_aware.microsecond >= 500000: dt_aware += timedelta(seconds=1) return dt_aware.replace(microsecond=0) -def _dt_to_local_naive(dt_aware: datetime) -> datetime: - dt_naive = datetime.now() + (dt_aware - dt_util.now()) +def _dt_to_local_naive(dt_aware: dt) -> dt: + dt_naive = dt.now() + (dt_aware - dt_util.now()) if dt_naive.microsecond >= 500000: dt_naive += timedelta(seconds=1) return dt_naive.replace(microsecond=0) @@ -114,7 +148,7 @@ def _handle_exception(err) -> bool: return False except aiohttp.ClientConnectionError: - # this appears to be common with Honeywell's servers + # this appears to be a common occurance with the vendor's servers _LOGGER.warning( "Unable to connect with the vendor's server. " "Check your network and the vendor's service status page. " @@ -143,7 +177,7 @@ def _handle_exception(err) -> bool: async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: - """Create a (EMEA/EU-based) Honeywell evohome system.""" + """Create a (EMEA/EU-based) Honeywell TCC system.""" async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]: app_storage = await store.async_load() @@ -209,7 +243,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await broker.save_auth_tokens() - await broker.update() # get initial state + await broker.async_update() # get initial state hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: @@ -218,12 +252,133 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) hass.helpers.event.async_track_time_interval( - broker.update, config[DOMAIN][CONF_SCAN_INTERVAL] + broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL] ) + setup_service_functions(hass, broker) + return True +@callback +def setup_service_functions(hass: HomeAssistantType, broker): + """Set up the service handlers for the system/zone operating modes. + + Not all Honeywell TCC-compatible systems support all operating modes. In addition, + each mode will require any of four distinct service schemas. This has to be + enumerated before registering the approperiate handlers. + + It appears that all TCC-compatible systems support the same three zones modes. + """ + + @verify_domain_control(hass, DOMAIN) + async def force_refresh(call) -> None: + """Obtain the latest state data via the vendor's RESTful API.""" + await broker.async_update() + + @verify_domain_control(hass, DOMAIN) + async def set_system_mode(call) -> None: + """Set the system mode.""" + payload = { + "unique_id": broker.tcs.systemId, + "service": call.service, + "data": call.data, + } + async_dispatcher_send(hass, DOMAIN, payload) + + @verify_domain_control(hass, DOMAIN) + async def set_zone_override(call) -> None: + """Set the zone override (setpoint).""" + entity_id = call.data[ATTR_ENTITY_ID] + + registry = await hass.helpers.entity_registry.async_get_registry() + registry_entry = registry.async_get(entity_id) + + if registry_entry is None or registry_entry.platform != DOMAIN: + raise ValueError(f"'{entity_id}' is not a known {DOMAIN} entity") + + if registry_entry.domain != "climate": + raise ValueError(f"'{entity_id}' is not an {DOMAIN} controller/zone") + + payload = { + "unique_id": registry_entry.unique_id, + "service": call.service, + "data": call.data, + } + + async_dispatcher_send(hass, DOMAIN, payload) + + hass.services.async_register(DOMAIN, SVC_REFRESH_SYSTEM, force_refresh) + + # Enumerate which operating modes are supported by this system + modes = broker.config["allowedSystemModes"] + + # Not all systems support "AutoWithReset": register this handler only if required + if [m["systemMode"] for m in modes if m["systemMode"] == "AutoWithReset"]: + hass.services.async_register(DOMAIN, SVC_RESET_SYSTEM, set_system_mode) + + system_mode_schemas = [] + modes = [m for m in modes if m["systemMode"] != "AutoWithReset"] + + # Permanent-only modes will use this schema + perm_modes = [m["systemMode"] for m in modes if not m["canBeTemporary"]] + if perm_modes: # any of: "Auto", "HeatingOff": permanent only + schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)}) + system_mode_schemas.append(schema) + + modes = [m for m in modes if m["canBeTemporary"]] + + # These modes are set for a number of hours (or indefinitely): use this schema + temp_modes = [m["systemMode"] for m in modes if m["timingMode"] == "Duration"] + if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours + schema = vol.Schema( + { + vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes), + vol.Optional(ATTR_DURATION_HOURS): vol.All( + cv.time_period, + vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)), + ), + } + ) + system_mode_schemas.append(schema) + + # These modes are set for a number of days (or indefinitely): use this schema + temp_modes = [m["systemMode"] for m in modes if m["timingMode"] == "Period"] + if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days + schema = vol.Schema( + { + vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes), + vol.Optional(ATTR_DURATION_DAYS): vol.All( + cv.time_period, + vol.Range(min=timedelta(days=1), max=timedelta(days=99)), + ), + } + ) + system_mode_schemas.append(schema) + + if system_mode_schemas: + hass.services.async_register( + DOMAIN, + SVC_SET_SYSTEM_MODE, + set_system_mode, + schema=vol.Any(*system_mode_schemas), + ) + + # The zone modes are consistent across all systems and use the same schema + hass.services.async_register( + DOMAIN, + SVC_RESET_ZONE_OVERRIDE, + set_zone_override, + schema=RESET_ZONE_OVERRIDE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SVC_SET_ZONE_OVERRIDE, + set_zone_override, + schema=SET_ZONE_OVERRIDE_SCHEMA, + ) + + class EvoBroker: """Container for evohome client and data.""" @@ -238,7 +393,7 @@ class EvoBroker: loc_idx = params[CONF_LOCATION_IDX] self.config = client.installation_info[loc_idx][GWS][0][TCS][0] self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] - self.temps = None + self.temps = {} async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" @@ -260,6 +415,19 @@ class EvoBroker: await self._store.async_save(app_storage) + async def call_client_api(self, api_function, refresh=True) -> Any: + """Call a client API.""" + try: + result = await api_function + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + if not _handle_exception(err): + return + + if refresh: + self.hass.helpers.event.async_call_later(1, self.async_update()) + + return result + async def _update_v1(self, *args, **kwargs) -> None: """Get the latest high-precision temperatures of the default Location.""" @@ -311,15 +479,15 @@ class EvoBroker: except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: _handle_exception(err) else: - self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + async_dispatcher_send(self.hass, DOMAIN) _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) if access_token != self.client.access_token: await self.save_auth_tokens() - async def update(self, *args, **kwargs) -> None: - """Get the latest state data of an entire evohome Location. + async def async_update(self, *args, **kwargs) -> None: + """Get the latest state data of an entire Honeywell TCC Location. This includes state data for a Controller and all its child devices, such as the operating mode of the Controller and the current temp of its children (e.g. @@ -331,7 +499,7 @@ class EvoBroker: await self._update_v1() # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + async_dispatcher_send(self.hass, DOMAIN) class EvoDevice(Entity): @@ -351,9 +519,25 @@ class EvoDevice(Entity): self._supported_features = None self._device_state_attrs = {} - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) + async def async_refresh(self, payload: Optional[dict] = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + if payload["unique_id"] != self._unique_id: + return + if payload["service"] in [SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE]: + await self.async_zone_svc_request(payload["service"], payload["data"]) + return + await self.async_tcs_svc_request(payload["service"], payload["data"]) + + async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + """Process a service request (system mode) for a controller.""" + raise NotImplementedError + + async def async_zone_svc_request(self, service: dict, data: dict) -> None: + """Process a service request (setpoint override) for a zone.""" + raise NotImplementedError @property def should_poll(self) -> bool: @@ -367,12 +551,12 @@ class EvoDevice(Entity): @property def name(self) -> str: - """Return the name of the Evohome entity.""" + """Return the name of the evohome entity.""" return self._name @property def device_state_attributes(self) -> Dict[str, Any]: - """Return the Evohome-specific state attributes.""" + """Return the evohome-specific state attributes.""" status = self._device_state_attrs if "systemModeStatus" in status: convert_until(status["systemModeStatus"], "timeUntil") @@ -395,7 +579,7 @@ class EvoDevice(Entity): async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" - self.hass.helpers.dispatcher.async_dispatcher_connect(DOMAIN, self._refresh) + async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh) @property def precision(self) -> float: @@ -407,18 +591,6 @@ class EvoDevice(Entity): """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - async def _call_client_api(self, api_function, refresh=True) -> Any: - try: - result = await api_function - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - if not _handle_exception(err): - return - - if refresh is True: - self.hass.helpers.event.async_call_later(1, self._evo_broker.update()) - - return result - class EvoChild(EvoDevice): """Base for any evohome child. @@ -491,18 +663,19 @@ class EvoChild(EvoDevice): except IndexError: self._setpoints = {} _LOGGER.warning( - "Failed to get setpoints - please report as an issue", exc_info=True + "Failed to get setpoints, report as an issue if this error persists", + exc_info=True, ) return self._setpoints async def _update_schedule(self) -> None: - """Get the latest schedule.""" + """Get the latest schedule, if any.""" if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]: if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: return # avoid unnecessary I/O - there's nothing to update - self._schedule = await self._call_client_api( + self._schedule = await self._evo_broker.call_client_api( self._evo_device.schedule(), refresh=False ) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 3da11bc8087..b7f6e965a8f 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,4 +1,5 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" +from datetime import datetime as dt import logging from typing import List, Optional @@ -21,7 +22,18 @@ from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoChild, EvoDevice +from . import ( + ATTR_DURATION_DAYS, + ATTR_DURATION_HOURS, + ATTR_DURATION_UNTIL, + ATTR_SYSTEM_MODE, + ATTR_ZONE_TEMP, + CONF_LOCATION_IDX, + SVC_RESET_ZONE_OVERRIDE, + SVC_SET_SYSTEM_MODE, + EvoChild, + EvoDevice, +) from .const import ( DOMAIN, EVO_AUTO, @@ -81,37 +93,37 @@ async def async_setup_platform( broker.params[CONF_LOCATION_IDX], ) - # special case of RoundModulation/RoundWireless as a single zone system - if len(broker.tcs.zones) == 1 and list(broker.tcs.zones.keys())[0] == "Thermostat": - zone = list(broker.tcs.zones.values())[0] - _LOGGER.debug( - "Found the Thermostat (%s), id=%s, name=%s", - zone.modelType, - zone.zoneId, - zone.name, - ) - - async_add_entities([EvoThermostat(broker, zone)], update_before_add=True) - return - controller = EvoController(broker, broker.tcs) zones = [] for zone in broker.tcs.zones.values(): - _LOGGER.debug( - "Found a %s (%s), id=%s, name=%s", - zone.zoneType, - zone.modelType, - zone.zoneId, - zone.name, - ) - zones.append(EvoZone(broker, zone)) + if zone.modelType == "HeatingZone" or zone.zoneType == "Thermostat": + _LOGGER.debug( + "Adding: %s (%s), id=%s, name=%s", + zone.zoneType, + zone.modelType, + zone.zoneId, + zone.name, + ) + + new_entity = EvoZone(broker, zone) + zones.append(new_entity) + + else: + _LOGGER.warning( + "Ignoring: %s (%s), id=%s, name=%s: unknown/invalid zone type, " + "report as an issue if you feel this zone type should be supported", + zone.zoneType, + zone.modelType, + zone.zoneId, + zone.name, + ) async_add_entities([controller] + zones, update_before_add=True) class EvoClimateDevice(EvoDevice, ClimateDevice): - """Base for a Honeywell evohome Climate device.""" + """Base for an evohome Climate device.""" def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" @@ -119,10 +131,6 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): self._preset_modes = None - async def _set_tcs_mode(self, op_mode: str) -> None: - """Set a Controller to any of its native EVO_* operating modes.""" - await self._call_client_api(self._evo_tcs.set_status(op_mode)) - @property def hvac_modes(self) -> List[str]: """Return a list of available hvac operation modes.""" @@ -135,23 +143,50 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): class EvoZone(EvoChild, EvoClimateDevice): - """Base for a Honeywell evohome Zone.""" + """Base for a Honeywell TCC Zone.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize a Zone.""" + """Initialize a Honeywell TCC Zone.""" super().__init__(evo_broker, evo_device) self._unique_id = evo_device.zoneId self._name = evo_device.name self._icon = "mdi:radiator" - self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE - self._preset_modes = list(HA_PRESET_TO_EVO) if evo_broker.client_v1: self._precision = PRECISION_TENTHS else: self._precision = self._evo_device.setpointCapabilities["valueResolution"] + self._preset_modes = list(HA_PRESET_TO_EVO) + self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + + async def async_zone_svc_request(self, service: dict, data: dict) -> None: + """Process a service request (setpoint override) for a zone.""" + if service == SVC_RESET_ZONE_OVERRIDE: + await self._evo_broker.call_client_api( + self._evo_device.cancel_temp_override() + ) + return + + # otherwise it is SVC_SET_ZONE_OVERRIDE + temp = round(data[ATTR_ZONE_TEMP] * self.precision) / self.precision + temp = max(min(temp, self.max_temp), self.min_temp) + + if ATTR_DURATION_UNTIL in data: + duration = data[ATTR_DURATION_UNTIL] + if duration == 0: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + else: + until = dt.now() + data[ATTR_DURATION_UNTIL] + else: + until = None # indefinitely + + await self._evo_broker.call_client_api( + self._evo_device.set_temperature(temperature=temp, until=until) + ) + @property def hvac_mode(self) -> str: """Return the current operating mode of a Zone.""" @@ -183,9 +218,7 @@ class EvoZone(EvoChild, EvoClimateDevice): """Return the current preset mode, e.g., home, away, temp.""" if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - return EVO_PRESET_TO_HA.get( - self._evo_device.setpointStatus["setpointMode"], "follow" - ) + return EVO_PRESET_TO_HA.get(self._evo_device.setpointStatus["setpointMode"]) @property def min_temp(self) -> float: @@ -206,16 +239,16 @@ class EvoZone(EvoChild, EvoClimateDevice): async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] + until = kwargs.get("until") - if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: - await self._update_schedule() - until = parse_datetime(str(self.setpoints.get("next_sp_from"))) - elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: - until = parse_datetime(self._evo_device.setpointStatus["until"]) - else: # EVO_PERMOVER - until = None + if until is None: + if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: + until = parse_datetime(self._evo_device.setpointStatus["until"]) - await self._call_client_api( + await self._evo_broker.call_client_api( self._evo_device.set_temperature(temperature, until) ) @@ -237,18 +270,22 @@ class EvoZone(EvoChild, EvoClimateDevice): and 'Away', Zones to (by default) 12C. """ if hvac_mode == HVAC_MODE_OFF: - await self._call_client_api( + await self._evo_broker.call_client_api( self._evo_device.set_temperature(self.min_temp, until=None) ) else: # HVAC_MODE_HEAT - await self._call_client_api(self._evo_device.cancel_temp_override()) + await self._evo_broker.call_client_api( + self._evo_device.cancel_temp_override() + ) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set the preset mode; if None, then revert to following the schedule.""" evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) if evo_preset_mode == EVO_FOLLOW: - await self._call_client_api(self._evo_device.cancel_temp_override()) + await self._evo_broker.call_client_api( + self._evo_device.cancel_temp_override() + ) return temperature = self._evo_device.setpointStatus["targetHeatTemperature"] @@ -259,7 +296,7 @@ class EvoZone(EvoChild, EvoClimateDevice): else: # EVO_PERMOVER until = None - await self._call_client_api( + await self._evo_broker.call_client_api( self._evo_device.set_temperature(temperature, until) ) @@ -272,14 +309,17 @@ class EvoZone(EvoChild, EvoClimateDevice): class EvoController(EvoClimateDevice): - """Base for a Honeywell evohome Controller (hub). + """Base for a Honeywell TCC Controller/Location. - The Controller (aka TCS, temperature control system) is the parent of all - the child (CH/DHW) devices. It is also a Climate device. + The Controller (aka TCS, temperature control system) is the parent of all the child + (CH/DHW) devices. It is implemented as a Climate entity to expose the controller's + operating modes to HA. + + It is assumed there is only one TCS per location, and they are thus synonymous. """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize a evohome Controller (hub).""" + """Initialize a Honeywell TCC Controller/Location.""" super().__init__(evo_broker, evo_device) self._unique_id = evo_device.systemId @@ -287,8 +327,38 @@ class EvoController(EvoClimateDevice): self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._supported_features = SUPPORT_PRESET_MODE - self._preset_modes = list(HA_PRESET_TO_TCS) + + modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]] + self._preset_modes = [ + TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) + ] + self._supported_features = SUPPORT_PRESET_MODE if self._preset_modes else 0 + + async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + """Process a service request (system mode) for a controller. + + Data validation is not required, it will have been done upstream. + """ + if service == SVC_SET_SYSTEM_MODE: + mode = data[ATTR_SYSTEM_MODE] + else: # otherwise it is SVC_RESET_SYSTEM + mode = EVO_RESET + + if ATTR_DURATION_DAYS in data: + until = dt.combine(dt.now().date(), dt.min.time()) + until += data[ATTR_DURATION_DAYS] + + elif ATTR_DURATION_HOURS in data: + until = dt.now() + data[ATTR_DURATION_HOURS] + + else: + until = None + + await self._set_tcs_mode(mode, until=until) + + async def _set_tcs_mode(self, mode: str, until: Optional[dt] = None) -> None: + """Set a Controller to any of its native EVO_* operating modes.""" + await self._evo_broker.call_client_api(self._evo_tcs.set_status(mode)) @property def hvac_mode(self) -> str: @@ -346,58 +416,3 @@ class EvoController(EvoClimateDevice): attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) else: attrs[attr] = getattr(self._evo_tcs, attr) - - -class EvoThermostat(EvoZone): - """Base for a Honeywell Round Thermostat. - - These are implemented as a combined Controller/Zone. - """ - - def __init__(self, evo_broker, evo_device) -> None: - """Initialize the Thermostat.""" - super().__init__(evo_broker, evo_device) - - self._name = evo_broker.tcs.location.name - self._preset_modes = [PRESET_AWAY, PRESET_ECO] - - @property - def hvac_mode(self) -> str: - """Return the current operating mode.""" - if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: - return HVAC_MODE_OFF - - return super().hvac_mode - - @property - def preset_mode(self) -> Optional[str]: - """Return the current preset mode, e.g., home, away, temp.""" - if ( - self._evo_tcs.systemModeStatus["mode"] == EVO_AUTOECO - and self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW - ): - return PRESET_ECO - - return super().preset_mode - - async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode.""" - await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - - async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set the preset mode; if None, then revert to following the schedule.""" - if preset_mode in list(HA_PRESET_TO_TCS): - await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) - else: - await super().async_set_hvac_mode(preset_mode) - - async def async_update(self) -> None: - """Get the latest state data for the Thermostat.""" - await super().async_update() - - attrs = self._device_state_attrs - for attr in STATE_ATTRS_TCS: - if attr == "activeFaults": # self._evo_device also has "activeFaults" - attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) - else: - attrs[attr] = getattr(self._evo_tcs, attr) diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index 444671cf82a..eaa7048e53b 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -13,7 +13,7 @@ EVO_DAYOFF = "DayOff" EVO_CUSTOM = "Custom" EVO_HEATOFF = "HeatingOff" -# The Childs' operating mode is one of: +# The Children's operating mode is one of: EVO_FOLLOW = "FollowSchedule" # the operating mode is 'inherited' from the TCS EVO_TEMPOVER = "TemporaryOverride" EVO_PERMOVER = "PermanentOverride" diff --git a/homeassistant/components/evohome/services.yaml b/homeassistant/components/evohome/services.yaml new file mode 100644 index 00000000000..ebc859ed9e3 --- /dev/null +++ b/homeassistant/components/evohome/services.yaml @@ -0,0 +1,53 @@ +# Support for (EMEA/EU-based) Honeywell TCC climate systems. +# Describes the format for available services + +set_system_mode: + description: >- + Set the system mode, either indefinitely, or for a specified period of time, after + which it will revert to Auto. Not all systems support all modes. + fields: + mode: + description: 'One of: Auto, AutoWithEco, Away, DayOff, HeatingOff, or Custom.' + example: Away + period: + description: >- + A period of time in days; used only with Away, DayOff, or Custom. The system + will revert to Auto at midnight (up to 99 days, today is day 1). + example: '{"days": 28}' + duration: + description: The duration in hours; used only with AutoWithEco (up to 24 hours). + example: '{"hours": 18}' + +reset_system: + description: >- + Set the system to Auto mode and reset all the zones to follow their schedules. + Not all Evohome systems support this feature (i.e. AutoWithReset mode). + +refresh_system: + description: >- + Pull the latest data from the vendor's servers now, rather than waiting for the + next scheduled update. + +set_zone_override: + description: >- + Override a zone's setpoint, either indefinitely, or for a specified period of + time, after which it will revert to following its schedule. + fields: + entity_id: + description: The entity_id of the Evohome zone. + example: climate.bathroom + setpoint: + description: The temperature to be used instead of the scheduled setpoint. + example: 5.0 + duration: + description: >- + The zone will revert to its schedule after this time. If 0 the change is until + the next scheduled setpoint. + example: '{"minutes": 135}' + +clear_zone_override: + description: Set a zone to follow its schedule. + fields: + entity_id: + description: The entity_id of the zone. + example: climate.bathroom diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index e29dbb49af2..cc282534f1b 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,21 +34,20 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found the DHW Controller (%s), id: %s", + "Adding: DhwController (%s), id=%s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId, ) + new_entity = EvoDHW(broker, broker.tcs.hotwater) - evo_dhw = EvoDHW(broker, broker.tcs.hotwater) - - async_add_entities([evo_dhw], update_before_add=True) + async_add_entities([new_entity], update_before_add=True) class EvoDHW(EvoChild, WaterHeaterDevice): - """Base for a Honeywell evohome DHW controller (aka boiler).""" + """Base for a Honeywell TCC DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize a evohome DHW controller.""" + """Initialize an evohome DHW controller.""" super().__init__(evo_broker, evo_device) self._unique_id = evo_device.dhwId @@ -88,23 +87,27 @@ class EvoDHW(EvoChild, WaterHeaterDevice): Except for Auto, the mode is only until the next SetPoint. """ if operation_mode == STATE_AUTO: - await self._call_client_api(self._evo_device.set_dhw_auto()) + await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) else: await self._update_schedule() until = parse_datetime(str(self.setpoints.get("next_sp_from"))) if operation_mode == STATE_ON: - await self._call_client_api(self._evo_device.set_dhw_on(until)) + await self._evo_broker.call_client_api( + self._evo_device.set_dhw_on(until) + ) else: # STATE_OFF - await self._call_client_api(self._evo_device.set_dhw_off(until)) + await self._evo_broker.call_client_api( + self._evo_device.set_dhw_off(until) + ) async def async_turn_away_mode_on(self): """Turn away mode on.""" - await self._call_client_api(self._evo_device.set_dhw_off()) + await self._evo_broker.call_client_api(self._evo_device.set_dhw_off()) async def async_turn_away_mode_off(self): """Turn away mode off.""" - await self._call_client_api(self._evo_device.set_dhw_auto()) + await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) async def async_update(self) -> None: """Get the latest state data for a DHW controller.""" diff --git a/homeassistant/components/fan/.translations/zh-Hans.json b/homeassistant/components/fan/.translations/zh-Hans.json new file mode 100644 index 00000000000..f909bd8ac62 --- /dev/null +++ b/homeassistant/components/fan/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u5173\u95ed {entity_name}", + "turn_on": "\u6253\u5f00 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u5df2\u5173\u95ed", + "is_on": "{entity_name} \u5df2\u5f00\u542f" + }, + "trigger_type": { + "turned_off": "{entity_name} \u88ab\u5173\u95ed", + "turned_on": "{entity_name} \u88ab\u5f00\u542f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 44b33af0c6e..38234a8f832 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -111,25 +111,20 @@ class FanEntity(ToggleEntity): """Set the speed of the fan.""" raise NotImplementedError() - def async_set_speed(self, speed: str): - """Set the speed of the fan. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_set_speed(self, speed: str): + """Set the speed of the fan.""" if speed is SPEED_OFF: - return self.async_turn_off() - return self.hass.async_add_job(self.set_speed, speed) + await self.async_turn_off() + else: + await self.hass.async_add_job(self.set_speed, speed) def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" raise NotImplementedError() - def async_set_direction(self, direction: str): - """Set the direction of the fan. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_direction, direction) + async def async_set_direction(self, direction: str): + """Set the direction of the fan.""" + await self.hass.async_add_job(self.set_direction, direction) # pylint: disable=arguments-differ def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: @@ -137,25 +132,20 @@ class FanEntity(ToggleEntity): raise NotImplementedError() # pylint: disable=arguments-differ - def async_turn_on(self, speed: Optional[str] = None, **kwargs): - """Turn on the fan. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_turn_on(self, speed: Optional[str] = None, **kwargs): + """Turn on the fan.""" if speed is SPEED_OFF: - return self.async_turn_off() - return self.hass.async_add_job(ft.partial(self.turn_on, speed, **kwargs)) + await self.async_turn_off() + else: + await self.hass.async_add_job(ft.partial(self.turn_on, speed, **kwargs)) def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" pass - def async_oscillate(self, oscillating: bool): - """Oscillate the fan. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.oscillate, oscillating) + async def async_oscillate(self, oscillating: bool): + """Oscillate the fan.""" + await self.hass.async_add_job(self.oscillate, oscillating) @property def is_on(self): diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index c69f28c10e9..d3a8aa5c395 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -64,6 +64,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: @@ -75,6 +76,7 @@ def async_condition_from_config( else: state = STATE_OFF + @callback def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" return condition.state(hass, config[ATTR_ENTITY_ID], state) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 32d8f328ef8..52ecb881205 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -425,11 +425,6 @@ class FibaroDevice(Entity): else: self.dont_know_message(cmd) - @property - def hidden(self) -> bool: - """Return True if the entity should be hidden from UIs.""" - return self.fibaro_device.visible is False - @property def current_power_w(self): """Return the current power usage in W.""" diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index a71601ea2c4..107c837970d 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -16,7 +16,7 @@ _RESOURCE = "https://api.flock.com/hooks/sendMessage/" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.string}) -async def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the Flock notification service.""" access_token = config.get(CONF_ACCESS_TOKEN) url = f"{_RESOURCE}{access_token}" diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index f27e409a28d..908cfd98a6e 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,7 +1,7 @@ """Support for FRITZ!Box routers.""" import logging -from fritzconnection import FritzHosts # pylint: disable=import-error +from fritzconnection.lib.fritzhosts import FritzHosts import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -68,7 +68,7 @@ class FritzBoxScanner(DeviceScanner): self._update_info() active_hosts = [] for known_host in self.last_results: - if known_host["status"] == "1" and known_host.get("mac"): + if known_host["status"] and known_host.get("mac"): active_hosts.append(known_host["mac"]) return active_hosts diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 80709db0437..21b86e26af1 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM Fritzbox", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==0.8.4"], + "requirements": ["fritzconnection==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index f05bcec846a..777105f9143 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox_callmonitor", "name": "AVM FRITZ!Box Call Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==0.8.4"], + "requirements": ["fritzconnection==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 600420db859..fe0393720dc 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -6,7 +6,7 @@ import socket import threading import time -import fritzconnection as fc # pylint: disable=import-error +from fritzconnection.lib.fritzphonebook import FritzPhonebook import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -256,7 +256,7 @@ class FritzBoxPhonebook: self.prefixes = prefixes or [] # Establish a connection to the FRITZ!Box. - self.fph = fc.FritzPhonebook( + self.fph = FritzPhonebook( address=self.host, user=self.username, password=self.password ) diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 4dbb978842c..89bc1e1fda6 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==0.8.4"], + "requirements": ["fritzconnection==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 0a82c5e29c3..c0d010cf37e 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -2,10 +2,8 @@ from datetime import timedelta import logging -from fritzconnection import FritzStatus # pylint: disable=import-error -from fritzconnection.fritzconnection import ( # pylint: disable=import-error - FritzConnectionException, -) +from fritzconnection.core.exceptions import FritzConnectionException +from fritzconnection.lib.fritzstatus import FritzStatus from requests.exceptions import RequestException import voluptuous as vol @@ -30,7 +28,6 @@ ATTR_IS_LINKED = "is_linked" ATTR_MAX_BYTE_RATE_DOWN = "max_byte_rate_down" ATTR_MAX_BYTE_RATE_UP = "max_byte_rate_up" ATTR_UPTIME = "uptime" -ATTR_WAN_ACCESS_TYPE = "wan_access_type" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) @@ -73,7 +70,7 @@ class FritzboxMonitorSensor(Entity): self._name = name self._fstatus = fstatus self._state = STATE_UNAVAILABLE - self._is_linked = self._is_connected = self._wan_access_type = None + self._is_linked = self._is_connected = None self._external_ip = self._uptime = None self._bytes_sent = self._bytes_received = None self._transmission_rate_up = None @@ -104,7 +101,6 @@ class FritzboxMonitorSensor(Entity): attr = { ATTR_IS_LINKED: self._is_linked, ATTR_IS_CONNECTED: self._is_connected, - ATTR_WAN_ACCESS_TYPE: self._wan_access_type, ATTR_EXTERNAL_IP: self._external_ip, ATTR_UPTIME: self._uptime, ATTR_BYTES_SENT: self._bytes_sent, @@ -122,7 +118,6 @@ class FritzboxMonitorSensor(Entity): try: self._is_linked = self._fstatus.is_linked self._is_connected = self._fstatus.is_connected - self._wan_access_type = self._fstatus.wan_access_type self._external_ip = self._fstatus.external_ip self._uptime = self._fstatus.uptime self._bytes_sent = self._fstatus.bytes_sent diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index efb1c34653b..fdea21fe91e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -13,7 +13,7 @@ from yarl import URL from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView -from homeassistant.config import find_config_file, load_yaml_config_file +from homeassistant.config import async_hass_config_yaml from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -342,10 +342,12 @@ def _async_setup_themes(hass, themes): """Update theme_color in manifest.""" name = hass.data[DATA_DEFAULT_THEME] themes = hass.data[DATA_THEMES] - if name != DEFAULT_THEME and PRIMARY_COLOR in themes[name]: - MANIFEST_JSON["theme_color"] = themes[name][PRIMARY_COLOR] - else: - MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR + MANIFEST_JSON["theme_color"] = DEFAULT_THEME_COLOR + if name != DEFAULT_THEME: + MANIFEST_JSON["theme_color"] = themes[name].get( + "app-header-background-color", + themes[name].get(PRIMARY_COLOR, DEFAULT_THEME_COLOR), + ) hass.bus.async_fire( EVENT_THEMES_UPDATED, {"themes": themes, "default_theme": name} ) @@ -362,11 +364,10 @@ def _async_setup_themes(hass, themes): else: _LOGGER.warning("Theme %s is not defined.", name) - @callback - def reload_themes(_): + async def reload_themes(_): """Reload themes.""" - path = find_config_file(hass.config.config_dir) - new_themes = load_yaml_config_file(path)[DOMAIN].get(CONF_THEMES, {}) + config = await async_hass_config_yaml(hass) + new_themes = config[DOMAIN].get(CONF_THEMES, {}) hass.data[DATA_THEMES] = new_themes if hass.data[DATA_DEFAULT_THEME] not in new_themes: hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index bd732a7a0a1..09bd35ba89b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,16 +2,21 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200108.2"], + "requirements": [ + "home-assistant-frontend==20200130.1" + ], "dependencies": [ "api", "auth", "http", "lovelace", "onboarding", + "search", "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"], + "codeowners": [ + "@home-assistant/frontend" + ], "quality_scale": "internal" -} +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/cs.json b/homeassistant/components/garmin_connect/.translations/cs.json new file mode 100644 index 00000000000..ed8d33cc65c --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/cs.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Tento \u00fa\u010det je ji\u017e nakonfigurov\u00e1n." + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit, zkuste to znovu.", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed.", + "too_many_requests": "P\u0159\u00edli\u0161 mnoho po\u017eadavk\u016f, opakujte to pozd\u011bji.", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/da.json b/homeassistant/components/garmin_connect/.translations/da.json new file mode 100644 index 00000000000..1bbc5e7edba --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Denne konto er allerede konfigureret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse - pr\u00f8v igen.", + "invalid_auth": "Ugyldig godkendelse.", + "too_many_requests": "For mange anmodninger - pr\u00f8v igen senere.", + "unknown": "Uventet fejl." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Indtast dine legitimationsoplysninger.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/en.json b/homeassistant/components/garmin_connect/.translations/en.json new file mode 100644 index 00000000000..5dac9131fb0 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This account is already configured." + }, + "error": { + "cannot_connect": "Failed to connect, please try again.", + "invalid_auth": "Invalid authentication.", + "too_many_requests": "Too many requests, retry later.", + "unknown": "Unexpected error." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Enter your credentials.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/hu.json b/homeassistant/components/garmin_connect/.translations/hu.json new file mode 100644 index 00000000000..de4dea29166 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a fi\u00f3k m\u00e1r konfigur\u00e1lva van." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/ko.json b/homeassistant/components/garmin_connect/.translations/ko.json new file mode 100644 index 00000000000..018a0a8d923 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uacc4\uc815\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "too_many_requests": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Garmin \uc5f0\uacb0" + } + }, + "title": "Garmin \uc5f0\uacb0" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/no.json b/homeassistant/components/garmin_connect/.translations/no.json new file mode 100644 index 00000000000..f7bdba27906 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Denne kontoen er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kunne ikke koble til, pr\u00f8v igjen.", + "invalid_auth": "Ugyldig godkjenning.", + "too_many_requests": "For mange foresp\u00f8rsler, pr\u00f8v p\u00e5 nytt senere.", + "unknown": "Uventet feil." + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Angi brukeropplysninger.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/pl.json b/homeassistant/components/garmin_connect/.translations/pl.json new file mode 100644 index 00000000000..45d0296b668 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "To konto jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/ru.json b/homeassistant/components/garmin_connect/.translations/ru.json new file mode 100644 index 00000000000..f8d018e1b1e --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/sv.json b/homeassistant/components/garmin_connect/.translations/sv.json new file mode 100644 index 00000000000..5426ce61bb4 --- /dev/null +++ b/homeassistant/components/garmin_connect/.translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Det h\u00e4r kontot har redan konfigurerats." + }, + "error": { + "cannot_connect": "Kunde inte ansluta, var god f\u00f6rs\u00f6k igen.", + "invalid_auth": "Ogiltig autentisering.", + "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare.", + "unknown": "Ov\u00e4ntat fel." + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange dina anv\u00e4ndaruppgifter.", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py new file mode 100644 index 00000000000..5336394f671 --- /dev/null +++ b/homeassistant/components/garmin_connect/__init__.py @@ -0,0 +1,108 @@ +"""The Garmin Connect integration.""" +import asyncio +from datetime import date, timedelta +import logging + +from garminconnect import ( + Garmin, + GarminConnectAuthenticationError, + GarminConnectConnectionError, + GarminConnectTooManyRequestsError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.util import Throttle + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +MIN_SCAN_INTERVAL = timedelta(minutes=10) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Garmin Connect component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Garmin Connect from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + garmin_client = Garmin(username, password) + + try: + garmin_client.login() + except ( + GarminConnectAuthenticationError, + GarminConnectTooManyRequestsError, + ) as err: + _LOGGER.error("Error occured during Garmin Connect login: %s", err) + return False + except (GarminConnectConnectionError) as err: + _LOGGER.error("Error occured during Garmin Connect login: %s", err) + raise ConfigEntryNotReady + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unknown error occured during Garmin Connect login") + return False + + garmin_data = GarminConnectData(hass, garmin_client) + hass.data[DOMAIN][entry.entry_id] = garmin_data + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class GarminConnectData: + """Define an object to hold sensor data.""" + + def __init__(self, hass, client): + """Initialize.""" + self.client = client + self.data = None + + @Throttle(MIN_SCAN_INTERVAL) + async def async_update(self): + """Update data via library.""" + today = date.today() + + try: + self.data = self.client.get_stats(today.isoformat()) + except ( + GarminConnectAuthenticationError, + GarminConnectTooManyRequestsError, + ) as err: + _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + return + except (GarminConnectConnectionError) as err: + _LOGGER.error("Error occured during Garmin Connect get stats: %s", err) + return + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unknown error occured during Garmin Connect get stats") + return diff --git a/homeassistant/components/garmin_connect/config_flow.py b/homeassistant/components/garmin_connect/config_flow.py new file mode 100644 index 00000000000..36c63c7b995 --- /dev/null +++ b/homeassistant/components/garmin_connect/config_flow.py @@ -0,0 +1,72 @@ +"""Config flow for Garmin Connect integration.""" +import logging + +from garminconnect import ( + Garmin, + GarminConnectAuthenticationError, + GarminConnectConnectionError, + GarminConnectTooManyRequestsError, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class GarminConnectConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Garmin Connect.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_setup_form(self, errors=None): + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return await self._show_setup_form() + + garmin_client = Garmin(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + + errors = {} + try: + garmin_client.login() + except GarminConnectConnectionError: + errors["base"] = "cannot_connect" + return await self._show_setup_form(errors) + except GarminConnectAuthenticationError: + errors["base"] = "invalid_auth" + return await self._show_setup_form(errors) + except GarminConnectTooManyRequestsError: + errors["base"] = "too_many_requests" + return await self._show_setup_form(errors) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return await self._show_setup_form(errors) + + unique_id = garmin_client.get_full_name() + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=unique_id, + data={ + CONF_ID: unique_id, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py new file mode 100644 index 00000000000..57cd35e667f --- /dev/null +++ b/homeassistant/components/garmin_connect/const.py @@ -0,0 +1,288 @@ +"""Constants for the Garmin Connect integration.""" +from homeassistant.const import DEVICE_CLASS_TIMESTAMP + +DOMAIN = "garmin_connect" +ATTRIBUTION = "Data provided by garmin.com" + +GARMIN_ENTITY_LIST = { + "totalSteps": ["Total Steps", "steps", "mdi:walk", None, True], + "dailyStepGoal": ["Daily Step Goal", "steps", "mdi:walk", None, True], + "totalKilocalories": ["Total KiloCalories", "kcal", "mdi:food", None, True], + "activeKilocalories": ["Active KiloCalories", "kcal", "mdi:food", None, True], + "bmrKilocalories": ["BMR KiloCalories", "kcal", "mdi:food", None, True], + "consumedKilocalories": ["Consumed KiloCalories", "kcal", "mdi:food", None, False], + "burnedKilocalories": ["Burned KiloCalories", "kcal", "mdi:food", None, True], + "remainingKilocalories": [ + "Remaining KiloCalories", + "kcal", + "mdi:food", + None, + False, + ], + "netRemainingKilocalories": [ + "Net Remaining KiloCalories", + "kcal", + "mdi:food", + None, + False, + ], + "netCalorieGoal": ["Net Calorie Goal", "cal", "mdi:food", None, False], + "totalDistanceMeters": ["Total Distance Mtr", "mtr", "mdi:walk", None, True], + "wellnessStartTimeLocal": [ + "Wellness Start Time", + "", + "mdi:clock", + DEVICE_CLASS_TIMESTAMP, + False, + ], + "wellnessEndTimeLocal": [ + "Wellness End Time", + "", + "mdi:clock", + DEVICE_CLASS_TIMESTAMP, + False, + ], + "wellnessDescription": ["Wellness Description", "", "mdi:clock", None, False], + "wellnessDistanceMeters": ["Wellness Distance Mtr", "mtr", "mdi:walk", None, False], + "wellnessActiveKilocalories": [ + "Wellness Active KiloCalories", + "kcal", + "mdi:food", + None, + False, + ], + "wellnessKilocalories": ["Wellness KiloCalories", "kcal", "mdi:food", None, False], + "highlyActiveSeconds": ["Highly Active Time", "minutes", "mdi:fire", None, False], + "activeSeconds": ["Active Time", "minutes", "mdi:fire", None, True], + "sedentarySeconds": ["Sedentary Time", "minutes", "mdi:seat", None, True], + "sleepingSeconds": ["Sleeping Time", "minutes", "mdi:sleep", None, True], + "measurableAwakeDuration": ["Awake Duration", "minutes", "mdi:sleep", None, True], + "measurableAsleepDuration": ["Sleep Duration", "minutes", "mdi:sleep", None, True], + "floorsAscendedInMeters": ["Floors Ascended Mtr", "mtr", "mdi:stairs", None, False], + "floorsDescendedInMeters": [ + "Floors Descended Mtr", + "mtr", + "mdi:stairs", + None, + False, + ], + "floorsAscended": ["Floors Ascended", "floors", "mdi:stairs", None, True], + "floorsDescended": ["Floors Descended", "floors", "mdi:stairs", None, True], + "userFloorsAscendedGoal": [ + "Floors Ascended Goal", + "floors", + "mdi:stairs", + None, + True, + ], + "minHeartRate": ["Min Heart Rate", "bpm", "mdi:heart-pulse", None, True], + "maxHeartRate": ["Max Heart Rate", "bpm", "mdi:heart-pulse", None, True], + "restingHeartRate": ["Resting Heart Rate", "bpm", "mdi:heart-pulse", None, True], + "minAvgHeartRate": ["Min Avg Heart Rate", "bpm", "mdi:heart-pulse", None, False], + "maxAvgHeartRate": ["Max Avg Heart Rate", "bpm", "mdi:heart-pulse", None, False], + "abnormalHeartRateAlertsCount": [ + "Abnormal HR Counts", + "", + "mdi:heart-pulse", + None, + False, + ], + "lastSevenDaysAvgRestingHeartRate": [ + "Last 7 Days Avg Heart Rate", + "bpm", + "mdi:heart-pulse", + None, + False, + ], + "averageStressLevel": ["Avg Stress Level", "", "mdi:flash-alert", None, True], + "maxStressLevel": ["Max Stress Level", "", "mdi:flash-alert", None, True], + "stressQualifier": ["Stress Qualifier", "", "mdi:flash-alert", None, False], + "stressDuration": ["Stress Duration", "minutes", "mdi:flash-alert", None, False], + "restStressDuration": [ + "Rest Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "activityStressDuration": [ + "Activity Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "uncategorizedStressDuration": [ + "Uncat. Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "totalStressDuration": [ + "Total Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "lowStressDuration": [ + "Low Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "mediumStressDuration": [ + "Medium Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "highStressDuration": [ + "High Stress Duration", + "minutes", + "mdi:flash-alert", + None, + True, + ], + "stressPercentage": ["Stress Percentage", "%", "mdi:flash-alert", None, False], + "restStressPercentage": [ + "Rest Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "activityStressPercentage": [ + "Activity Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "uncategorizedStressPercentage": [ + "Uncat. Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "lowStressPercentage": [ + "Low Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "mediumStressPercentage": [ + "Medium Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "highStressPercentage": [ + "High Stress Percentage", + "%", + "mdi:flash-alert", + None, + False, + ], + "moderateIntensityMinutes": [ + "Moderate Intensity", + "minutes", + "mdi:flash-alert", + None, + False, + ], + "vigorousIntensityMinutes": [ + "Vigorous Intensity", + "minutes", + "mdi:run-fast", + None, + False, + ], + "intensityMinutesGoal": ["Intensity Goal", "minutes", "mdi:run-fast", None, False], + "bodyBatteryChargedValue": [ + "Body Battery Charged", + "%", + "mdi:battery-charging-100", + None, + True, + ], + "bodyBatteryDrainedValue": [ + "Body Battery Drained", + "%", + "mdi:battery-alert-variant-outline", + None, + True, + ], + "bodyBatteryHighestValue": [ + "Body Battery Highest", + "%", + "mdi:battery-heart", + None, + True, + ], + "bodyBatteryLowestValue": [ + "Body Battery Lowest", + "%", + "mdi:battery-heart-outline", + None, + True, + ], + "bodyBatteryMostRecentValue": [ + "Body Battery Most Recent", + "%", + "mdi:battery-positive", + None, + True, + ], + "averageSpo2": ["Average SPO2", "%", "mdi:diabetes", None, True], + "lowestSpo2": ["Lowest SPO2", "%", "mdi:diabetes", None, True], + "latestSpo2": ["Latest SPO2", "%", "mdi:diabetes", None, True], + "latestSpo2ReadingTimeLocal": [ + "Latest SPO2 Time", + "", + "mdi:diabetes", + DEVICE_CLASS_TIMESTAMP, + False, + ], + "averageMonitoringEnvironmentAltitude": [ + "Average Altitude", + "%", + "mdi:image-filter-hdr", + None, + False, + ], + "highestRespirationValue": [ + "Highest Respiration", + "brpm", + "mdi:progress-clock", + None, + True, + ], + "lowestRespirationValue": [ + "Lowest Respiration", + "brpm", + "mdi:progress-clock", + None, + True, + ], + "latestRespirationValue": [ + "Latest Respiration", + "brpm", + "mdi:progress-clock", + None, + True, + ], + "latestRespirationTimeGMT": [ + "Latest Respiration Update", + "", + "mdi:progress-clock", + DEVICE_CLASS_TIMESTAMP, + False, + ], +} diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json new file mode 100644 index 00000000000..b2282831572 --- /dev/null +++ b/homeassistant/components/garmin_connect/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "garmin_connect", + "name": "Garmin Connect", + "documentation": "https://www.home-assistant.io/integrations/garmin_connect", + "dependencies": [], + "requirements": ["garminconnect==0.1.8"], + "codeowners": ["@cyberjunky"], + "config_flow": true +} diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py new file mode 100644 index 00000000000..6a3128cae01 --- /dev/null +++ b/homeassistant/components/garmin_connect/sensor.py @@ -0,0 +1,177 @@ +"""Platform for Garmin Connect integration.""" +import logging +from typing import Any, Dict + +from garminconnect import ( + GarminConnectAuthenticationError, + GarminConnectConnectionError, + GarminConnectTooManyRequestsError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_ID +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ATTRIBUTION, DOMAIN, GARMIN_ENTITY_LIST + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up Garmin Connect sensor based on a config entry.""" + garmin_data = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.data[CONF_ID] + + try: + await garmin_data.async_update() + except ( + GarminConnectConnectionError, + GarminConnectAuthenticationError, + GarminConnectTooManyRequestsError, + ) as err: + _LOGGER.error("Error occured during Garmin Connect Client update: %s", err) + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unknown error occured during Garmin Connect Client update.") + + entities = [] + for ( + sensor_type, + (name, unit, icon, device_class, enabled_by_default), + ) in GARMIN_ENTITY_LIST.items(): + + _LOGGER.debug( + "Registering entity: %s, %s, %s, %s, %s, %s", + sensor_type, + name, + unit, + icon, + device_class, + enabled_by_default, + ) + entities.append( + GarminConnectSensor( + garmin_data, + unique_id, + sensor_type, + name, + unit, + icon, + device_class, + enabled_by_default, + ) + ) + + async_add_entities(entities, True) + + +class GarminConnectSensor(Entity): + """Representation of a Garmin Connect Sensor.""" + + def __init__( + self, + data, + unique_id, + sensor_type, + name, + unit, + icon, + device_class, + enabled_default: bool = True, + ): + """Initialize.""" + self._data = data + self._unique_id = unique_id + self._type = sensor_type + self._name = name + self._unit = unit + self._icon = icon + self._device_class = device_class + self._enabled_default = enabled_default + self._available = True + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self._unique_id}_{self._type}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def device_state_attributes(self): + """Return attributes for sensor.""" + attributes = {} + if self._data.data: + attributes = { + "source": self._data.data["source"], + "last_synced": self._data.data["lastSyncTimestampGMT"], + ATTR_ATTRIBUTION: ATTRIBUTION, + } + return attributes + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information.""" + return { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": "Garmin Connect", + "manufacturer": "Garmin Connect", + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + async def async_update(self): + """Update the data from Garmin Connect.""" + if not self.enabled: + return + + await self._data.async_update() + if not self._data.data: + _LOGGER.error("Didn't receive data from Garmin Connect") + return + + data = self._data.data + if "Duration" in self._type: + self._state = data[self._type] // 60 + elif "Seconds" in self._type: + self._state = data[self._type] // 60 + else: + self._state = data[self._type] + + _LOGGER.debug( + "Entity %s set to state %s %s", self._type, self._state, self._unit + ) diff --git a/homeassistant/components/garmin_connect/strings.json b/homeassistant/components/garmin_connect/strings.json new file mode 100644 index 00000000000..faf463ea8db --- /dev/null +++ b/homeassistant/components/garmin_connect/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This account is already configured." + }, + "error": { + "cannot_connect": "Failed to connect, please try again.", + "invalid_auth": "Invalid authentication.", + "too_many_requests": "Too many requests, retry later.", + "unknown": "Unexpected error." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Enter your credentials.", + "title": "Garmin Connect" + } + }, + "title": "Garmin Connect" + } +} diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 3d39d75ff4a..3abeab32262 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -49,7 +49,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, - vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_FRAMERATE, default=2): vol.Any( + cv.small_float, cv.positive_int + ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 58514934fc7..23ee049052c 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -416,6 +416,10 @@ class GenericThermostat(ClimateDevice, RestoreEntity): await self._async_heater_turn_off() elif time is not None: # The time argument is passed only in keep-alive case + _LOGGER.info( + "Keep-alive - Turning on heater heater %s", + self.heater_entity_id, + ) await self._async_heater_turn_on() else: if (self.ac_mode and too_hot) or (not self.ac_mode and too_cold): @@ -423,6 +427,9 @@ class GenericThermostat(ClimateDevice, RestoreEntity): await self._async_heater_turn_on() elif time is not None: # The time argument is passed only in keep-alive case + _LOGGER.info( + "Keep-alive - Turning off heater %s", self.heater_entity_id + ) await self._async_heater_turn_off() @property @@ -446,10 +453,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): await self.hass.services.async_call(HA_DOMAIN, SERVICE_TURN_OFF, data) async def async_set_preset_mode(self, preset_mode: str): - """Set new preset mode. - - This method must be run in the event loop and returns a coroutine. - """ + """Set new preset mode.""" if preset_mode == PRESET_AWAY and not self._is_away: self._is_away = True self._saved_target_temp = self._target_temp diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 977656149c5..bb25d2d619d 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -8,6 +8,7 @@ from geniushubclient import GeniusHub import voluptuous as vol from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_HOST, CONF_MAC, @@ -26,11 +27,10 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -ATTR_DURATION = "duration" - _LOGGER = logging.getLogger(__name__) DOMAIN = "geniushub" @@ -68,6 +68,30 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) +ATTR_ZONE_MODE = "mode" +ATTR_DURATION = "duration" + +SVC_SET_ZONE_MODE = "set_zone_mode" +SVC_SET_ZONE_OVERRIDE = "set_zone_override" + +SET_ZONE_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_ZONE_MODE): vol.In(["off", "timer", "footprint"]), + } +) +SET_ZONE_OVERRIDE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TEMPERATURE): vol.All( + vol.Coerce(float), vol.Range(min=4, max=28) + ), + vol.Optional(ATTR_DURATION): vol.All( + cv.time_period, vol.Range(min=timedelta(minutes=5), max=timedelta(days=1)), + ), + } +) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a Genius Hub system.""" @@ -96,9 +120,45 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: for platform in ["climate", "water_heater", "sensor", "binary_sensor", "switch"]: hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) + setup_service_functions(hass, broker) + return True +@callback +def setup_service_functions(hass: HomeAssistantType, broker): + """Set up the service functions.""" + + @verify_domain_control(hass, DOMAIN) + async def set_zone_mode(call) -> None: + """Set the system mode.""" + entity_id = call.data[ATTR_ENTITY_ID] + + registry = await hass.helpers.entity_registry.async_get_registry() + registry_entry = registry.async_get(entity_id) + + if registry_entry is None or registry_entry.platform != DOMAIN: + raise ValueError(f"'{entity_id}' is not a known {DOMAIN} entity") + + if registry_entry.domain != "climate": + raise ValueError(f"'{entity_id}' is not an {DOMAIN} zone") + + payload = { + "unique_id": registry_entry.unique_id, + "service": call.service, + "data": call.data, + } + + async_dispatcher_send(hass, DOMAIN, payload) + + hass.services.async_register( + DOMAIN, SVC_SET_ZONE_MODE, set_zone_mode, schema=SET_ZONE_MODE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SVC_SET_ZONE_OVERRIDE, set_zone_mode, schema=SET_ZONE_OVERRIDE_SCHEMA + ) + + class GeniusBroker: """Container for geniushub client and data.""" @@ -146,8 +206,8 @@ class GeniusEntity(Entity): """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - @callback - def _refresh(self) -> None: + async def _refresh(self, payload: Optional[dict] = None) -> None: + """Process any signals.""" self.async_schedule_update_ha_state(force_refresh=True) @property @@ -175,7 +235,6 @@ class GeniusDevice(GeniusEntity): self._device = device self._unique_id = f"{broker.hub_uid}_device_{device.id}" - self._last_comms = self._state_attr = None @property @@ -188,7 +247,7 @@ class GeniusDevice(GeniusEntity): attrs["last_comms"] = self._last_comms.isoformat() state = dict(self._device.data["state"]) - if "_state" in self._device.data: # only for v3 API + if "_state" in self._device.data: # only via v3 API state.update(self._device.data["_state"]) attrs["state"] = { @@ -199,7 +258,7 @@ class GeniusDevice(GeniusEntity): async def async_update(self) -> None: """Update an entity's state data.""" - if "_state" in self._device.data: # only for v3 API + if "_state" in self._device.data: # only via v3 API self._last_comms = dt_util.utc_from_timestamp( self._device.data["_state"]["lastComms"] ) @@ -215,6 +274,32 @@ class GeniusZone(GeniusEntity): self._zone = zone self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" + async def _refresh(self, payload: Optional[dict] = None) -> None: + """Process any signals.""" + if payload is None: + self.async_schedule_update_ha_state(force_refresh=True) + return + + if payload["unique_id"] != self._unique_id: + return + + if payload["service"] == SVC_SET_ZONE_OVERRIDE: + temperature = round(payload["data"][ATTR_TEMPERATURE] * 10) / 10 + duration = payload["data"].get(ATTR_DURATION, timedelta(hours=1)) + + await self._zone.set_override(temperature, int(duration.total_seconds())) + return + + mode = payload["data"][ATTR_ZONE_MODE] + + # pylint: disable=protected-access + if mode == "footprint" and not self._zone._has_pir: + raise TypeError( + f"'{self.entity_id}' can not support footprint mode (it has no PIR)" + ) + + await self._zone.set_mode(mode) + @property def name(self) -> str: """Return the name of the climate device.""" diff --git a/homeassistant/components/geniushub/services.yaml b/homeassistant/components/geniushub/services.yaml new file mode 100644 index 00000000000..d7522ac2995 --- /dev/null +++ b/homeassistant/components/geniushub/services.yaml @@ -0,0 +1,29 @@ +# Support for a Genius Hub system +# Describes the format for available services + +set_zone_mode: + description: >- + Set the zone to an operating mode. + fields: + entity_id: + description: The zone's entity_id. + example: climate.kitchen + mode: + description: 'One of: off, timer or footprint.' + example: timer + +set_zone_override: + description: >- + Override the zone's setpoint for a given duration. + fields: + entity_id: + description: The zone's entity_id. + example: climate.bathroom + temperature: + description: The target temperature, to 0.1 C. + example: 19.2 + duration: + description: >- + The duration of the override. Optional, default 1 hour, maximum 24 hours. + example: '{"minutes": 135}' + diff --git a/homeassistant/components/geofency/.translations/de.json b/homeassistant/components/geofency/.translations/de.json index ad4722fa9fc..f7773f13db8 100644 --- a/homeassistant/components/geofency/.translations/de.json +++ b/homeassistant/components/geofency/.translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "M\u00f6chtest du den Geofency Webhook wirklich einrichten?", - "title": "Richten Sie den Geofency Webhook ein" + "title": "Richte den Geofency Webhook ein" } }, "title": "Geofency Webhook" diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json index 7c6fd08af96..e5c2acf352c 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/de.json +++ b/homeassistant/components/geonetnz_quakes/.translations/de.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Radius" }, - "title": "F\u00fcllen Sie Ihre Filterdaten aus." + "title": "F\u00fclle deine Filterdaten aus." } }, "title": "GeoNet NZ Erdbeben" diff --git a/homeassistant/components/geonetnz_volcano/.translations/de.json b/homeassistant/components/geonetnz_volcano/.translations/de.json index fa87d24811c..59396e3a440 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/de.json +++ b/homeassistant/components/geonetnz_volcano/.translations/de.json @@ -8,7 +8,7 @@ "data": { "radius": "Radius" }, - "title": "F\u00fcllen Sie Ihre Filterangaben aus." + "title": "F\u00fclle deine Filterangaben aus." } }, "title": "GeoNet NZ Volcano" diff --git a/homeassistant/components/geonetnz_volcano/.translations/hu.json b/homeassistant/components/geonetnz_volcano/.translations/hu.json new file mode 100644 index 00000000000..875a8330f76 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 2fa10812d37..e5153e9675e 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -2,7 +2,7 @@ "domain": "geonetnz_volcano", "name": "GeoNet NZ Volcano", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geonetnz_volcano", + "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", "requirements": ["aio_geojson_geonetnz_volcano==0.5"], "dependencies": [], "codeowners": ["@exxamalte"] diff --git a/homeassistant/components/gios/.translations/de.json b/homeassistant/components/gios/.translations/de.json index 36813d71d76..5fd36f7a9fb 100644 --- a/homeassistant/components/gios/.translations/de.json +++ b/homeassistant/components/gios/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a integration f\u00fcr diese Messstation ist bereits konfiguriert. " + }, "error": { "cannot_connect": "Es kann keine Verbindung zum GIO\u015a-Server hergestellt werden.", "invalid_sensors_data": "Ung\u00fcltige Sensordaten f\u00fcr diese Messstation.", @@ -10,7 +13,9 @@ "data": { "name": "Name der Integration", "station_id": "ID der Messstation" - } + }, + "description": "Einrichtung von GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz) Integration der Luftqualit\u00e4t. Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz)" } }, "title": "GIO\u015a" diff --git a/homeassistant/components/gios/.translations/fr.json b/homeassistant/components/gios/.translations/fr.json index 2a9136bab4f..3e870448659 100644 --- a/homeassistant/components/gios/.translations/fr.json +++ b/homeassistant/components/gios/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'int\u00e9gration GIO\u015a pour cette station de mesure est d\u00e9j\u00e0 configur\u00e9e." + }, "error": { "cannot_connect": "Impossible de se connecter au serveur GIOS", "invalid_sensors_data": "Donn\u00e9es des capteurs non valides pour cette station de mesure.", @@ -10,8 +13,11 @@ "data": { "name": "Nom de l'int\u00e9gration", "station_id": "Identifiant de la station de mesure" - } + }, + "description": "Mettre en place l'int\u00e9gration de la qualit\u00e9 de l'air GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement). Si vous avez besoin d'aide pour la configuration, regardez ici: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement)" } - } + }, + "title": "GIO\u015a" } } \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ko.json b/homeassistant/components/gios/.translations/ko.json index 2a92f935794..cc338a82e16 100644 --- a/homeassistant/components/gios/.translations/ko.json +++ b/homeassistant/components/gios/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c GIO\u015a \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_sensors_data": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c \uc13c\uc11c \ub370\uc774\ud130\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", @@ -15,6 +18,6 @@ "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" } }, - "title": "GIO\u015a" + "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json index 8330745f4b4..e652ccc966b 100644 --- a/homeassistant/components/glances/.translations/de.json +++ b/homeassistant/components/glances/.translations/de.json @@ -14,9 +14,9 @@ "name": "Name", "password": "Passwort", "port": "Port", - "ssl": "Verwenden Sie SSL / TLS, um eine Verbindung zum Glances-System herzustellen", + "ssl": "Verwende SSL / TLS, um eine Verbindung zum Glances-System herzustellen", "username": "Benutzername", - "verify_ssl": "\u00dcberpr\u00fcfen Sie die Zertifizierung des Systems", + "verify_ssl": "\u00dcberpr\u00fcfe die Zertifizierung des Systems", "version": "Glances API-Version (2 oder 3)" }, "title": "Glances einrichten" @@ -30,7 +30,7 @@ "data": { "scan_interval": "Aktualisierungsfrequenz" }, - "description": "Konfigurieren Sie die Optionen f\u00fcr Glances" + "description": "Konfiguriere die Optionen f\u00fcr Glances" } } } diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 760958f0dee..968081cfc43 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -11,11 +11,6 @@ from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Glances sensors is done through async_setup_entry.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Glances sensors.""" diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index dcb87d1d93d..add625d2de4 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -143,3 +143,6 @@ CHALLENGE_PIN_NEEDED = "pinNeeded" CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" STORE_AGENT_USER_IDS = "agent_user_ids" + +SOURCE_CLOUD = "cloud" +SOURCE_LOCAL = "local" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6493d759880..f1b7a89bffe 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -28,6 +28,7 @@ from .const import ( DOMAIN, DOMAIN_TO_GOOGLE_TYPES, ERR_FUNCTION_NOT_SUPPORTED, + SOURCE_LOCAL, STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -121,6 +122,7 @@ class AbstractConfig(ABC): ] await gather(*jobs) + @callback def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep @@ -130,6 +132,7 @@ class AbstractConfig(ABC): if self._unsub_report_state is None: self._unsub_report_state = async_enable_report_state(self.hass, self) + @callback def async_disable_report_state(self): """Disable report state.""" if self._unsub_report_state is not None: @@ -232,7 +235,7 @@ class AbstractConfig(ABC): return json_response(smart_home.turned_off_response(payload)) result = await smart_home.async_handle_message( - self.hass, self, self.local_sdk_user_id, payload + self.hass, self, self.local_sdk_user_id, payload, SOURCE_LOCAL ) if _LOGGER.isEnabledFor(logging.DEBUG): @@ -286,15 +289,22 @@ class RequestData: self, config: AbstractConfig, user_id: str, + source: str, request_id: str, devices: Optional[List[dict]], ): """Initialize the request data.""" self.config = config + self.source = source self.request_id = request_id self.context = Context(user_id=user_id) self.devices = devices + @property + def is_local_request(self): + """Return if this is a local request.""" + return self.source == SOURCE_LOCAL + def get_google_type(domain, device_class): """Google type based on domain and device class.""" @@ -354,6 +364,9 @@ class GoogleEntity: features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) device_class = state.attributes.get(ATTR_DEVICE_CLASS) + if not self.config.should_2fa(state): + return False + return any( trait.might_2fa(domain, features, device_class) for trait in self.traits() ) @@ -386,7 +399,7 @@ class GoogleEntity: # use aliases aliases = entity_config.get(CONF_ALIASES) if aliases: - device["name"]["nicknames"] = aliases + device["name"]["nicknames"] = [name] + aliases if self.config.is_local_sdk_active: device["otherDeviceIds"] = [{"deviceId": self.entity_id}] diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index f8fa51da8d7..7bd3583e5c2 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -30,6 +30,7 @@ from .const import ( HOMEGRAPH_TOKEN_URL, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, + SOURCE_CLOUD, ) from .helpers import AbstractConfig from .smart_home import async_handle_message @@ -238,6 +239,10 @@ class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" message: dict = await request.json() result = await async_handle_message( - request.app["hass"], self.config, request["hass_user"].id, message + request.app["hass"], + self.config, + request["hass_user"].id, + message, + SOURCE_CLOUD, ) return self.json(result) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 8033bcec865..bf6c32505aa 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -21,9 +21,11 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) -async def async_handle_message(hass, config, user_id, message): +async def async_handle_message(hass, config, user_id, message, source): """Handle incoming API messages.""" - data = RequestData(config, user_id, message["requestId"], message.get("devices")) + data = RequestData( + config, user_id, source, message["requestId"], message.get("devices") + ) response = await _process(hass, data, message) @@ -75,7 +77,9 @@ async def async_devices_sync(hass, data, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC """ hass.bus.async_fire( - EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context + EVENT_SYNC_RECEIVED, + {"request_id": data.request_id, "source": data.source}, + context=data.context, ) agent_user_id = data.config.get_agent_user_id(data.context) @@ -108,7 +112,11 @@ async def async_devices_query(hass, data, payload): hass.bus.async_fire( EVENT_QUERY_RECEIVED, - {"request_id": data.request_id, ATTR_ENTITY_ID: devid}, + { + "request_id": data.request_id, + ATTR_ENTITY_ID: devid, + "source": data.source, + }, context=data.context, ) @@ -142,6 +150,7 @@ async def handle_devices_execute(hass, data, payload): "request_id": data.request_id, ATTR_ENTITY_ID: entity_id, "execution": execution, + "source": data.source, }, context=data.context, ) @@ -234,7 +243,9 @@ async def async_devices_reachable(hass, data: RequestData, payload): "devices": [ entity.reachable_device_serialize() for entity in async_get_entities(hass, data.config) - if entity.entity_id in google_ids and entity.should_expose() + if entity.entity_id in google_ids + and entity.should_expose() + and not entity.might_2fa() ] } diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 14839066ebe..b4585ebde03 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1447,6 +1447,8 @@ def _verify_pin_challenge(data, state, challenge): def _verify_ack_challenge(data, state, challenge): - """Verify a pin challenge.""" + """Verify an ack challenge.""" + if not data.config.should_2fa(state): + return if not challenge or not challenge.get("ack"): raise ChallengeNeeded(CHALLENGE_ACK_NEEDED) diff --git a/homeassistant/components/gpslogger/.translations/de.json b/homeassistant/components/gpslogger/.translations/de.json index 82c1dfa3e53..840cbdf234f 100644 --- a/homeassistant/components/gpslogger/.translations/de.json +++ b/homeassistant/components/gpslogger/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie den GPSLogger Webhook wirklich einrichten?", + "description": "M\u00f6chtest du den GPSLogger Webhook wirklich einrichten?", "title": "GPSLogger Webhook einrichten" } }, diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index 4f5899f6a4a..dcd383a7463 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -28,6 +28,7 @@ CONF_SENSORS = "sensors" CONF_SENSOR_TYPE = "sensor_type" CONF_TEMPERATURE_SENSORS = "temperature_sensors" CONF_TIME_UNIT = "time_unit" +CONF_VOLTAGE_SENSORS = "voltage" DATA_GREENEYE_MONITOR = "greeneye_monitor" DOMAIN = "greeneye_monitor" @@ -35,6 +36,7 @@ DOMAIN = "greeneye_monitor" SENSOR_TYPE_CURRENT = "current_sensor" SENSOR_TYPE_PULSE_COUNTER = "pulse_counter" SENSOR_TYPE_TEMPERATURE = "temperature_sensor" +SENSOR_TYPE_VOLTAGE = "voltage_sensor" TEMPERATURE_UNIT_CELSIUS = "C" @@ -55,6 +57,12 @@ TEMPERATURE_SENSORS_SCHEMA = vol.Schema( } ) +VOLTAGE_SENSOR_SCHEMA = vol.Schema( + {vol.Required(CONF_NUMBER): vol.Range(1, 48), vol.Required(CONF_NAME): cv.string} +) + +VOLTAGE_SENSORS_SCHEMA = vol.All(cv.ensure_list, [VOLTAGE_SENSOR_SCHEMA]) + PULSE_COUNTER_SCHEMA = vol.Schema( { vol.Required(CONF_NUMBER): vol.Range(1, 4), @@ -97,6 +105,7 @@ MONITOR_SCHEMA = vol.Schema( default={CONF_TEMPERATURE_UNIT: TEMPERATURE_UNIT_CELSIUS, CONF_SENSORS: []}, ): TEMPERATURE_SENSORS_SCHEMA, vol.Optional(CONF_PULSE_COUNTERS, default=[]): PULSE_COUNTERS_SCHEMA, + vol.Optional(CONF_VOLTAGE_SENSORS, default=[]): VOLTAGE_SENSORS_SCHEMA, } ) @@ -140,6 +149,16 @@ async def async_setup(hass, config): } ) + voltage_configs = monitor_config[CONF_VOLTAGE_SENSORS] + for voltage_config in voltage_configs: + all_sensors.append( + { + CONF_SENSOR_TYPE: SENSOR_TYPE_VOLTAGE, + **monitor_serial_number, + **voltage_config, + } + ) + sensor_configs = monitor_config[CONF_TEMPERATURE_SENSORS] if sensor_configs: temperature_unit = { @@ -168,7 +187,7 @@ async def async_setup(hass, config): if not all_sensors: _LOGGER.error( "Configuration must specify at least one " - "channel, pulse counter or temperature sensor" + "channel, voltage, pulse counter or temperature sensor" ) return False diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 0c55b644d94..88183acf918 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": ["greeneye_monitor==1.0.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@jkeljo"] } diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index c4b5fc67898..19ef7529b0a 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -16,6 +16,7 @@ from . import ( SENSOR_TYPE_CURRENT, SENSOR_TYPE_PULSE_COUNTER, SENSOR_TYPE_TEMPERATURE, + SENSOR_TYPE_VOLTAGE, TIME_UNIT_HOUR, TIME_UNIT_MINUTE, TIME_UNIT_SECOND, @@ -31,6 +32,7 @@ UNIT_WATTS = POWER_WATT COUNTER_ICON = "mdi:counter" CURRENT_SENSOR_ICON = "mdi:flash" TEMPERATURE_ICON = "mdi:thermometer" +VOLTAGE_ICON = "mdi:current-ac" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -70,6 +72,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor[CONF_TEMPERATURE_UNIT], ) ) + elif sensor_type == SENSOR_TYPE_VOLTAGE: + entities.append( + VoltageSensor( + sensor[CONF_MONITOR_SERIAL_NUMBER], + sensor[CONF_NUMBER], + sensor[CONF_NAME], + ) + ) async_add_entities(entities) @@ -276,3 +286,35 @@ class TemperatureSensor(GEMSensor): def unit_of_measurement(self): """Return the unit of measurement for this sensor (user specified).""" return self._unit + + +class VoltageSensor(GEMSensor): + """Entity showing voltage.""" + + def __init__(self, monitor_serial_number, number, name): + """Construct the entity.""" + super().__init__(monitor_serial_number, name, "volts", number) + self._monitor = None + + def _get_sensor(self, monitor): + """Wire the updates to a current channel.""" + self._monitor = monitor + return monitor.channels[self._number - 1] + + @property + def icon(self): + """Return the icon that should represent this sensor in the UI.""" + return VOLTAGE_ICON + + @property + def state(self): + """Return the current voltage being reported by this sensor.""" + if not self._monitor.voltage: + return None + + return self._monitor.voltage + + @property + def unit_of_measurement(self): + """Return the unit of measurement for this sensor.""" + return "V" diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index c8a138abe41..fc37f904e0d 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -73,15 +73,19 @@ def _conf_preprocess(value): return value -GROUP_SCHEMA = vol.Schema( - { - vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), - CONF_VIEW: cv.boolean, - CONF_NAME: cv.string, - CONF_ICON: cv.icon, - CONF_CONTROL: CONTROL_TYPES, - CONF_ALL: cv.boolean, - } +GROUP_SCHEMA = vol.All( + cv.deprecated(CONF_CONTROL, invalidation_version="0.107.0"), + cv.deprecated(CONF_VIEW, invalidation_version="0.107.0"), + vol.Schema( + { + vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None), + CONF_VIEW: cv.boolean, + CONF_NAME: cv.string, + CONF_ICON: cv.icon, + CONF_CONTROL: CONTROL_TYPES, + CONF_ALL: cv.boolean, + } + ), ) CONFIG_SCHEMA = vol.Schema( @@ -183,6 +187,24 @@ def get_entity_ids( return [ent_id for ent_id in entity_ids if ent_id.startswith(domain_filter)] +@bind_hass +def groups_with_entity(hass: HomeAssistantType, entity_id: str) -> List[str]: + """Get all groups that contain this entity. + + Async friendly. + """ + if DOMAIN not in hass.data: + return [] + + groups = [] + + for group in hass.data[DOMAIN].entities: + if entity_id in group.tracking: + groups.append(group.entity_id) + + return groups + + async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) @@ -299,18 +321,23 @@ async def async_setup(hass, config): DOMAIN, SERVICE_SET, locked_service_handler, - schema=vol.Schema( - { - vol.Required(ATTR_OBJECT_ID): cv.slug, - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_VIEW): cv.boolean, - vol.Optional(ATTR_ICON): cv.string, - vol.Optional(ATTR_CONTROL): CONTROL_TYPES, - vol.Optional(ATTR_VISIBLE): cv.boolean, - vol.Optional(ATTR_ALL): cv.boolean, - vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, - vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, - } + schema=vol.All( + cv.deprecated(ATTR_CONTROL, invalidation_version="0.107.0"), + cv.deprecated(ATTR_VIEW, invalidation_version="0.107.0"), + cv.deprecated(ATTR_VISIBLE, invalidation_version="0.107.0"), + vol.Schema( + { + vol.Required(ATTR_OBJECT_ID): cv.slug, + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_VIEW): cv.boolean, + vol.Optional(ATTR_ICON): cv.string, + vol.Optional(ATTR_CONTROL): CONTROL_TYPES, + vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, + vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + } + ), ), ) @@ -325,6 +352,11 @@ async def async_setup(hass, config): """Change visibility of a group.""" visible = service.data.get(ATTR_VISIBLE) + _LOGGER.warning( + "The group.set_visibility service has been deprecated and will" + "be removed in Home Assistant 0.107.0." + ) + tasks = [] for group in await component.async_extract_from_service( service, expand_group=False diff --git a/homeassistant/components/hangouts/.translations/de.json b/homeassistant/components/hangouts/.translations/de.json index fa96c00f666..4f48187b49b 100644 --- a/homeassistant/components/hangouts/.translations/de.json +++ b/homeassistant/components/hangouts/.translations/de.json @@ -5,7 +5,7 @@ "unknown": "Ein unbekannter Fehler ist aufgetreten." }, "error": { - "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuchen Sie es erneut.", + "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", "invalid_2fa_method": "Ung\u00fcltige 2FA Methode (mit Telefon verifizieren)", "invalid_login": "Ung\u00fcltige Daten, bitte erneut versuchen." }, diff --git a/homeassistant/components/hangouts/.translations/zh-Hant.json b/homeassistant/components/hangouts/.translations/zh-Hant.json index c8da604e6f2..5c2fd47068d 100644 --- a/homeassistant/components/hangouts/.translations/zh-Hant.json +++ b/homeassistant/components/hangouts/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "error": { - "invalid_2fa": "\u5169\u6b65\u9a5f\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_2fa": "\u96d9\u91cd\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "invalid_2fa_method": "\u8a8d\u8b49\u65b9\u5f0f\u7121\u6548\uff08\u65bc\u96fb\u8a71\u4e0a\u9a57\u8b49\uff09\u3002", "invalid_login": "\u767b\u5165\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, @@ -15,7 +15,7 @@ "2fa": "\u8a8d\u8b49\u78bc" }, "description": "\u7a7a\u767d", - "title": "\u5169\u6b65\u9a5f\u9a57\u8b49" + "title": "\u96d9\u91cd\u9a57\u8b49" }, "user": { "data": { diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index fd14ec0b094..b3dfdecac2a 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -7,6 +7,7 @@ import aiohttp import hangups from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 +from homeassistant.core import callback from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -75,6 +76,7 @@ class HangoutsBot: return conv return None + @callback def async_update_conversation_commands(self): """Refresh the commands for every conversation.""" self._conversation_intents = {} @@ -110,6 +112,7 @@ class HangoutsBot: self._async_handle_conversation_event ) + @callback def async_resolve_conversations(self, _): """Resolve the list of default and error suppressed conversations.""" self._default_conv_ids = [] diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 28e06cc5d6a..cc03f26085c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -194,14 +194,14 @@ async def async_setup(hass, config): await hass.components.panel_custom.async_register_panel( frontend_url_path="hassio", webcomponent_name="hassio-main", - sidebar_title="Hass.io", + sidebar_title="Supervisor", sidebar_icon="hass:home-assistant", js_url="/api/hassio/app/entrypoint.js", embed_iframe=True, require_admin=True, ) - await hassio.update_hass_api(config.get("http", {}), refresh_token.token) + await hassio.update_hass_api(config.get("http", {}), refresh_token) async def push_config(_): """Push core config to Hass.io.""" @@ -290,7 +290,7 @@ async def async_setup(hass, config): async_setup_discovery_view(hass, hassio) # Init auth Hass.io feature - async_setup_auth_view(hass) + async_setup_auth_view(hass, user) # Init ingress Hass.io feature async_setup_ingress_view(hass, host) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 800801b4350..f8474e0fd24 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -4,11 +4,16 @@ import logging import os from aiohttp import web -from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +from aiohttp.web_exceptions import ( + HTTPInternalServerError, + HTTPNotFound, + HTTPUnauthorized, +) import voluptuous as vol +from homeassistant.auth.models import User from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.const import KEY_HASS_USER, KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -29,34 +34,42 @@ SCHEMA_API_AUTH = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SCHEMA_API_PASSWORD_RESET = vol.Schema( + {vol.Required(ATTR_USERNAME): cv.string, vol.Required(ATTR_PASSWORD): cv.string}, + extra=vol.ALLOW_EXTRA, +) + @callback -def async_setup_auth_view(hass: HomeAssistantType): +def async_setup_auth_view(hass: HomeAssistantType, user: User): """Auth setup.""" - hassio_auth = HassIOAuth(hass) + hassio_auth = HassIOAuth(hass, user) + hassio_password_reset = HassIOPasswordReset(hass, user) + hass.http.register_view(hassio_auth) + hass.http.register_view(hassio_password_reset) -class HassIOAuth(HomeAssistantView): - """Hass.io view to handle base part.""" +class HassIOBaseAuth(HomeAssistantView): + """Hass.io view to handle auth requests.""" - name = "api:hassio_auth" - url = "/api/hassio_auth" - - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType, user: User): """Initialize WebView.""" self.hass = hass + self.user = user - @RequestDataValidator(SCHEMA_API_AUTH) - async def post(self, request, data): - """Handle new discovery requests.""" + def _check_access(self, request: web.Request): + """Check if this call is from Supervisor.""" + # Check caller IP hassio_ip = os.environ["HASSIO"].split(":")[0] if request[KEY_REAL_IP] != ip_address(hassio_ip): _LOGGER.error("Invalid auth request from %s", request[KEY_REAL_IP]) - raise HTTPForbidden() + raise HTTPUnauthorized() - await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) - return web.Response(status=200) + # Check caller token + if request[KEY_HASS_USER].id != self.user.id: + _LOGGER.error("Invalid auth request from %s", request[KEY_HASS_USER].name) + raise HTTPUnauthorized() def _get_provider(self): """Return Homeassistant auth provider.""" @@ -67,6 +80,21 @@ class HassIOAuth(HomeAssistantView): _LOGGER.error("Can't find Home Assistant auth.") raise HTTPNotFound() + +class HassIOAuth(HassIOBaseAuth): + """Hass.io view to handle auth requests.""" + + name = "api:hassio:auth" + url = "/api/hassio_auth" + + @RequestDataValidator(SCHEMA_API_AUTH) + async def post(self, request, data): + """Handle auth requests.""" + self._check_access(request) + + await self._check_login(data[ATTR_USERNAME], data[ATTR_PASSWORD]) + return web.Response(status=200) + async def _check_login(self, username, password): """Check User credentials.""" provider = self._get_provider() @@ -74,4 +102,31 @@ class HassIOAuth(HomeAssistantView): try: await provider.async_validate_login(username, password) except HomeAssistantError: - raise HTTPForbidden() from None + raise HTTPUnauthorized() from None + + +class HassIOPasswordReset(HassIOBaseAuth): + """Hass.io view to handle password reset requests.""" + + name = "api:hassio:auth:password:reset" + url = "/api/hassio_auth/password_reset" + + @RequestDataValidator(SCHEMA_API_PASSWORD_RESET) + async def post(self, request, data): + """Handle password reset requests.""" + self._check_access(request) + + await self._change_password(data[ATTR_USERNAME], data[ATTR_PASSWORD]) + return web.Response(status=200) + + async def _change_password(self, username, password): + """Check User credentials.""" + provider = self._get_provider() + + try: + await self.hass.async_add_executor_job( + provider.data.change_password, username, password + ) + await provider.data.async_save() + except HomeAssistantError: + raise HTTPInternalServerError() diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 5213443614c..e471bfae543 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -130,7 +130,7 @@ class HassIO: "ssl": CONF_SSL_CERTIFICATE in http_config, "port": port, "watchdog": True, - "refresh_token": refresh_token, + "refresh_token": refresh_token.token, } if CONF_SERVER_HOST in http_config: diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index ddb9269219b..be2cec5ae9c 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -31,7 +31,9 @@ NO_TIMEOUT = re.compile( r")$" ) -NO_AUTH = re.compile(r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r")$") +NO_AUTH = re.compile( + r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$" +) class HassIOView(HomeAssistantView): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 9016a8b3cea..39c5a9928af 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -60,11 +60,6 @@ CONTROL_TO_SUPPORT = { _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ): diff --git a/homeassistant/components/hisense_aehw4a1/.translations/de.json b/homeassistant/components/hisense_aehw4a1/.translations/de.json index 8b474ea0418..322c7e2f4c6 100644 --- a/homeassistant/components/hisense_aehw4a1/.translations/de.json +++ b/homeassistant/components/hisense_aehw4a1/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie Hisense AEH-W4A1 einrichten?", + "description": "M\u00f6chtest du Hisense AEH-W4A1 einrichten?", "title": "Hisense AEH-W4A1" } }, diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json index da8e3ad9419..a101ab6dd9f 100644 --- a/homeassistant/components/hisense_aehw4a1/manifest.json +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -3,7 +3,7 @@ "name": "Hisense AEH-W4A1", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", - "requirements": ["pyaehw4a1==0.3.1"], + "requirements": ["pyaehw4a1==0.3.4"], "dependencies": [], "codeowners": ["@bannhead"] } diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index 2b89556818f..e132b1d5d4c 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -35,6 +35,11 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Load graph configurations.""" + _LOGGER.warning( + "The history_graph integration has been deprecated and is pending for removal " + "in Home Assistant 0.107.0." + ) + component = EntityComponent(_LOGGER, DOMAIN, hass) graphs = [] diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index e7264c4e0dd..5ab16ed17e6 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -24,6 +24,7 @@ _LOGGER = logging.getLogger(__name__) DATA_DEVICE_REGISTER = "hlk_sw16_device_register" DEFAULT_RECONNECT_INTERVAL = 10 +DEFAULT_KEEP_ALIVE_INTERVAL = 3 CONNECTION_TIMEOUT = 10 DEFAULT_PORT = 8080 @@ -93,6 +94,7 @@ async def async_setup(hass, config): loop=hass.loop, timeout=CONNECTION_TIMEOUT, reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, ) hass.data[DATA_DEVICE_REGISTER][device] = client diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 741c81b367c..7df3238e287 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -2,7 +2,7 @@ "domain": "hlk_sw16", "name": "Hi-Link HLK-SW16", "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", - "requirements": ["hlk-sw16==0.0.7"], + "requirements": ["hlk-sw16==0.0.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c79c22e36a3..af5f4cea828 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,6 +1,7 @@ """Allow users to set and activate scenes.""" from collections import namedtuple import logging +from typing import List import voluptuous as vol @@ -17,7 +18,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import DOMAIN as HA_DOMAIN, State +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_per_platform, @@ -71,7 +72,7 @@ def _ensure_no_intersection(value): CONF_SCENE_ID = "scene_id" CONF_SNAPSHOT = "snapshot_entities" - +DATA_PLATFORM = f"homeassistant_scene" STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( @@ -108,6 +109,39 @@ SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) +@callback +def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all scenes that reference the entity.""" + if DATA_PLATFORM not in hass.data: + return [] + + platform = hass.data[DATA_PLATFORM] + + results = [] + + for scene_entity in platform.entities.values(): + if entity_id in scene_entity.scene_config.states: + results.append(scene_entity.entity_id) + + return results + + +@callback +def entities_in_scene(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all entities in a scene.""" + if DATA_PLATFORM not in hass.data: + return [] + + platform = hass.data[DATA_PLATFORM] + + entity = platform.entities.get(entity_id) + + if entity is None: + return [] + + return list(entity.scene_config.states) + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Home Assistant scene entries.""" _process_scenes_config(hass, async_add_entities, config) @@ -117,7 +151,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return # Store platform for later. - platform = entity_platform.current_platform.get() + platform = hass.data[DATA_PLATFORM] = entity_platform.current_platform.get() async def reload_config(call): """Reload the scene config.""" @@ -227,6 +261,11 @@ class HomeAssistantScene(Scene): """Return the name of the scene.""" return self.scene_config.name + @property + def unique_id(self): + """Return unique ID.""" + return self._id + @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 69e4554d81b..bbbc6561a87 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.6.0"], + "requirements": ["HAP-python==2.7.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 807941c7a6d..d77ea22dc96 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -119,7 +119,8 @@ class WindowCovering(HomeAccessory): def update_state(self, new_state): """Update cover position after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) - if isinstance(current_position, int): + if isinstance(current_position, (float, int)): + current_position = int(current_position) self.char_current_position.set_value(current_position) if ( self._homekit_target is None diff --git a/homeassistant/components/homekit_controller/.translations/de.json b/homeassistant/components/homekit_controller/.translations/de.json index 22420b79661..e6942a125cd 100644 --- a/homeassistant/components/homekit_controller/.translations/de.json +++ b/homeassistant/components/homekit_controller/.translations/de.json @@ -4,7 +4,7 @@ "accessory_not_found_error": "Die Kopplung kann nicht durchgef\u00fchrt werden, da das Ger\u00e4t nicht mehr gefunden werden kann.", "already_configured": "Das Zubeh\u00f6r ist mit diesem Controller bereits konfiguriert.", "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", - "already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setzen Sie das Zubeh\u00f6r zur\u00fcck und versuchen Sie es erneut.", + "already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setze das Zubeh\u00f6r zur\u00fcck und versuche es erneut.", "ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.", "invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.", "no_devices": "Keine ungekoppelten Ger\u00e4te gefunden" @@ -24,7 +24,7 @@ "data": { "pairing_code": "Kopplungscode" }, - "description": "Geben Sie Ihren HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", + "description": "Gebe deinen HomeKit-Kopplungscode ein, um dieses Zubeh\u00f6r zu verwenden", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "user": { diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 854c12e6f88..41194cb340c 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -2,6 +2,7 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.air_quality import AirQualityEntity +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -78,16 +79,12 @@ class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): return data -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit air quality sensor.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] != "air-quality": return False diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 8cdbe9b2f36..f4f89507fca 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -40,16 +41,12 @@ TARGET_STATE_MAP = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit alarm control panel.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] != "security-system": return False diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 2998ce18641..0a6a3fca1cf 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorDevice, ) +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -93,16 +94,12 @@ ENTITY_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lighting.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): entity_class = ENTITY_TYPES.get(service["stype"]) if not entity_class: diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index d0ab7bd2e99..bbef10d3204 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -20,6 +20,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -45,16 +46,12 @@ CURRENT_MODE_HOMEKIT_TO_HASS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit climate.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] != "thermostat": return False diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index f3e728c6cdc..64581da45b1 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -12,6 +12,7 @@ from homekit.exceptions import ( from homekit.model.characteristics import CharacteristicsTypes from homekit.model.services import ServicesTypes +from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH @@ -116,6 +117,7 @@ class HKDevice: char for char in self.pollable_characteristics if char[0] != accessory_id ] + @callback def async_set_unavailable(self): """Mark state of all entities on this connection as unavailable.""" self.available = False diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7e5591d9505..191405a9355 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -16,6 +16,7 @@ from homeassistant.components.cover import ( CoverDevice, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -36,16 +37,12 @@ TARGET_GARAGE_STATE_MAP = {STATE_OPEN: 0, STATE_CLOSED: 1, STATE_STOPPED: 2} CURRENT_WINDOW_STATE_MAP = {0: STATE_CLOSING, 1: STATE_OPENING, 2: STATE_STOPPED} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit covers.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): info = {"aid": aid, "iid": service["iid"]} if service["stype"] == "garage-door-opener": diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index efb41808429..694ae8a2c09 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -15,6 +15,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -230,16 +231,12 @@ ENTITY_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit fans.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): entity_class = ENTITY_TYPES.get(service["stype"]) if not entity_class: diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index fe2a0e9bc97..e7d1e4d3273 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -12,22 +12,19 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, Light, ) +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lightbulb.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] != "lightbulb": return False diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 53f7bb5dfd5..1799d30d8c8 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -5,6 +5,7 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -17,16 +18,12 @@ CURRENT_STATE_MAP = {0: STATE_UNLOCKED, 1: STATE_LOCKED, 2: STATE_JAMMED, 3: Non TARGET_STATE_MAP = {STATE_UNLOCKED: 0, STATE_LOCKED: 1} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lock.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] != "lock-mechanism": return False diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index f91dae26ba0..e59dda007d4 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -2,6 +2,7 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.const import DEVICE_CLASS_BATTERY, TEMP_CELSIUS +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -241,16 +242,12 @@ ENTITY_TYPES = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit sensors.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): entity_class = ENTITY_TYPES.get(service["stype"]) if not entity_class: diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ffc2da5fbf2..ffc5bdc2381 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -45,6 +45,7 @@ class EntityMapStorage: """Get a pairing cache item.""" return self.storage_data.get(homekit_id) + @callback def async_create_or_update_map(self, homekit_id, config_num, accessories): """Create a new pairing cache.""" data = {"config_num": config_num, "accessories": accessories} @@ -52,6 +53,7 @@ class EntityMapStorage: self._async_schedule_save() return data + @callback def async_delete_map(self, homekit_id): """Delete pairing cache.""" if homekit_id not in self.storage_data: diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 7eedda1b191..60b16c8ddab 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -4,6 +4,7 @@ import logging from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -12,16 +13,12 @@ OUTLET_IN_USE = "outlet_in_use" _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Legacy set up platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Homekit lock.""" hkid = config_entry.data["AccessoryPairingID"] conn = hass.data[KNOWN_DEVICES][hkid] + @callback def async_add_service(aid, service): if service["stype"] not in ("switch", "outlet"): return False diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 4ed893bbf14..54811c3ccdf 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -40,6 +40,7 @@ class HMDevice(Entity): self._hmdevice = None self._connected = False self._available = False + self._channel_map = set() # Set parameter to uppercase if self._state: @@ -110,15 +111,12 @@ class HMDevice(Entity): def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" - _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) has_changed = False # Is data needed for this instance? - if attribute in self._data: - # Did data change? - if self._data[attribute] != value: - self._data[attribute] = value - has_changed = True + if f"{attribute}:{device.partition(':')[2]}" in self._channel_map: + self._data[attribute] = value + has_changed = True # Availability has changed if self.available != (not self._hmdevice.UNREACH): @@ -131,9 +129,6 @@ class HMDevice(Entity): def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" - channels_to_sub = set() - - # Push data to channels_to_sub from hmdevice metadata for metadata in ( self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, @@ -150,19 +145,11 @@ class HMDevice(Entity): channel = channels[0] else: channel = self._channel - - # Prepare for subscription - try: - channels_to_sub.add(int(channel)) - except (ValueError, TypeError): - _LOGGER.error("Invalid channel in metadata from %s", self._name) + # Remember the channel for this attribute to ignore invalid events later + self._channel_map.add(f"{node}:{channel!s}") # Set callbacks - for channel in channels_to_sub: - _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) - self._hmdevice.setEventCallback( - callback=self._hm_event_callback, bequeath=False, channel=channel - ) + self._hmdevice.setEventCallback(callback=self._hm_event_callback, bequeath=True) def _load_data_from_hm(self): """Load first value from pyhomematic.""" diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f3e1fc9fbec..46bf300753f 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -17,7 +17,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .config_flow import configured_haps from .const import ( CONF_ACCESSPOINT, CONF_AUTHTOKEN, @@ -130,7 +129,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: accesspoints = config.get(DOMAIN, []) for conf in accesspoints: - if conf[CONF_ACCESSPOINT] not in configured_haps(hass): + if conf[CONF_ACCESSPOINT] not in set( + entry.data[HMIPC_HAPID] + for entry in hass.config_entries.async_entries(DOMAIN) + ): hass.async_add_job( hass.config_entries.flow.async_init( DOMAIN, @@ -274,7 +276,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: anonymize = service.data[ATTR_ANONYMIZE] for hap in hass.data[DOMAIN].values(): - hap_sgtin = hap.config_entry.title + hap_sgtin = hap.config_entry.unique_id if anonymize: hap_sgtin = hap_sgtin[-4:] @@ -331,9 +333,17 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" + + # 0.104 introduced config entry unique id, this makes upgrading possible + if entry.unique_id is None: + new_data = dict(entry.data) + + hass.config_entries.async_update_entry( + entry, unique_id=new_data[HMIPC_HAPID], data=new_data + ) + hap = HomematicipHAP(hass, entry) - hapid = entry.data[HMIPC_HAPID].replace("-", "").upper() - hass.data[DOMAIN][hapid] = hap + hass.data[DOMAIN][entry.unique_id] = hap if not await hap.async_setup(): return False @@ -356,5 +366,5 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) + hap = hass.data[DOMAIN].pop(entry.unique_id) return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f9a91203426..c2f4d833a35 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID +from . import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -26,18 +26,11 @@ _LOGGER = logging.getLogger(__name__) CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud alarm control devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] async_add_entities([HomematicipAlarmControlPanel(hap)]) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 3efd4ad91bc..f16dfc986f0 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -41,7 +41,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -75,18 +75,11 @@ SAM_DEVICE_ATTRIBUTES = { } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud binary sensor devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index e3c922dc577..c5fb978e690 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -27,7 +27,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .hap import HomematicipHAP HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} @@ -43,18 +43,11 @@ HMIP_MANUAL_CM = "MANUAL" HMIP_ECO_CM = "ECO" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud climate devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 8d85dfda328..547289f871a 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,11 +1,9 @@ """Config flow to configure the HomematicIP Cloud component.""" -from typing import Any, Dict, Set +from typing import Any, Dict import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback -from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -18,15 +16,6 @@ from .const import ( from .hap import HomematicipAuth -@callback -def configured_haps(hass: HomeAssistantType) -> Set[str]: - """Return a set of the configured access points.""" - return set( - entry.data[HMIPC_HAPID] - for entry in hass.config_entries.async_entries(HMIPC_DOMAIN) - ) - - @config_entries.HANDLERS.register(HMIPC_DOMAIN) class HomematicipCloudFlowHandler(config_entries.ConfigFlow): """Config flow for the HomematicIP Cloud component.""" @@ -48,8 +37,9 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): if user_input is not None: user_input[HMIPC_HAPID] = user_input[HMIPC_HAPID].replace("-", "").upper() - if user_input[HMIPC_HAPID] in configured_haps(self.hass): - return self.async_abort(reason="already_configured") + + await self.async_set_unique_id(user_input[HMIPC_HAPID]) + self._abort_if_unique_id_configured() self.auth = HomematicipAuth(self.hass, user_input) connected = await self.auth.async_setup() @@ -93,16 +83,14 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): async def async_step_import(self, import_info) -> Dict[str, Any]: """Import a new access point as a config entry.""" - hapid = import_info[HMIPC_HAPID] + hapid = import_info[HMIPC_HAPID].replace("-", "").upper() authtoken = import_info[HMIPC_AUTHTOKEN] name = import_info[HMIPC_NAME] - hapid = hapid.replace("-", "").upper() - if hapid in configured_haps(self.hass): - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(hapid) + self._abort_if_unique_id_configured() _LOGGER.info("Imported authentication for %s", hapid) - return self.async_create_entry( title=hapid, data={HMIPC_AUTHTOKEN: authtoken, HMIPC_HAPID: hapid, HMIPC_NAME: name}, diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 32f38637e36..0d2131f9cb3 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -17,7 +17,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice _LOGGER = logging.getLogger(__name__) @@ -27,18 +27,11 @@ HMIP_SLATS_OPEN = 0 HMIP_SLATS_CLOSED = 1 -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud cover devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 63bdf3166eb..3c97cc1af9f 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -95,8 +95,7 @@ class HomematicipHAP: raise ConfigEntryNotReady _LOGGER.info( - "Connected to HomematicIP with HAP %s", - self.config_entry.data.get(HMIPC_HAPID), + "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) for component in COMPONENTS: @@ -193,7 +192,7 @@ class HomematicipHAP: _LOGGER.error( "Error connecting to HomematicIP with HAP %s. " "Retrying in %d seconds", - self.config_entry.data.get(HMIPC_HAPID), + self.config_entry.unique_id, retry_delay, ) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 79083f031ae..4e081f4d8fa 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -25,7 +25,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -34,18 +34,11 @@ ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" ATTR_CURRENT_POWER_W = "current_power_w" -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Old way of setting up HomematicIP Cloud lights.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index e920a847292..d823621f6cb 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==0.10.15"], "dependencies": [], - "codeowners": ["@SukramJ"] + "codeowners": ["@SukramJ"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index a8ca3d17eb9..ebbee1abc44 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -34,7 +34,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE from .hap import HomematicipHAP @@ -56,18 +56,11 @@ ILLUMINATION_DEVICE_ATTRIBUTES = { } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud sensors devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [HomematicipAccesspointStatus(hap)] for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 6fdb0b8c95c..45adf54df2b 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,25 +19,18 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud switch devices.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index ebc7eacf78e..04f3b06cbb0 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericDevice from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -37,18 +37,11 @@ HOME_WEATHER_CONDITION = { } -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None -) -> None: - """Set up the HomematicIP Cloud weather sensor.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" - hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 5d17824e0cb..aed28949591 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,6 +1,6 @@ { "domain": "honeywell", - "name": "Honeywell Total Connect Comfort (TCC)", + "name": "Honeywell Total Connect Comfort (US)", "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": ["somecomfort==0.5.2"], "dependencies": [], diff --git a/homeassistant/components/hook/__init__.py b/homeassistant/components/hook/__init__.py deleted file mode 100644 index bc85e27d742..00000000000 --- a/homeassistant/components/hook/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hook component.""" diff --git a/homeassistant/components/hook/manifest.json b/homeassistant/components/hook/manifest.json deleted file mode 100644 index 035354c969a..00000000000 --- a/homeassistant/components/hook/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "hook", - "name": "Hook", - "documentation": "https://www.home-assistant.io/integrations/hook", - "requirements": [], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py deleted file mode 100644 index 582dc61af14..00000000000 --- a/homeassistant/components/hook/switch.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Support Hook, available at hooksmarthome.com.""" -import asyncio -import logging - -import aiohttp -import async_timeout -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -HOOK_ENDPOINT = "https://api.gethook.io/v1/" -TIMEOUT = 10 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Exclusive( - CONF_PASSWORD, - "hook_secret", - msg="hook: provide username/password OR token", - ): cv.string, - vol.Exclusive( - CONF_TOKEN, "hook_secret", msg="hook: provide username/password OR token", - ): cv.string, - vol.Inclusive(CONF_USERNAME, "hook_auth"): cv.string, - vol.Inclusive(CONF_PASSWORD, "hook_auth"): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Hook by getting the access token and list of actions.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - token = config.get(CONF_TOKEN) - websession = async_get_clientsession(hass) - # If password is set in config, prefer it over token - if username is not None and password is not None: - try: - with async_timeout.timeout(TIMEOUT): - response = await websession.post( - "{}{}".format(HOOK_ENDPOINT, "user/login"), - data={"username": username, "password": password}, - ) - # The Hook API returns JSON but calls it 'text/html'. Setting - # content_type=None disables aiohttp's content-type validation. - data = await response.json(content_type=None) - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed authentication API call: %s", error) - return False - - try: - token = data["data"]["token"] - except KeyError: - _LOGGER.error("No token. Check username and password") - return False - - try: - with async_timeout.timeout(TIMEOUT): - response = await websession.get( - "{}{}".format(HOOK_ENDPOINT, "device"), params={"token": token} - ) - data = await response.json(content_type=None) - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed getting devices: %s", error) - return False - - async_add_entities( - HookSmartHome(hass, token, d["device_id"], d["device_name"]) - for lst in data["data"] - for d in lst - ) - - -class HookSmartHome(SwitchDevice): - """Representation of a Hook device, allowing on and off commands.""" - - def __init__(self, hass, token, device_id, device_name): - """Initialize the switch.""" - self.hass = hass - self._token = token - self._state = False - self._id = device_id - self._name = device_name - _LOGGER.debug("Creating Hook object: ID: %s Name: %s", self._id, self._name) - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - async def _send(self, url): - """Send the url to the Hook API.""" - try: - _LOGGER.debug("Sending: %s", url) - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(TIMEOUT): - response = await websession.get(url, params={"token": self._token}) - data = await response.json(content_type=None) - - except (asyncio.TimeoutError, aiohttp.ClientError) as error: - _LOGGER.error("Failed setting state: %s", error) - return False - - _LOGGER.debug("Got: %s", data) - return data["return_value"] == "1" - - async def async_turn_on(self, **kwargs): - """Turn the device on asynchronously.""" - _LOGGER.debug("Turning on: %s", self._name) - url = "{}{}{}{}".format(HOOK_ENDPOINT, "device/trigger/", self._id, "/On") - success = await self._send(url) - self._state = success - - async def async_turn_off(self, **kwargs): - """Turn the device off asynchronously.""" - _LOGGER.debug("Turning off: %s", self._name) - url = "{}{}{}{}".format(HOOK_ENDPOINT, "device/trigger/", self._id, "/Off") - success = await self._send(url) - # If it wasn't successful, keep state as true - self._state = not success diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 0d93461f90f..565f84fdb8a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -3,7 +3,7 @@ from ipaddress import ip_network import logging import os import ssl -from typing import Optional +from typing import Optional, cast from aiohttp import web from aiohttp.web_exceptions import HTTPMovedPermanently @@ -14,7 +14,10 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVER_PORT, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util @@ -56,6 +59,9 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1 MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + HTTP_SCHEMA = vol.Schema( { @@ -85,6 +91,13 @@ HTTP_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) +@bind_hass +async def async_get_last_config(hass: HomeAssistant) -> Optional[dict]: + """Return the last known working config.""" + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + return cast(Optional[dict], await store.async_load()) + + class ApiConfig: """Configuration settings for API server.""" @@ -151,6 +164,19 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server) await server.start() + # If we are set up successful, we store the HTTP settings for safe mode. + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + + if CONF_TRUSTED_PROXIES in conf: + conf_to_save = dict(conf) + conf_to_save[CONF_TRUSTED_PROXIES] = [ + str(ip.network_address) for ip in conf_to_save[CONF_TRUSTED_PROXIES] + ] + else: + conf_to_save = conf + + await store.async_save(conf_to_save) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) hass.http = server diff --git a/homeassistant/components/huawei_lte/.translations/de.json b/homeassistant/components/huawei_lte/.translations/de.json index ddf6ad55eaa..a346753aeb3 100644 --- a/homeassistant/components/huawei_lte/.translations/de.json +++ b/homeassistant/components/huawei_lte/.translations/de.json @@ -12,7 +12,7 @@ "incorrect_username": "Ung\u00fcltiger Benutzername", "incorrect_username_or_password": "Ung\u00fcltiger Benutzername oder Kennwort", "invalid_url": "Ung\u00fcltige URL", - "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuchen Sie es sp\u00e4ter erneut", + "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuche es sp\u00e4ter erneut", "response_error": "Unbekannter Fehler vom Ger\u00e4t", "unknown_connection_error": "Unbekannter Fehler beim Herstellen der Verbindung zum Ger\u00e4t" }, @@ -23,8 +23,8 @@ "url": "URL", "username": "Benutzername" }, - "description": "Geben Sie die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.", - "title": "Konfigurieren Sie Huawei LTE" + "description": "Gib die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.", + "title": "Konfiguriere Huawei LTE" } }, "title": "Huawei LTE" diff --git a/homeassistant/components/huawei_lte/.translations/sv.json b/homeassistant/components/huawei_lte/.translations/sv.json new file mode 100644 index 00000000000..fb73612d897 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Den h\u00e4r enheten har redan konfigurerats" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 97a57405ae0..1b8cb658c28 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -506,6 +506,19 @@ async def async_signal_options_update( async_dispatcher_send(hass, UPDATE_OPTIONS_SIGNAL, config_entry) +async def async_migrate_entry(hass: HomeAssistantType, config_entry: ConfigEntry): + """Migrate config entry to new version.""" + if config_entry.version == 1: + options = config_entry.options + recipient = options[CONF_RECIPIENT] + if isinstance(recipient, str): + options[CONF_RECIPIENT] = [x.strip() for x in recipient.split(",")] + config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, options=options) + _LOGGER.info("Migrated config entry to version %d", config_entry.version) + return True + + @attr.s class HuaweiLteBaseEntity(Entity): """Huawei LTE entity base class.""" diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 0dcdb6636c6..223ca9dc34a 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -40,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Huawei LTE config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @@ -247,9 +247,16 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" + + # Recipients are persisted as a list, but handled as comma separated string in UI + if user_input is not None: # Preserve existing options, for example *_from_yaml markers data = {**self.config_entry.options, **user_input} + if not isinstance(data[CONF_RECIPIENT], list): + data[CONF_RECIPIENT] = [ + x.strip() for x in data[CONF_RECIPIENT].split(",") + ] return self.async_create_entry(title="", data=data) data_schema = vol.Schema( @@ -262,7 +269,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ): str, vol.Optional( CONF_RECIPIENT, - default=self.config_entry.options.get(CONF_RECIPIENT, ""), + default=", ".join( + self.config_entry.options.get(CONF_RECIPIENT, []) + ), ): str, } ) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index a9c61831fdd..54e8f318cf6 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -13,6 +13,7 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.const import CONF_URL +from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -70,6 +71,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_new_entities(hass, router.url, async_add_entities, tracked) +@callback def async_add_new_entities(hass, router_url, async_add_entities, tracked): """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 1f5fa69d341..8525b9eeaad 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.4.4", + "huawei-lte-api==1.4.7", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/hue/.translations/cs.json b/homeassistant/components/hue/.translations/cs.json index 82be2e7fb00..260dbc4021a 100644 --- a/homeassistant/components/hue/.translations/cs.json +++ b/homeassistant/components/hue/.translations/cs.json @@ -20,7 +20,7 @@ "title": "Vybrat Hue p\u0159emost\u011bn\u00ed" }, "link": { - "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed] (/ static/images/config_philips_hue.jpg)", + "description": "Stiskn\u011bte tla\u010d\u00edtko na p\u0159emost\u011bn\u00ed k registraci Philips Hue v Home Assistant.\n\n! [Um\u00edst\u011bn\u00ed tla\u010d\u00edtka na p\u0159emost\u011bn\u00ed](/ static/images/config_philips_hue.jpg)", "title": "P\u0159ipojit Hub" } }, diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index 3ec9ed871d3..bc41d3d2df0 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -22,7 +22,7 @@ "title": "Elige el puente de Hue" }, "link": { - "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_philips_hue.jpg)", + "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en el puente](/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index 55b308b2373..de54ddb3bf3 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -22,7 +22,7 @@ "title": "Choisissez le pont Philips Hue" }, "link": { - "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont] (/static/images/config_philips_hue.jpg)", + "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_philips_hue.jpg)", "title": "Hub de liaison" } }, diff --git a/homeassistant/components/hue/.translations/id.json b/homeassistant/components/hue/.translations/id.json index bf5557436ce..253dedb7c4d 100644 --- a/homeassistant/components/hue/.translations/id.json +++ b/homeassistant/components/hue/.translations/id.json @@ -20,7 +20,7 @@ "title": "Pilih Hue bridge" }, "link": { - "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge] (/static/images/config_philips_hue.jpg)", + "description": "Tekan tombol di bridge untuk mendaftar Philips Hue dengan Home Assistant.\n\n![Lokasi tombol di bridge](/static/images/config_philips_hue.jpg)", "title": "Tautan Hub" } }, diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json index 9b84b4a7afc..0c7a1bfb60d 100644 --- a/homeassistant/components/hue/.translations/nl.json +++ b/homeassistant/components/hue/.translations/nl.json @@ -22,7 +22,7 @@ "title": "Kies Hue bridge" }, "link": { - "description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. \n\n![Locatie van de knop op bridge](/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/nn.json b/homeassistant/components/hue/.translations/nn.json index 45d6bc89d72..744a8e10c22 100644 --- a/homeassistant/components/hue/.translations/nn.json +++ b/homeassistant/components/hue/.translations/nn.json @@ -20,7 +20,7 @@ "title": "Vel Hue bru" }, "link": { - "description": "Trykk p\u00e5 knappen p\u00e5 brua, for \u00e5 registrere Philips Hue med Home Assistant.\n\n![Lokasjon til knappen p\u00e5 brua]\n(/statisk/bilete/konfiguer_philips_hue.jpg)", + "description": "Trykk p\u00e5 knappen p\u00e5 brua, for \u00e5 registrere Philips Hue med Home Assistant.\n\n![Lokasjon til knappen p\u00e5 brua](/statisk/bilete/konfiguer_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json index 92187b7f54f..3f69f9803a4 100644 --- a/homeassistant/components/hue/.translations/pt-BR.json +++ b/homeassistant/components/hue/.translations/pt-BR.json @@ -22,7 +22,7 @@ "title": "Escolha a ponte Hue" }, "link": { - "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte] (/static/images/config_philips_hue.jpg)", + "description": "Pressione o bot\u00e3o na ponte para registrar o Philips Hue com o Home Assistant. \n\n ![Localiza\u00e7\u00e3o do bot\u00e3o na ponte](/static/images/config_philips_hue.jpg)", "title": "Hub de links" } }, diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json index d52540b0921..a3e755fa790 100644 --- a/homeassistant/components/hue/.translations/pt.json +++ b/homeassistant/components/hue/.translations/pt.json @@ -20,7 +20,7 @@ "title": "Hue bridge" }, "link": { - "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n ! [Localiza\u00e7\u00e3o do bot\u00e3o] (/ static / images / config_philips_hue.jpg)", + "description": "Pressione o bot\u00e3o no Philips Hue para registrar com o Home Assistant. \n\n![Localiza\u00e7\u00e3o do bot\u00e3o] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index 9da771a52dc..4866ace08d6 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -17,7 +17,7 @@ } }, "link": { - "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" + "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n![Loca\u021bia butonului pe pod](/static/images/config_philips_hue.jpg)" } }, "title": "Philips Hue" diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json index 29fc66488eb..09083742310 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -22,7 +22,7 @@ "title": "Izberite Hue most" }, "link": { - "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistantom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", + "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistantom. \n\n![Polo\u017eaj gumba na mostu](/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json index 7e5b7c52dd5..aedfc0c0f40 100644 --- a/homeassistant/components/hue/.translations/sv.json +++ b/homeassistant/components/hue/.translations/sv.json @@ -22,7 +22,7 @@ "title": "V\u00e4lj Hue-brygga" }, "link": { - "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n ! [Placering av knapp p\u00e5 brygga] (/ static / images / config_philips_hue.jpg)", + "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n![Placering av knapp p\u00e5 brygga](/static/images/config_philips_hue.jpg)", "title": "L\u00e4nka hub" } }, diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7349f4fe6a6..c8864e97607 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -122,7 +122,7 @@ async def async_setup_entry( if not await bridge.async_setup(): return False - hass.data[DOMAIN][host] = bridge + hass.data[DOMAIN][entry.entry_id] = bridge config = bridge.api.config # For backwards compat @@ -151,5 +151,5 @@ async def async_setup_entry( async def async_unload_entry(hass, entry): """Unload a config entry.""" - bridge = hass.data[DOMAIN].pop(entry.data["host"]) + bridge = hass.data[DOMAIN].pop(entry.entry_id) return await bridge.async_reset() diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index e4b7dd85e37..319f8f5fa19 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -6,27 +6,18 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, BinarySensorDevice, ) -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - SensorManager, - async_setup_entry as shared_async_setup_entry, -) + +from .const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" - SensorManager.sensor_config_map.update( - { - TYPE_ZLL_PRESENCE: { - "binary": True, - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - } - } - ) - await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True) + await hass.data[HUE_DOMAIN][ + config_entry.entry_id + ].sensor_manager.async_register_component(True, async_add_entities) class HuePresence(GenericZLLSensor, BinarySensorDevice): @@ -34,9 +25,6 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): device_class = DEVICE_CLASS_MOTION - async def _async_update_ha_state(self, *args, **kwargs): - await self.async_update_ha_state(self, *args, **kwargs) - @property def is_on(self): """Return true if the binary sensor is on.""" @@ -51,3 +39,14 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): if "sensitivitymax" in self.sensor.config: attributes["sensitivity_max"] = self.sensor.config["sensitivitymax"] return attributes + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } +) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 58a744dd5b0..a153ed7a096 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -13,6 +13,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect from .helpers import create_config_flow +from .sensor_base import SensorManager SERVICE_HUE_SCENE = "hue_activate_scene" ATTR_GROUP_NAME = "group_name" @@ -35,6 +36,9 @@ class HueBridge: self.authorized = False self.api = None self.parallel_updates_semaphore = None + # Jobs to be executed when API is reset. + self.reset_jobs = [] + self.sensor_manager = None @property def host(self): @@ -72,6 +76,7 @@ class HueBridge: return False self.api = bridge + self.sensor_manager = SensorManager(self) hass.async_create_task( hass.config_entries.async_forward_entry_setup(self.config_entry, "light") @@ -118,6 +123,9 @@ class HueBridge: self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + while self.reset_jobs: + self.reset_jobs.pop()() + # If setup was successful, we set api variable, forwarded entry and # register service results = await asyncio.gather( @@ -131,6 +139,7 @@ class HueBridge: self.config_entry, "sensor" ), ) + # None and True are OK return False not in results diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 66b9c97a58a..a46f8816fbb 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge @@ -169,7 +170,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host}) + self.bridge = bridge return await self.async_step_link() @@ -180,7 +182,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host}) + self.bridge = bridge return await self.async_step_link() diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py index d884389c0c1..e48cd4a8583 100644 --- a/homeassistant/components/hue/const.py +++ b/homeassistant/components/hue/const.py @@ -4,3 +4,7 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "hue" API_NUPNP = "https://www.meethue.com/api/nupnp" + +# How long to wait to actually do the refresh after requesting it. +# We wait some time so if we control multiple lights, we batch requests. +REQUEST_REFRESH_DELAY = 0.3 diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 8a5fa973e4f..885677dc269 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -6,7 +6,7 @@ from homeassistant.helpers.entity_registry import async_get_registry as get_ent_ from .const import DOMAIN -async def remove_devices(hass, config_entry, api_ids, current): +async def remove_devices(bridge, api_ids, current): """Get items that are removed from api.""" removed_items = [] @@ -18,16 +18,16 @@ async def remove_devices(hass, config_entry, api_ids, current): entity = current[item_id] removed_items.append(item_id) await entity.async_remove() - ent_registry = await get_ent_reg(hass) + ent_registry = await get_ent_reg(bridge.hass) if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(hass) + dev_registry = await get_dev_reg(bridge.hass) device = dev_registry.async_get_device( identifiers={(DOMAIN, entity.device_id)}, connections=set() ) if device is not None: dev_registry.async_update_device( - device.id, remove_config_entry_id=config_entry.entry_id + device.id, remove_config_entry_id=bridge.config_entry.entry_id ) for item_id in removed_items: diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 2a668779cb5..7ed2dcc84f2 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -1,14 +1,13 @@ """Support for the Philips Hue lights.""" import asyncio from datetime import timedelta +from functools import partial import logging import random -from time import monotonic import aiohue import async_timeout -from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -28,8 +27,13 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) +from homeassistant.core import callback +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import color +from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) @@ -70,9 +74,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Hue lights from a config entry.""" - bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - cur_lights = {} - cur_groups = {} + bridge = hass.data[HUE_DOMAIN][config_entry.entry_id] + + light_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + "light", + partial(async_safe_fetch, bridge, bridge.api.lights.update), + SCAN_INTERVAL, + Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) + + # First do a refresh to see if we can reach the hub. + # Otherwise we will declare not ready. + await light_coordinator.async_refresh() + + if light_coordinator.failed_last_update: + raise PlatformNotReady + + update_lights = partial( + async_update_items, + bridge, + bridge.api.lights, + {}, + async_add_entities, + partial(HueLight, light_coordinator, bridge, False), + ) + + # We add a listener after fetching the data, so manually trigger listener + light_coordinator.async_add_listener(update_lights) + update_lights() + + bridge.reset_jobs.append( + lambda: light_coordinator.async_remove_listener(update_lights) + ) api_version = tuple(int(v) for v in bridge.api.config.apiversion.split(".")) @@ -81,168 +116,60 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.warning("Please update your Hue bridge to support groups") allow_groups = False - # Hue updates all lights via a single API call. - # - # If we call a service to update 2 lights, we only want the API to be - # called once. - # - # The throttle decorator will return right away if a call is currently - # in progress. This means that if we are updating 2 lights, the first one - # is in the update method, the second one will skip it and assume the - # update went through and updates it's data, not good! - # - # The current mechanism will make sure that all lights will wait till - # the update call is done before writing their data to the state machine. - # - # An alternative approach would be to disable automatic polling by Home - # Assistant and take control ourselves. This works great for polling as now - # we trigger from 1 time update an update to all entities. However it gets - # tricky from inside async_turn_on and async_turn_off. - # - # If automatic polling is enabled, Home Assistant will call the entity - # update method after it is done calling all the services. This means that - # when we update, we know all commands have been processed. If we trigger - # the update from inside async_turn_on, the update will not capture the - # changes to the second entity until the next polling update because the - # throttle decorator will prevent the call. - - progress = None - light_progress = set() - group_progress = set() - - async def request_update(is_group, object_id): - """Request an update. - - We will only make 1 request to the server for updating at a time. If a - request is in progress, we will join the request that is in progress. - - This approach is possible because should_poll=True. That means that - Home Assistant will ask lights for updates during a polling cycle or - after it has called a service. - - We keep track of the lights that are waiting for the request to finish. - When new data comes in, we'll trigger an update for all non-waiting - lights. This covers the case where a service is called to enable 2 - lights but in the meanwhile some other light has changed too. - """ - nonlocal progress - - progress_set = group_progress if is_group else light_progress - progress_set.add(object_id) - - if progress is not None: - return await progress - - progress = asyncio.ensure_future(update_bridge()) - result = await progress - progress = None - light_progress.clear() - group_progress.clear() - return result - - async def update_bridge(): - """Update the values of the bridge. - - Will update lights and, if enabled, groups from the bridge. - """ - tasks = [] - tasks.append( - async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_update, - False, - cur_lights, - light_progress, - ) - ) - - if allow_groups: - tasks.append( - async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_update, - True, - cur_groups, - group_progress, - ) - ) - - await asyncio.wait(tasks) - - await update_bridge() - - -async def async_update_items( - hass, - config_entry, - bridge, - async_add_entities, - request_bridge_update, - is_group, - current, - progress_waiting, -): - """Update either groups or lights from the bridge.""" - if not bridge.authorized: + if not allow_groups: return - if is_group: - api_type = "group" - api = bridge.api.groups - else: - api_type = "light" - api = bridge.api.lights + group_coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + "group", + partial(async_safe_fetch, bridge, bridge.api.groups.update), + SCAN_INTERVAL, + Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) + update_groups = partial( + async_update_items, + bridge, + bridge.api.groups, + {}, + async_add_entities, + partial(HueLight, group_coordinator, bridge, True), + ) + + group_coordinator.async_add_listener(update_groups) + await group_coordinator.async_refresh() + + bridge.reset_jobs.append( + lambda: group_coordinator.async_remove_listener(update_groups) + ) + + +async def async_safe_fetch(bridge, fetch_method): + """Safely fetch data.""" try: - start = monotonic() with async_timeout.timeout(4): - await bridge.async_request_call(api.update()) + return await bridge.async_request_call(fetch_method()) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() - return - except (asyncio.TimeoutError, aiohue.AiohueException) as err: - _LOGGER.debug("Failed to fetch %s: %s", api_type, err) + raise UpdateFailed + except (asyncio.TimeoutError, aiohue.AiohueException): + raise UpdateFailed - if not bridge.available: - return - - _LOGGER.error("Unable to reach bridge %s (%s)", bridge.host, err) - bridge.available = False - - for item_id, item in current.items(): - if item_id not in progress_waiting: - item.async_schedule_update_ha_state() - - return - - finally: - _LOGGER.debug( - "Finished %s request in %.3f seconds", api_type, monotonic() - start - ) - - if not bridge.available: - _LOGGER.info("Reconnected to bridge %s", bridge.host) - bridge.available = True +@callback +def async_update_items(bridge, api, current, async_add_entities, create_item): + """Update items.""" new_items = [] for item_id in api: - if item_id not in current: - current[item_id] = HueLight( - api[item_id], request_bridge_update, bridge, is_group - ) + if item_id in current: + continue - new_items.append(current[item_id]) - elif item_id not in progress_waiting: - current[item_id].async_schedule_update_ha_state() + current[item_id] = create_item(api[item_id]) + new_items.append(current[item_id]) - await remove_devices(hass, config_entry, api, current) + bridge.hass.async_create_task(remove_devices(bridge, api, current)) if new_items: async_add_entities(new_items) @@ -251,10 +178,10 @@ async def async_update_items( class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light, request_bridge_update, bridge, is_group=False): + def __init__(self, coordinator, bridge, is_group, light): """Initialize the light.""" self.light = light - self.async_request_bridge_update = request_bridge_update + self.coordinator = coordinator self.bridge = bridge self.is_group = is_group @@ -289,6 +216,11 @@ class HueLight(Light): """Return the unique ID of this Hue light.""" return self.light.uniqueid + @property + def should_poll(self): + """No polling required.""" + return False + @property def device_id(self): """Return the ID of this Hue light.""" @@ -345,14 +277,10 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return ( - self.bridge.available - and self.bridge.authorized - and ( - self.is_group - or self.bridge.allow_unreachable - or self.light.state["reachable"] - ) + return not self.coordinator.failed_last_update and ( + self.is_group + or self.bridge.allow_unreachable + or self.light.state["reachable"] ) @property @@ -379,7 +307,7 @@ class HueLight(Light): return None return { - "identifiers": {(hue.DOMAIN, self.device_id)}, + "identifiers": {(HUE_DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, # productname added in Hue Bridge API 1.24 @@ -387,9 +315,17 @@ class HueLight(Light): "model": self.light.productname or self.light.modelid, # Not yet exposed as properties in aiohue "sw_version": self.light.raw["swversion"], - "via_device": (hue.DOMAIN, self.bridge.api.config.bridgeid), + "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {"on": True} @@ -440,6 +376,8 @@ class HueLight(Light): else: await self.bridge.async_request_call(self.light.set_state(**command)) + await self.coordinator.async_request_refresh() + async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" command = {"on": False} @@ -463,9 +401,14 @@ class HueLight(Light): else: await self.bridge.async_request_call(self.light.set_state(**command)) + await self.coordinator.async_request_refresh() + async def async_update(self): - """Synchronize state with bridge.""" - await self.async_request_bridge_update(self.is_group, self.light.id) + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() @property def device_state_attributes(self): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index f6b14ec69d4..ea01da0980f 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -6,7 +6,16 @@ "requirements": ["aiohue==1.10.1"], "ssdp": [ { - "manufacturer": "Royal Philips Electronics" + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2012" + }, + { + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2015" + }, + { + "manufacturer": "Signify", + "modelName": "Philips hue bridge 2015" } ], "homekit": { diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index f2e02d49ecf..5fa2ed68389 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,11 +1,6 @@ """Hue sensor entities.""" from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - SensorManager, - async_setup_entry as shared_async_setup_entry, -) from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, @@ -13,27 +8,18 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity +from .const import DOMAIN as HUE_DOMAIN +from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor + LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" - SensorManager.sensor_config_map.update( - { - TYPE_ZLL_LIGHTLEVEL: { - "binary": False, - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - TYPE_ZLL_TEMPERATURE: { - "binary": False, - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - } - ) - await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False) + await hass.data[HUE_DOMAIN][ + config_entry.entry_id + ].sensor_manager.async_register_component(False, async_add_entities) class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity): @@ -91,3 +77,19 @@ class HueTemperature(GenericHueGaugeSensorEntity): return None return self.sensor.temperature / 100 + + +SENSOR_CONFIG_MAP.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + } +) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index f7882b102c0..f57b0f98d30 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -2,22 +2,19 @@ import asyncio from datetime import timedelta import logging -from time import monotonic from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout -from homeassistant.components import hue -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.util.dt import utcnow +from homeassistant.core import callback +from homeassistant.helpers import debounce, entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import DOMAIN as HUE_DOMAIN, REQUEST_REFRESH_DELAY from .helpers import remove_devices -CURRENT_SENSORS_FORMAT = "{}_current_sensors" -SENSOR_MANAGER_FORMAT = "{}_sensor_manager" - +SENSOR_CONFIG_MAP = {} _LOGGER = logging.getLogger(__name__) @@ -29,22 +26,6 @@ def _device_id(aiohue_sensor): return device_id -async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): - """Set up the Hue sensors from a config entry.""" - sensor_key = CURRENT_SENSORS_FORMAT.format(config_entry.data["host"]) - bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - hass.data[hue.DOMAIN].setdefault(sensor_key, {}) - - sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) - manager = hass.data[hue.DOMAIN].get(sm_key) - if manager is None: - manager = SensorManager(hass, bridge, config_entry) - hass.data[hue.DOMAIN][sm_key] = manager - - manager.register_component(binary, async_add_entities) - await manager.start() - - class SensorManager: """Class that handles registering and updating Hue sensor entities. @@ -52,84 +33,60 @@ class SensorManager: """ SCAN_INTERVAL = timedelta(seconds=5) - sensor_config_map = {} - def __init__(self, hass, bridge, config_entry): + def __init__(self, bridge): """Initialize the sensor manager.""" - self.hass = hass self.bridge = bridge - self.config_entry = config_entry self._component_add_entities = {} - self._started = False + self.current = {} + self.coordinator = DataUpdateCoordinator( + bridge.hass, + _LOGGER, + "sensor", + self.async_update_data, + self.SCAN_INTERVAL, + debounce.Debouncer(bridge.hass, _LOGGER, REQUEST_REFRESH_DELAY, True), + ) - def register_component(self, binary, async_add_entities): + async def async_update_data(self): + """Update sensor data.""" + try: + with async_timeout.timeout(4): + return await self.bridge.async_request_call( + self.bridge.api.sensors.update() + ) + except Unauthorized: + await self.bridge.handle_unauthorized_error() + raise UpdateFailed + except (asyncio.TimeoutError, AiohueException): + raise UpdateFailed + + async def async_register_component(self, binary, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[binary] = async_add_entities - async def start(self): - """Start updating sensors from the bridge on a schedule.""" - # but only if it's not already started, and when we've got both - # async_add_entities methods - if self._started or len(self._component_add_entities) < 2: + if len(self._component_add_entities) < 2: return - self._started = True - _LOGGER.info( - "Starting sensor polling loop with %s second interval", - self.SCAN_INTERVAL.total_seconds(), + # We have all components available, start the updating. + self.coordinator.async_add_listener(self.async_update_items) + self.bridge.reset_jobs.append( + lambda: self.coordinator.async_remove_listener(self.async_update_items) ) + await self.coordinator.async_refresh() - async def async_update_bridge(now): - """Will update sensors from the bridge.""" - - # don't update when we are not authorized - if not self.bridge.authorized: - return - - await self.async_update_items() - - async_track_point_in_utc_time( - self.hass, async_update_bridge, utcnow() + self.SCAN_INTERVAL - ) - - await async_update_bridge(None) - - async def async_update_items(self): + @callback + def async_update_items(self): """Update sensors from the bridge.""" api = self.bridge.api.sensors - try: - start = monotonic() - with async_timeout.timeout(4): - await self.bridge.async_request_call(api.update()) - except Unauthorized: - await self.bridge.handle_unauthorized_error() + if len(self._component_add_entities) < 2: return - except (asyncio.TimeoutError, AiohueException) as err: - _LOGGER.debug("Failed to fetch sensor: %s", err) - - if not self.bridge.available: - return - - _LOGGER.error("Unable to reach bridge %s (%s)", self.bridge.host, err) - self.bridge.available = False - - return - - finally: - _LOGGER.debug( - "Finished sensor request in %.3f seconds", monotonic() - start - ) - - if not self.bridge.available: - _LOGGER.info("Reconnected to bridge %s", self.bridge.host) - self.bridge.available = True new_sensors = [] new_binary_sensors = [] primary_sensor_devices = {} - sensor_key = CURRENT_SENSORS_FORMAT.format(self.config_entry.data["host"]) - current = self.hass.data[hue.DOMAIN][sensor_key] + current = self.current # Physical Hue motion sensors present as three sensors in the API: a # presence sensor, a temperature sensor, and a light level sensor. Of @@ -155,11 +112,10 @@ class SensorManager: for item_id in api: existing = current.get(api[item_id].uniqueid) if existing is not None: - self.hass.async_create_task(existing.async_maybe_update_ha_state()) continue primary_sensor = None - sensor_config = self.sensor_config_map.get(api[item_id].type) + sensor_config = SENSOR_CONFIG_MAP.get(api[item_id].type) if sensor_config is None: continue @@ -177,22 +133,19 @@ class SensorManager: else: new_sensors.append(current[api[item_id].uniqueid]) - await remove_devices( - self.hass, - self.config_entry, - [value.uniqueid for value in api.values()], - current, + self.bridge.hass.async_create_task( + remove_devices( + self.bridge, [value.uniqueid for value in api.values()], current, + ) ) - async_add_sensor_entities = self._component_add_entities.get(False) - async_add_binary_entities = self._component_add_entities.get(True) - if new_sensors and async_add_sensor_entities: - async_add_sensor_entities(new_sensors) - if new_binary_sensors and async_add_binary_entities: - async_add_binary_entities(new_binary_sensors) + if new_sensors: + self._component_add_entities[False](new_sensors) + if new_binary_sensors: + self._component_add_entities[True](new_binary_sensors) -class GenericHueSensor: +class GenericHueSensor(entity.Entity): """Representation of a Hue sensor.""" should_poll = False @@ -230,10 +183,8 @@ class GenericHueSensor: @property def available(self): """Return if sensor is available.""" - return ( - self.bridge.available - and self.bridge.authorized - and (self.bridge.allow_unreachable or self.sensor.config["reachable"]) + return not self.bridge.sensor_manager.coordinator.failed_last_update and ( + self.bridge.allow_unreachable or self.sensor.config["reachable"] ) @property @@ -241,15 +192,24 @@ class GenericHueSensor: """Return detail of available software updates for this device.""" return self.primary_sensor.raw.get("swupdate", {}).get("state") - async def async_maybe_update_ha_state(self): - """Try to update Home Assistant with current state of entity. + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.bridge.sensor_manager.coordinator.async_add_listener( + self.async_write_ha_state + ) - But if it's not been added to hass yet, then don't throw an error. + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self.bridge.sensor_manager.coordinator.async_remove_listener( + self.async_write_ha_state + ) + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. """ - try: - await self._async_update_ha_state() - except (RuntimeError, NoEntitySpecifiedError): - _LOGGER.debug("Hue sensor update requested before it has been added.") + await self.bridge.sensor_manager.coordinator.async_request_refresh() @property def device_info(self): @@ -258,12 +218,12 @@ class GenericHueSensor: Links individual entities together in the hass device registry. """ return { - "identifiers": {(hue.DOMAIN, self.device_id)}, + "identifiers": {(HUE_DOMAIN, self.device_id)}, "name": self.primary_sensor.name, "manufacturer": self.primary_sensor.manufacturername, "model": (self.primary_sensor.productname or self.primary_sensor.modelid), "sw_version": self.primary_sensor.swversion, - "via_device": (hue.DOMAIN, self.bridge.api.config.bridgeid), + "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), } diff --git a/homeassistant/components/iaqualink/.translations/de.json b/homeassistant/components/iaqualink/.translations/de.json index d929022c905..26ff4b9dcf5 100644 --- a/homeassistant/components/iaqualink/.translations/de.json +++ b/homeassistant/components/iaqualink/.translations/de.json @@ -12,7 +12,7 @@ "password": "Passwort", "username": "Benutzername/E-Mail-Adresse" }, - "description": "Bitte geben Sie den Benutzernamen und das Passwort f\u00fcr Ihr iAqualink-Konto ein.", + "description": "Bitte gib den Benutzernamen und das Passwort f\u00fcr dein iAqualink-Konto ein.", "title": "Mit iAqualink verbinden" } }, diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 85392e6371b..ea3b1eef8d0 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], "codeowners": ["@flz"], - "requirements": ["iaqualink==0.3.0"] + "requirements": ["iaqualink==0.3.1"] } diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json index 30e6c50b81b..33fd399d33e 100644 --- a/homeassistant/components/icloud/.translations/ca.json +++ b/homeassistant/components/icloud/.translations/ca.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat" }, "error": { "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", - "username_exists": "El compte ja ha estat configurat", "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" }, "step": { diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json index 1a06bd8e0f2..e60b5120a83 100644 --- a/homeassistant/components/icloud/.translations/da.json +++ b/homeassistant/components/icloud/.translations/da.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Kontoen er allerede konfigureret" + "already_configured": "Kontoen er allerede konfigureret" }, "error": { "login": "Loginfejl: Kontroller din email og adgangskode", "send_verification_code": "Bekr\u00e6ftelseskoden kunne ikke sendes", - "username_exists": "Kontoen er allerede konfigureret", "validate_verification_code": "Bekr\u00e6ftelseskoden kunne ikke bekr\u00e6ftes, V\u00e6lg en betroet enhed, og start bekr\u00e6ftelsen igen" }, "step": { diff --git a/homeassistant/components/icloud/.translations/de.json b/homeassistant/components/icloud/.translations/de.json index ac9a401e70d..c31f648a4ad 100644 --- a/homeassistant/components/icloud/.translations/de.json +++ b/homeassistant/components/icloud/.translations/de.json @@ -1,20 +1,19 @@ { "config": { "abort": { - "username_exists": "Konto bereits konfiguriert" + "already_configured": "Konto bereits konfiguriert" }, "error": { - "login": "Login-Fehler: Bitte \u00fcberpr\u00fcfen Sie Ihre E-Mail & Passwort", + "login": "Login-Fehler: Bitte \u00fcberpr\u00fcfe deine E-Mail & Passwort", "send_verification_code": "Fehler beim Senden des Best\u00e4tigungscodes", - "username_exists": "Konto bereits konfiguriert", - "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hlen Sie ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starten Sie die Verifizierung erneut" + "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut" }, "step": { "trusted_device": { "data": { "trusted_device": "Vertrauensw\u00fcrdiges Ger\u00e4t" }, - "description": "W\u00e4hlen Sie Ihr vertrauensw\u00fcrdiges Ger\u00e4t aus", + "description": "W\u00e4hle dein vertrauensw\u00fcrdiges Ger\u00e4t aus", "title": "iCloud vertrauensw\u00fcrdiges Ger\u00e4t" }, "user": { @@ -22,14 +21,14 @@ "password": "Passwort", "username": "E-Mail" }, - "description": "Geben Sie Ihre Zugangsdaten ein", + "description": "Gib deine Zugangsdaten ein", "title": "iCloud-Anmeldeinformationen" }, "verification_code": { "data": { "verification_code": "Verifizierungscode" }, - "description": "Bitte geben Sie den Best\u00e4tigungscode ein, den Sie gerade von iCloud erhalten haben", + "description": "Bitte gib den Best\u00e4tigungscode ein, den du gerade von iCloud erhalten hast", "title": "iCloud-Best\u00e4tigungscode" } }, diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json index 58101759356..3b7da70bcaf 100644 --- a/homeassistant/components/icloud/.translations/en.json +++ b/homeassistant/components/icloud/.translations/en.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Account already configured" + "already_configured": "Account already configured" }, "error": { "login": "Login error: please check your email & password", "send_verification_code": "Failed to send verification code", - "username_exists": "Account already configured", "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" }, "step": { diff --git a/homeassistant/components/icloud/.translations/es.json b/homeassistant/components/icloud/.translations/es.json index 13355fa2b8e..7a0d4b66047 100644 --- a/homeassistant/components/icloud/.translations/es.json +++ b/homeassistant/components/icloud/.translations/es.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Cuenta ya configurada" + "already_configured": "Cuenta ya configurada" }, "error": { "login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", - "username_exists": "Cuenta ya configurada", "validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo" }, "step": { diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json index 81996d908a6..91cff9912b6 100644 --- a/homeassistant/components/icloud/.translations/fr.json +++ b/homeassistant/components/icloud/.translations/fr.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" }, "error": { "login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe", "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", - "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" }, "step": { diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json index 0a986f1fe77..9d93a07565f 100644 --- a/homeassistant/components/icloud/.translations/it.json +++ b/homeassistant/components/icloud/.translations/it.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Account gi\u00e0 configurato" + "already_configured": "Account gi\u00e0 configurato" }, "error": { "login": "Errore di accesso: si prega di controllare la tua e-mail e la password", "send_verification_code": "Impossibile inviare il codice di verifica", - "username_exists": "Account gi\u00e0 configurato", "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica" }, "step": { diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json index a689a895278..10df5c4519c 100644 --- a/homeassistant/components/icloud/.translations/ko.json +++ b/homeassistant/components/icloud/.translations/ko.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", "send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uace0 \uc778\uc99d\uc744 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, "step": { diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json index eaeb300f7a8..f90ec545c39 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Kont ass scho konfigur\u00e9iert" + "already_configured": "Kont ass scho konfigur\u00e9iert" }, "error": { "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", "send_verification_code": "Feeler beim sch\u00e9cken vum Verifikatiouns Code", - "username_exists": "Kont ass scho konfigur\u00e9iert", "validate_verification_code": "Feeler beim iwwerpr\u00e9iwe vum Verifikatiouns Code, wielt ee vertrauten Apparat aus a start d'Iwwerpr\u00e9iwung nei" }, "step": { diff --git a/homeassistant/components/icloud/.translations/nl.json b/homeassistant/components/icloud/.translations/nl.json index d35496b171b..fe0e7d07572 100644 --- a/homeassistant/components/icloud/.translations/nl.json +++ b/homeassistant/components/icloud/.translations/nl.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Account reeds geconfigureerd" + "already_configured": "Account reeds geconfigureerd" }, "error": { "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", "send_verification_code": "Kan verificatiecode niet verzenden", - "username_exists": "Account reeds geconfigureerd", "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" }, "step": { diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json index a582b916310..589c220ec9c 100644 --- a/homeassistant/components/icloud/.translations/no.json +++ b/homeassistant/components/icloud/.translations/no.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert" }, "error": { "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", "send_verification_code": "Kunne ikke sende bekreftelseskode", - "username_exists": "Kontoen er allerede konfigurert", "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt" }, "step": { diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json index f154f77f186..169fe2eac2d 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Konto jest ju\u017c skonfigurowane" + "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", "send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego", - "username_exists": "Konto jest ju\u017c skonfigurowane", "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119" }, "step": { diff --git a/homeassistant/components/icloud/.translations/pt-BR.json b/homeassistant/components/icloud/.translations/pt-BR.json index c8bfe0e0a6d..4e45568ae68 100644 --- a/homeassistant/components/icloud/.translations/pt-BR.json +++ b/homeassistant/components/icloud/.translations/pt-BR.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada" }, "error": { "login": "Erro de login: verifique seu e-mail e senha", "send_verification_code": "Falha ao enviar c\u00f3digo de verifica\u00e7\u00e3o", - "username_exists": "Conta j\u00e1 configurada", "validate_verification_code": "Falha ao verificar seu c\u00f3digo de verifica\u00e7\u00e3o, escolha um dispositivo confi\u00e1vel e inicie a verifica\u00e7\u00e3o novamente" }, "step": { diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json index 000edd71e00..b3a9578ad1e 100644 --- a/homeassistant/components/icloud/.translations/ru.json +++ b/homeassistant/components/icloud/.translations/ru.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", - "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430." }, "step": { diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json index 91cb4312cb3..14d6168409c 100644 --- a/homeassistant/components/icloud/.translations/sl.json +++ b/homeassistant/components/icloud/.translations/sl.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "Ra\u010dun \u017ee nastavljen" + "already_configured": "Ra\u010dun \u017ee nastavljen" }, "error": { "login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo", "send_verification_code": "Kode za preverjanje ni bilo mogo\u010de poslati", - "username_exists": "Ra\u010dun \u017ee nastavljen", "validate_verification_code": "Kode za preverjanje ni bilo mogo\u010de preveriti, izberi napravo za zaupanje in znova za\u017eeni preverjanje" }, "step": { diff --git a/homeassistant/components/icloud/.translations/zh-Hant.json b/homeassistant/components/icloud/.translations/zh-Hant.json index 80d8ba1485b..a3f4e68e167 100644 --- a/homeassistant/components/icloud/.translations/zh-Hant.json +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -1,12 +1,11 @@ { "config": { "abort": { - "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027", "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", - "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" }, "step": { diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index fc4a5465bff..62eb2fb91ac 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,54 +1,24 @@ """The iCloud component.""" -from datetime import timedelta import logging -import operator -from typing import Dict -from pyicloud import PyiCloudService -from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException -from pyicloud.services.findmyiphone import AppleDevice import voluptuous as vol -from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.util import slugify -from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.dt import utcnow -from homeassistant.util.location import distance +from .account import IcloudAccount from .const import ( - CONF_ACCOUNT_NAME, CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, DEFAULT_GPS_ACCURACY_THRESHOLD, DEFAULT_MAX_INTERVAL, - DEVICE_BATTERY_LEVEL, - DEVICE_BATTERY_STATUS, - DEVICE_CLASS, - DEVICE_DISPLAY_NAME, - DEVICE_ID, - DEVICE_LOCATION, - DEVICE_LOCATION_LATITUDE, - DEVICE_LOCATION_LONGITUDE, - DEVICE_LOST_MODE_CAPABLE, - DEVICE_LOW_POWER_MODE, - DEVICE_NAME, - DEVICE_PERSON_ID, - DEVICE_RAW_DEVICE_MODEL, - DEVICE_STATUS, - DEVICE_STATUS_CODES, - DEVICE_STATUS_SET, DOMAIN, ICLOUD_COMPONENTS, STORAGE_KEY, STORAGE_VERSION, - TRACKER_UPDATE, ) ATTRIBUTION = "Data provided by Apple iCloud" @@ -100,7 +70,6 @@ ACCOUNT_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_ACCOUNT_NAME): cv.string, vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, vol.Optional( CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD @@ -140,22 +109,22 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - account_name = entry.data.get(CONF_ACCOUNT_NAME) max_interval = entry.data[CONF_MAX_INTERVAL] gps_accuracy_threshold = entry.data[CONF_GPS_ACCURACY_THRESHOLD] + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry(entry, unique_id=username) + icloud_dir = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) account = IcloudAccount( - hass, - username, - password, - icloud_dir, - account_name, - max_interval, - gps_accuracy_threshold, + hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold, ) await hass.async_add_executor_job(account.setup) + if not account.devices: + return False + hass.data[DOMAIN][username] = account for component in ICLOUD_COMPONENTS: @@ -243,360 +212,3 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) return True - - -class IcloudAccount: - """Representation of an iCloud account.""" - - def __init__( - self, - hass: HomeAssistantType, - username: str, - password: str, - icloud_dir: Store, - account_name: str, - max_interval: int, - gps_accuracy_threshold: int, - ): - """Initialize an iCloud account.""" - self.hass = hass - self._username = username - self._password = password - self._name = account_name or slugify(username.partition("@")[0]) - self._fetch_interval = max_interval - self._max_interval = max_interval - self._gps_accuracy_threshold = gps_accuracy_threshold - - self._icloud_dir = icloud_dir - - self.api = None - self._owner_fullname = None - self._family_members_fullname = {} - self._devices = {} - - self.unsub_device_tracker = None - - def setup(self): - """Set up an iCloud account.""" - try: - self.api = PyiCloudService( - self._username, self._password, self._icloud_dir.path - ) - except PyiCloudFailedLoginException as error: - self.api = None - _LOGGER.error("Error logging into iCloud Service: %s", error) - return - - user_info = None - try: - # Gets device owners infos - user_info = self.api.devices.response["userInfo"] - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" - - self._family_members_fullname = {} - if user_info.get("membersInfo") is not None: - for prs_id, member in user_info["membersInfo"].items(): - self._family_members_fullname[ - prs_id - ] = f"{member['firstName']} {member['lastName']}" - - self._devices = {} - self.update_devices() - - def update_devices(self) -> None: - """Update iCloud devices.""" - if self.api is None: - return - - api_devices = {} - try: - api_devices = self.api.devices - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - # Gets devices infos - for device in api_devices: - status = device.status(DEVICE_STATUS_SET) - device_id = status[DEVICE_ID] - device_name = status[DEVICE_NAME] - - if self._devices.get(device_id, None) is not None: - # Seen device -> updating - _LOGGER.debug("Updating iCloud device: %s", device_name) - self._devices[device_id].update(status) - else: - # New device, should be unique - _LOGGER.debug( - "Adding iCloud device: %s [model: %s]", - device_name, - status[DEVICE_RAW_DEVICE_MODEL], - ) - self._devices[device_id] = IcloudDevice(self, device, status) - self._devices[device_id].update(status) - - dispatcher_send(self.hass, TRACKER_UPDATE) - self._fetch_interval = self._determine_interval() - track_point_in_utc_time( - self.hass, - self.keep_alive, - utcnow() + timedelta(minutes=self._fetch_interval), - ) - - def _determine_interval(self) -> int: - """Calculate new interval between two API fetch (in minutes).""" - intervals = {} - for device in self._devices.values(): - if device.location is None: - continue - - current_zone = run_callback_threadsafe( - self.hass.loop, - async_active_zone, - self.hass, - device.location[DEVICE_LOCATION_LATITUDE], - device.location[DEVICE_LOCATION_LONGITUDE], - ).result() - - if current_zone is not None: - intervals[device.name] = self._max_interval - continue - - zones = ( - self.hass.states.get(entity_id) - for entity_id in sorted(self.hass.states.entity_ids("zone")) - ) - - distances = [] - for zone_state in zones: - zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] - zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] - zone_distance = distance( - device.location[DEVICE_LOCATION_LATITUDE], - device.location[DEVICE_LOCATION_LONGITUDE], - zone_state_lat, - zone_state_long, - ) - distances.append(round(zone_distance / 1000, 1)) - - if not distances: - continue - mindistance = min(distances) - - # Calculate out how long it would take for the device to drive - # to the nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) - - # Never poll more than once per minute - interval = max(interval, 1) - - if interval > 180: - # Three hour drive? - # This is far enough that they might be flying - interval = self._max_interval - - if ( - device.battery_level is not None - and device.battery_level <= 33 - and mindistance > 3 - ): - # Low battery - let's check half as often - interval = interval * 2 - - intervals[device.name] = interval - - return max( - int(min(intervals.items(), key=operator.itemgetter(1))[1]), - self._max_interval, - ) - - def keep_alive(self, now=None) -> None: - """Keep the API alive.""" - if self.api is None: - self.setup() - - if self.api is None: - return - - self.api.authenticate() - self.update_devices() - - def get_devices_with_name(self, name: str) -> [any]: - """Get devices by name.""" - result = [] - name_slug = slugify(name.replace(" ", "", 99)) - for device in self.devices.values(): - if slugify(device.name.replace(" ", "", 99)) == name_slug: - result.append(device) - if not result: - raise Exception(f"No device with name {name}") - return result - - @property - def name(self) -> str: - """Return the account name.""" - return self._name - - @property - def username(self) -> str: - """Return the account username.""" - return self._username - - @property - def owner_fullname(self) -> str: - """Return the account owner fullname.""" - return self._owner_fullname - - @property - def family_members_fullname(self) -> Dict[str, str]: - """Return the account family members fullname.""" - return self._family_members_fullname - - @property - def fetch_interval(self) -> int: - """Return the account fetch interval.""" - return self._fetch_interval - - @property - def devices(self) -> Dict[str, any]: - """Return the account devices.""" - return self._devices - - -class IcloudDevice: - """Representation of a iCloud device.""" - - def __init__(self, account: IcloudAccount, device: AppleDevice, status): - """Initialize the iCloud device.""" - self._account = account - account_name = account.name - - self._device = device - self._status = status - - self._name = self._status[DEVICE_NAME] - self._device_id = self._status[DEVICE_ID] - self._device_class = self._status[DEVICE_CLASS] - self._device_model = self._status[DEVICE_DISPLAY_NAME] - - if self._status[DEVICE_PERSON_ID]: - owner_fullname = account.family_members_fullname[ - self._status[DEVICE_PERSON_ID] - ] - else: - owner_fullname = account.owner_fullname - - self._battery_level = None - self._battery_status = None - self._location = None - - self._attrs = { - ATTR_ATTRIBUTION: ATTRIBUTION, - CONF_ACCOUNT_NAME: account_name, - ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, - ATTR_DEVICE_NAME: self._device_model, - ATTR_DEVICE_STATUS: None, - ATTR_OWNER_NAME: owner_fullname, - } - - def update(self, status) -> None: - """Update the iCloud device.""" - self._status = status - - self._status[ATTR_ACCOUNT_FETCH_INTERVAL] = self._account.fetch_interval - - device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") - self._attrs[ATTR_DEVICE_STATUS] = device_status - - if self._status[DEVICE_BATTERY_STATUS] != "Unknown": - self._battery_level = int(self._status.get(DEVICE_BATTERY_LEVEL, 0) * 100) - self._battery_status = self._status[DEVICE_BATTERY_STATUS] - low_power_mode = self._status[DEVICE_LOW_POWER_MODE] - - self._attrs[ATTR_BATTERY] = self._battery_level - self._attrs[ATTR_BATTERY_STATUS] = self._battery_status - self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode - - if ( - self._status[DEVICE_LOCATION] - and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] - ): - location = self._status[DEVICE_LOCATION] - self._location = location - - def play_sound(self) -> None: - """Play sound on the device.""" - if self._account.api is None: - return - - self._account.api.authenticate() - _LOGGER.debug("Playing sound for %s", self.name) - self.device.play_sound() - - def display_message(self, message: str, sound: bool = False) -> None: - """Display a message on the device.""" - if self._account.api is None: - return - - self._account.api.authenticate() - _LOGGER.debug("Displaying message for %s", self.name) - self.device.display_message("Subject not working", message, sound) - - def lost_device(self, number: str, message: str) -> None: - """Make the device in lost state.""" - if self._account.api is None: - return - - self._account.api.authenticate() - if self._status[DEVICE_LOST_MODE_CAPABLE]: - _LOGGER.debug("Make device lost for %s", self.name) - self.device.lost_device(number, message, None) - else: - _LOGGER.error("Cannot make device lost for %s", self.name) - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._device_id - - @property - def name(self) -> str: - """Return the Apple device name.""" - return self._name - - @property - def device(self) -> AppleDevice: - """Return the Apple device.""" - return self._device - - @property - def device_class(self) -> str: - """Return the Apple device class.""" - return self._device_class - - @property - def device_model(self) -> str: - """Return the Apple device model.""" - return self._device_model - - @property - def battery_level(self) -> int: - """Return the Apple device battery level.""" - return self._battery_level - - @property - def battery_status(self) -> str: - """Return the Apple device battery status.""" - return self._battery_status - - @property - def location(self) -> Dict[str, any]: - """Return the Apple device location.""" - return self._location - - @property - def state_attributes(self) -> Dict[str, any]: - """Return the attributes.""" - return self._attrs diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py new file mode 100644 index 00000000000..af7963d8dc1 --- /dev/null +++ b/homeassistant/components/icloud/account.py @@ -0,0 +1,422 @@ +"""iCloud account.""" +from datetime import timedelta +import logging +import operator +from typing import Dict + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException +from pyicloud.services.findmyiphone import AppleDevice + +from homeassistant.components.zone import async_active_zone +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.dt import utcnow +from homeassistant.util.location import distance + +from .const import ( + DEVICE_BATTERY_LEVEL, + DEVICE_BATTERY_STATUS, + DEVICE_CLASS, + DEVICE_DISPLAY_NAME, + DEVICE_ID, + DEVICE_LOCATION, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, + DEVICE_LOST_MODE_CAPABLE, + DEVICE_LOW_POWER_MODE, + DEVICE_NAME, + DEVICE_PERSON_ID, + DEVICE_RAW_DEVICE_MODEL, + DEVICE_STATUS, + DEVICE_STATUS_CODES, + DEVICE_STATUS_SET, + SERVICE_UPDATE, +) + +ATTRIBUTION = "Data provided by Apple iCloud" + +# entity attributes +ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" +ATTR_BATTERY = "battery" +ATTR_BATTERY_STATUS = "battery_status" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_STATUS = "device_status" +ATTR_LOW_POWER_MODE = "low_power_mode" +ATTR_OWNER_NAME = "owner_fullname" + +# services +SERVICE_ICLOUD_PLAY_SOUND = "play_sound" +SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" +SERVICE_ICLOUD_LOST_DEVICE = "lost_device" +SERVICE_ICLOUD_UPDATE = "update" +ATTR_ACCOUNT = "account" +ATTR_LOST_DEVICE_MESSAGE = "message" +ATTR_LOST_DEVICE_NUMBER = "number" +ATTR_LOST_DEVICE_SOUND = "sound" + +_LOGGER = logging.getLogger(__name__) + + +class IcloudAccount: + """Representation of an iCloud account.""" + + def __init__( + self, + hass: HomeAssistantType, + username: str, + password: str, + icloud_dir: Store, + max_interval: int, + gps_accuracy_threshold: int, + ): + """Initialize an iCloud account.""" + self.hass = hass + self._username = username + self._password = password + self._fetch_interval = max_interval + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold + + self._icloud_dir = icloud_dir + + self.api: PyiCloudService = None + self._owner_fullname = None + self._family_members_fullname = {} + self._devices = {} + + self.unsub_device_tracker = None + + def setup(self) -> None: + """Set up an iCloud account.""" + try: + self.api = PyiCloudService( + self._username, self._password, self._icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error("Error logging into iCloud Service: %s", error) + return + + user_info = None + try: + # Gets device owners infos + user_info = self.api.devices.response["userInfo"] + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud device found") + return + + self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" + + self._family_members_fullname = {} + if user_info.get("membersInfo") is not None: + for prs_id, member in user_info["membersInfo"].items(): + self._family_members_fullname[ + prs_id + ] = f"{member['firstName']} {member['lastName']}" + + self._devices = {} + self.update_devices() + + def update_devices(self) -> None: + """Update iCloud devices.""" + if self.api is None: + return + + api_devices = {} + try: + api_devices = self.api.devices + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud device found") + return + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Unknown iCloud error: %s", err) + self._fetch_interval = 5 + dispatcher_send(self.hass, SERVICE_UPDATE) + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + return + + # Gets devices infos + for device in api_devices: + status = device.status(DEVICE_STATUS_SET) + device_id = status[DEVICE_ID] + device_name = status[DEVICE_NAME] + + if self._devices.get(device_id, None) is not None: + # Seen device -> updating + _LOGGER.debug("Updating iCloud device: %s", device_name) + self._devices[device_id].update(status) + else: + # New device, should be unique + _LOGGER.debug( + "Adding iCloud device: %s [model: %s]", + device_name, + status[DEVICE_RAW_DEVICE_MODEL], + ) + self._devices[device_id] = IcloudDevice(self, device, status) + self._devices[device_id].update(status) + + self._fetch_interval = self._determine_interval() + dispatcher_send(self.hass, SERVICE_UPDATE) + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + + def _determine_interval(self) -> int: + """Calculate new interval between two API fetch (in minutes).""" + intervals = {} + for device in self._devices.values(): + if device.location is None: + continue + + current_zone = run_callback_threadsafe( + self.hass.loop, + async_active_zone, + self.hass, + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + ).result() + + if current_zone is not None: + intervals[device.name] = self._max_interval + continue + + zones = ( + self.hass.states.get(entity_id) + for entity_id in sorted(self.hass.states.entity_ids("zone")) + ) + + distances = [] + for zone_state in zones: + zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] + zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] + zone_distance = distance( + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + zone_state_lat, + zone_state_long, + ) + distances.append(round(zone_distance / 1000, 1)) + + if not distances: + continue + mindistance = min(distances) + + # Calculate out how long it would take for the device to drive + # to the nearest zone at 120 km/h: + interval = round(mindistance / 2, 0) + + # Never poll more than once per minute + interval = max(interval, 1) + + if interval > 180: + # Three hour drive? + # This is far enough that they might be flying + interval = self._max_interval + + if ( + device.battery_level is not None + and device.battery_level <= 33 + and mindistance > 3 + ): + # Low battery - let's check half as often + interval = interval * 2 + + intervals[device.name] = interval + + return max( + int(min(intervals.items(), key=operator.itemgetter(1))[1]), + self._max_interval, + ) + + def keep_alive(self, now=None) -> None: + """Keep the API alive.""" + if self.api is None: + self.setup() + + if self.api is None: + return + + self.api.authenticate() + self.update_devices() + + def get_devices_with_name(self, name: str) -> [any]: + """Get devices by name.""" + result = [] + name_slug = slugify(name.replace(" ", "", 99)) + for device in self.devices.values(): + if slugify(device.name.replace(" ", "", 99)) == name_slug: + result.append(device) + if not result: + raise Exception(f"No device with name {name}") + return result + + @property + def username(self) -> str: + """Return the account username.""" + return self._username + + @property + def owner_fullname(self) -> str: + """Return the account owner fullname.""" + return self._owner_fullname + + @property + def family_members_fullname(self) -> Dict[str, str]: + """Return the account family members fullname.""" + return self._family_members_fullname + + @property + def fetch_interval(self) -> int: + """Return the account fetch interval.""" + return self._fetch_interval + + @property + def devices(self) -> Dict[str, any]: + """Return the account devices.""" + return self._devices + + +class IcloudDevice: + """Representation of a iCloud device.""" + + def __init__(self, account: IcloudAccount, device: AppleDevice, status): + """Initialize the iCloud device.""" + self._account = account + + self._device = device + self._status = status + + self._name = self._status[DEVICE_NAME] + self._device_id = self._status[DEVICE_ID] + self._device_class = self._status[DEVICE_CLASS] + self._device_model = self._status[DEVICE_DISPLAY_NAME] + + if self._status[DEVICE_PERSON_ID]: + owner_fullname = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + else: + owner_fullname = account.owner_fullname + + self._battery_level = None + self._battery_status = None + self._location = None + + self._attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, + ATTR_DEVICE_NAME: self._device_model, + ATTR_DEVICE_STATUS: None, + ATTR_OWNER_NAME: owner_fullname, + } + + def update(self, status) -> None: + """Update the iCloud device.""" + self._status = status + + self._status[ATTR_ACCOUNT_FETCH_INTERVAL] = self._account.fetch_interval + + device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") + self._attrs[ATTR_DEVICE_STATUS] = device_status + + self._battery_status = self._status[DEVICE_BATTERY_STATUS] + self._attrs[ATTR_BATTERY_STATUS] = self._battery_status + device_battery_level = self._status.get(DEVICE_BATTERY_LEVEL, 0) + if self._battery_status != "Unknown" and device_battery_level is not None: + self._battery_level = int(device_battery_level * 100) + self._attrs[ATTR_BATTERY] = self._battery_level + self._attrs[ATTR_LOW_POWER_MODE] = self._status[DEVICE_LOW_POWER_MODE] + + if ( + self._status[DEVICE_LOCATION] + and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] + ): + location = self._status[DEVICE_LOCATION] + self._location = location + + def play_sound(self) -> None: + """Play sound on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Playing sound for %s", self.name) + self.device.play_sound() + + def display_message(self, message: str, sound: bool = False) -> None: + """Display a message on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Displaying message for %s", self.name) + self.device.display_message("Subject not working", message, sound) + + def lost_device(self, number: str, message: str) -> None: + """Make the device in lost state.""" + if self._account.api is None: + return + + self._account.api.authenticate() + if self._status[DEVICE_LOST_MODE_CAPABLE]: + _LOGGER.debug("Make device lost for %s", self.name) + self.device.lost_device(number, message, None) + else: + _LOGGER.error("Cannot make device lost for %s", self.name) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_id + + @property + def name(self) -> str: + """Return the Apple device name.""" + return self._name + + @property + def device(self) -> AppleDevice: + """Return the Apple device.""" + return self._device + + @property + def device_class(self) -> str: + """Return the Apple device class.""" + return self._device_class + + @property + def device_model(self) -> str: + """Return the Apple device model.""" + return self._device_model + + @property + def battery_level(self) -> int: + """Return the Apple device battery level.""" + return self._battery_level + + @property + def battery_status(self) -> str: + """Return the Apple device battery status.""" + return self._battery_status + + @property + def location(self) -> Dict[str, any]: + """Return the Apple device location.""" + return self._location + + @property + def state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index cf05c07e26f..b3cb9c28181 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -8,10 +8,8 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.util import slugify from .const import ( - CONF_ACCOUNT_NAME, CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, DEFAULT_GPS_ACCURACY_THRESHOLD, @@ -45,17 +43,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._trusted_device = None self._verification_code = None - def _configuration_exists(self, username: str, account_name: str) -> bool: - """Return True if username or account_name exists in configuration.""" - for entry in self._async_current_entries(): - if ( - entry.data[CONF_USERNAME] == username - or entry.data.get(CONF_ACCOUNT_NAME) == account_name - or slugify(entry.data[CONF_USERNAME].partition("@")[0]) == account_name - ): - return True - return False - async def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" @@ -91,15 +78,15 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._username = user_input[CONF_USERNAME] self._password = user_input[CONF_PASSWORD] - self._account_name = user_input.get(CONF_ACCOUNT_NAME) self._max_interval = user_input.get(CONF_MAX_INTERVAL, DEFAULT_MAX_INTERVAL) self._gps_accuracy_threshold = user_input.get( CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD ) - if self._configuration_exists(self._username, self._account_name): - errors[CONF_USERNAME] = "username_exists" - return await self._show_setup_form(user_input, errors) + # Check if already configured + if self.unique_id is None: + await self.async_set_unique_id(self._username) + self._abort_if_unique_id_configured() try: self.api = await self.hass.async_add_executor_job( @@ -111,7 +98,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_USERNAME] = "login" return await self._show_setup_form(user_input, errors) - if self.api.requires_2fa: + if self.api.requires_2sa: return await self.async_step_trusted_device() return self.async_create_entry( @@ -119,7 +106,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={ CONF_USERNAME: self._username, CONF_PASSWORD: self._password, - CONF_ACCOUNT_NAME: self._account_name, CONF_MAX_INTERVAL: self._max_interval, CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, }, @@ -127,11 +113,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Import a config entry.""" - if self._configuration_exists( - user_input[CONF_USERNAME], user_input.get(CONF_ACCOUNT_NAME) - ): - return self.async_abort(reason="username_exists") - return await self.async_step_user(user_input) async def async_step_trusted_device(self, user_input=None, errors=None): @@ -214,7 +195,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { CONF_USERNAME: self._username, CONF_PASSWORD: self._password, - CONF_ACCOUNT_NAME: self._account_name, CONF_MAX_INTERVAL: self._max_interval, CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, } diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index ed2fc78fe6d..57a3f48936c 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,9 +1,8 @@ """iCloud component constants.""" DOMAIN = "icloud" -TRACKER_UPDATE = f"{DOMAIN}_tracker_update" +SERVICE_UPDATE = f"{DOMAIN}_update" -CONF_ACCOUNT_NAME = "account_name" CONF_MAX_INTERVAL = "max_interval" CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 511ce7f9447..00f35fbee85 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -9,13 +9,13 @@ from homeassistant.const import CONF_USERNAME from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from . import IcloudDevice +from .account import IcloudDevice from .const import ( DEVICE_LOCATION_HORIZONTAL_ACCURACY, DEVICE_LOCATION_LATITUDE, DEVICE_LOCATION_LONGITUDE, DOMAIN, - TRACKER_UPDATE, + SERVICE_UPDATE, ) _LOGGER = logging.getLogger(__name__) @@ -77,11 +77,6 @@ class IcloudTrackerEntity(TrackerEntity): """Return longitude value of the device.""" return self._device.location[DEVICE_LOCATION_LONGITUDE] - @property - def should_poll(self) -> bool: - """No polling needed.""" - return False - @property def battery_level(self) -> int: """Return the battery level of the device.""" @@ -112,10 +107,15 @@ class IcloudTrackerEntity(TrackerEntity): "model": self._device.device_model, } + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( - self.hass, TRACKER_UPDATE, self.async_write_ha_state + self.hass, SERVICE_UPDATE, self.async_write_ha_state ) async def async_will_remove_from_hass(self): diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 9652ef10469..a4a51f9e1a2 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,7 +3,7 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.1"], + "requirements": ["pyicloud==0.9.2"], "dependencies": [], "codeowners": ["@Quentame"] } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 4351d4ffa19..e24016795d3 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -4,12 +4,13 @@ from typing import Dict from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.typing import HomeAssistantType -from . import IcloudDevice -from .const import DOMAIN +from .account import IcloudDevice +from .const import DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class IcloudDeviceBatterySensor(Entity): def __init__(self, device: IcloudDevice): """Initialize the battery sensor.""" self._device = device + self._unsub_dispatcher = None @property def unique_id(self) -> str: @@ -83,3 +85,18 @@ class IcloudDeviceBatterySensor(Entity): "manufacturer": "Apple", "model": self._device.device_model, } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, SERVICE_UPDATE, self.async_write_ha_state + ) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 117e26c8830..e0a7b7a32ce 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -26,13 +26,12 @@ } }, "error": { - "username_exists": "Account already configured", "login": "Login error: please check your email & password", "send_verification_code": "Failed to send verification code", "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" }, "abort": { - "username_exists": "Account already configured" + "already_configured": "Account already configured" } } } \ No newline at end of file diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index b246943b6ad..9acf710a58e 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -191,7 +191,10 @@ SET_RUNTIME_VALUE_BOOL_SCHEMA = vol.Schema( ) SET_RUNTIME_VALUE_INT_SCHEMA = vol.Schema( - {vol.Required(ATTR_IHC_ID): cv.positive_int, vol.Required(ATTR_VALUE): int} + { + vol.Required(ATTR_IHC_ID): cv.positive_int, + vol.Required(ATTR_VALUE): vol.Coerce(int), + } ) SET_RUNTIME_VALUE_FLOAT_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index 4c5ab49c83e..ac9f2f60218 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -2,7 +2,10 @@ "domain": "ihc", "name": "IHC Controller", "documentation": "https://www.home-assistant.io/integrations/ihc", - "requirements": ["defusedxml==0.6.0", "ihcsdk==2.4.0"], + "requirements": [ + "defusedxml==0.6.0", + "ihcsdk==2.5.0" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ihc/util.py b/homeassistant/components/ihc/util.py index 40434dadc8e..1b6b1dbd3e0 100644 --- a/homeassistant/components/ihc/util.py +++ b/homeassistant/components/ihc/util.py @@ -2,6 +2,8 @@ import asyncio +from homeassistant.core import callback + async def async_pulse(hass, ihc_controller, ihc_id: int): """Send a short on/off pulse to an IHC controller resource.""" @@ -10,6 +12,7 @@ async def async_pulse(hass, ihc_controller, ihc_id: int): await async_set_bool(hass, ihc_controller, ihc_id, False) +@callback def async_set_bool(hass, ihc_controller, ihc_id: int, value: bool): """Set a bool value on an IHC controller resource.""" return hass.async_add_executor_job( @@ -17,6 +20,7 @@ def async_set_bool(hass, ihc_controller, ihc_id: int, value: bool): ) +@callback def async_set_int(hass, ihc_controller, ihc_id: int, value: int): """Set a int value on an IHC controller resource.""" return hass.async_add_executor_job( diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index a8f5f0f097e..84ba5b45fc4 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -107,12 +107,9 @@ class ImageProcessingEntity(Entity): """Process image.""" raise NotImplementedError() - def async_process_image(self, image): - """Process image. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.process_image, image) + async def async_process_image(self, image): + """Process image.""" + return await self.hass.async_add_job(self.process_image, image) async def async_update(self): """Update image and process it. diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index a12c6552399..daadfac3705 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import callback -from homeassistant.helpers import collection, entity_registry +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -105,7 +105,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await yaml_collection.async_load( - [{CONF_ID: id_, **(conf or {})} for id_, conf in config[DOMAIN].items()] + [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] ) await storage_collection.async_load() @@ -113,18 +113,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS ).async_setup(hass) - async def _collection_changed( - change_type: str, item_id: str, config: typing.Optional[typing.Dict] - ) -> None: - """Handle a collection change: clean up entity registry on removals.""" - if change_type != collection.CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(hass) - ent_reg.async_remove(ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) - - yaml_collection.async_add_listener(_collection_changed) - storage_collection.async_add_listener(_collection_changed) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) async def reload_service_handler(service_call: ServiceCallType) -> None: """Remove all input booleans and load new ones from config.""" @@ -132,7 +122,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if conf is None: return await yaml_collection.async_load( - [{CONF_ID: id_, **(conf or {})} for id_, conf in conf[DOMAIN].items()] + [ + {CONF_ID: id_, **(conf or {})} + for id_, conf in conf.get(DOMAIN, {}).items() + ] ) homeassistant.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index da684e03ddc..371e0dea185 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,20 +1,27 @@ """Support to select a date and/or a time.""" import datetime import logging +import typing import voluptuous as vol from homeassistant.const import ( ATTR_DATE, + ATTR_EDITABLE, ATTR_TIME, CONF_ICON, + CONF_ID, CONF_NAME, SERVICE_RELOAD, ) +from homeassistant.core import callback +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -27,10 +34,29 @@ CONF_HAS_TIME = "has_time" CONF_INITIAL = "initial" DEFAULT_VALUE = "1970-01-01 00:00:00" +DEFAULT_DATE = datetime.date(1970, 1, 1) +DEFAULT_TIME = datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" SERVICE_SET_DATETIME = "set_datetime" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_HAS_DATE, default=False): cv.boolean, + vol.Optional(CONF_HAS_TIME, default=False): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_INITIAL): cv.string, +} +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_HAS_DATE): cv.boolean, + vol.Optional(CONF_HAS_TIME): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_INITIAL): cv.string, +} def has_date_or_time(conf): @@ -61,20 +87,47 @@ CONFIG_SCHEMA = vol.Schema( RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up an input datetime.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = await _async_process_config(config) + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, InputDatetime.from_yaml + ) - async def reload_service_handler(service_call): - """Remove all entities and load new ones from config.""" - conf = await component.async_prepare_reload() + storage_collection = DateTimeStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputDatetime + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) if conf is None: - return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -119,68 +172,79 @@ async def async_setup(hass, config): async_set_datetime_service, ) - if entities: - await component.async_add_entities(entities) return True -async def _async_process_config(config): - """Process config and create list of entities.""" - entities = [] +class DateTimeStorageCollection(collection.StorageCollection): + """Input storage based collection.""" - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - has_time = cfg.get(CONF_HAS_TIME) - has_date = cfg.get(CONF_HAS_DATE) - icon = cfg.get(CONF_ICON) - initial = cfg.get(CONF_INITIAL) - entities.append( - InputDatetime(object_id, name, has_date, has_time, icon, initial) - ) + CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time)) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - return entities + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) + + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return has_date_or_time({**data, **update_data}) class InputDatetime(RestoreEntity): """Representation of a datetime input.""" - def __init__(self, object_id, name, has_date, has_time, icon, initial): + def __init__(self, config: typing.Dict) -> None: """Initialize a select input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self.has_date = has_date - self.has_time = has_time - self._icon = icon - self._initial = initial + self._config = config + self.editable = True self._current_datetime = None + initial = config.get(CONF_INITIAL) + if initial: + if self.has_date and self.has_time: + self._current_datetime = dt_util.parse_datetime(initial) + elif self.has_date: + date = dt_util.parse_date(initial) + self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME) + else: + time = dt_util.parse_time(initial) + self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time) + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputDatetime": + """Return entity instance initialized from yaml storage.""" + input_dt = cls(config) + input_dt.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) + input_dt.editable = False + return input_dt async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() - restore_val = None - # Priority 1: Initial State - if self._initial is not None: - restore_val = self._initial + # Priority 1: Initial value + if self.state is not None: + return # Priority 2: Old state - if restore_val is None: - old_state = await self.async_get_last_state() - if old_state is not None: - restore_val = old_state.state + old_state = await self.async_get_last_state() + if old_state is None: + self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + return - if not self.has_date: - if not restore_val: - restore_val = DEFAULT_VALUE.split()[1] - self._current_datetime = dt_util.parse_time(restore_val) - elif not self.has_time: - if not restore_val: - restore_val = DEFAULT_VALUE.split()[0] - self._current_datetime = dt_util.parse_date(restore_val) + if self.has_date and self.has_time: + self._current_datetime = dt_util.parse_datetime(old_state.state) + elif self.has_date: + date = dt_util.parse_date(old_state.state) + self._current_datetime = datetime.datetime.combine(date, DEFAULT_TIME) else: - if not restore_val: - restore_val = DEFAULT_VALUE - self._current_datetime = dt_util.parse_datetime(restore_val) + time = dt_util.parse_time(old_state.state) + self._current_datetime = datetime.datetime.combine(DEFAULT_DATE, time) @property def should_poll(self): @@ -190,22 +254,43 @@ class InputDatetime(RestoreEntity): @property def name(self): """Return the name of the select input.""" - return self._name + return self._config.get(CONF_NAME) + + @property + def has_date(self) -> bool: + """Return True if entity has date.""" + return self._config[CONF_HAS_DATE] + + @property + def has_time(self) -> bool: + """Return True if entity has time.""" + return self._config[CONF_HAS_TIME] @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) @property def state(self): """Return the state of the component.""" - return self._current_datetime + if self._current_datetime is None: + return None + + if self.has_date and self.has_time: + return self._current_datetime + if self.has_date: + return self._current_datetime.date() + return self._current_datetime.time() @property def state_attributes(self): """Return the state attributes.""" - attrs = {"has_date": self.has_date, "has_time": self.has_time} + attrs = { + ATTR_EDITABLE: self.editable, + CONF_HAS_DATE: self.has_date, + CONF_HAS_TIME: self.has_time, + } if self._current_datetime is None: return attrs @@ -236,13 +321,28 @@ class InputDatetime(RestoreEntity): return attrs + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id of the entity.""" + return self._config[CONF_ID] + + @callback def async_set_datetime(self, date_val, time_val): """Set a new date / time.""" if self.has_date and self.has_time and date_val and time_val: self._current_datetime = datetime.datetime.combine(date_val, time_val) elif self.has_date and not self.has_time and date_val: - self._current_datetime = date_val + self._current_datetime = datetime.datetime.combine( + date_val, self._current_datetime.time() + ) if self.has_time and not self.has_date and time_val: - self._current_datetime = time_val + self._current_datetime = datetime.datetime.combine( + self._current_datetime.date(), time_val + ) - self.async_schedule_update_ha_state() + self.async_write_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 472bd1b83b9..4c5c998d0a5 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -4,11 +4,11 @@ set_datetime: entity_id: {description: Entity id of the input datetime to set the new value., example: input_datetime.test_date_time} date: {description: The target date the entity should be set to. Do not use with datetime., - example: '"date": "2019-04-22"'} + example: '"2019-04-20"'} time: {description: The target time the entity should be set to. Do not use with datetime., - example: '"time": "05:30:00"'} + example: '"05:04:20"'} datetime: {description: The target date & time the entity should be set to. Do not use with date or time., - example: '"datetime": "2019-04-22 05:30:00"'} + example: '"2019-04-20 05:04:20"'} reload: description: Reload the input_datetime configuration. diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a4438020886..f78fc485e40 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,20 +1,27 @@ """Support to set a numeric value from a slider or text box.""" import logging +import typing import voluptuous as vol from homeassistant.const import ( + ATTR_EDITABLE, ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, + CONF_ID, CONF_MODE, CONF_NAME, SERVICE_RELOAD, ) +from homeassistant.core import callback +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -54,6 +61,28 @@ def _cv_input_number(cfg): return cfg +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Required(CONF_MIN): vol.Coerce(float), + vol.Required(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP, default=1): vol.All(vol.Coerce(float), vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]), +} + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN): vol.Coerce(float), + vol.Optional(CONF_MAX): vol.Coerce(float), + vol.Optional(CONF_INITIAL): vol.Coerce(float), + vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-3)), + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]), +} + CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -76,26 +105,54 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = await _async_process_config(config) + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, InputNumber.from_yaml + ) - async def reload_service_handler(service_call): - """Remove all entities and load new ones from config.""" - conf = await component.async_prepare_reload() + storage_collection = NumberStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputNumber + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) if conf is None: - return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **conf} for id_, conf in conf.get(DOMAIN, {}).items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -115,86 +172,102 @@ async def async_setup(hass, config): component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") - if entities: - await component.async_add_entities(entities) return True -async def _async_process_config(config): - """Process config and create list of entities.""" - entities = [] +class NumberStorageCollection(collection.StorageCollection): + """Input storage based collection.""" - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) - initial = cfg.get(CONF_INITIAL) - step = cfg.get(CONF_STEP) - icon = cfg.get(CONF_ICON) - unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) - mode = cfg.get(CONF_MODE) + CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number)) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - entities.append( - InputNumber( - object_id, name, initial, minimum, maximum, step, icon, unit, mode - ) - ) + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) - return entities + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return _cv_input_number({**data, **update_data}) class InputNumber(RestoreEntity): """Representation of a slider.""" - def __init__( - self, object_id, name, initial, minimum, maximum, step, icon, unit, mode - ): + def __init__(self, config: typing.Dict): """Initialize an input number.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_value = initial - self._initial = initial - self._minimum = minimum - self._maximum = maximum - self._step = step - self._icon = icon - self._unit = unit - self._mode = mode + self._config = config + self.editable = True + self._current_value = config.get(CONF_INITIAL) + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputNumber": + """Return entity instance initialized from yaml storage.""" + input_num = cls(config) + input_num.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) + input_num.editable = False + return input_num @property def should_poll(self): """If entity should be polled.""" return False + @property + def _minimum(self) -> float: + """Return minimum allowed value.""" + return self._config[CONF_MIN] + + @property + def _maximum(self) -> float: + """Return maximum allowed value.""" + return self._config[CONF_MAX] + @property def name(self): """Return the name of the input slider.""" - return self._name + return self._config.get(CONF_NAME) @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) @property def state(self): """Return the state of the component.""" return self._current_value + @property + def _step(self) -> int: + """Return entity's increment/decrement step.""" + return self._config[CONF_STEP] + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit + return self._config.get(ATTR_UNIT_OF_MEASUREMENT) + + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id of the entity.""" + return self._config[CONF_ID] @property def state_attributes(self): """Return the state attributes.""" return { - ATTR_INITIAL: self._initial, + ATTR_INITIAL: self._config.get(CONF_INITIAL), + ATTR_EDITABLE: self.editable, ATTR_MIN: self._minimum, ATTR_MAX: self._maximum, ATTR_STEP: self._step, - ATTR_MODE: self._mode, + ATTR_MODE: self._config[CONF_MODE], } async def async_added_to_hass(self): @@ -224,7 +297,7 @@ class InputNumber(RestoreEntity): ) return self._current_value = num_value - await self.async_update_ha_state() + self.async_write_ha_state() async def async_increment(self): """Increment value.""" @@ -238,7 +311,7 @@ class InputNumber(RestoreEntity): ) return self._current_value = new_value - await self.async_update_ha_state() + self.async_write_ha_state() async def async_decrement(self): """Decrement value.""" @@ -252,4 +325,12 @@ class InputNumber(RestoreEntity): ) return self._current_value = new_value - await self.async_update_ha_state() + self.async_write_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + # just in case min/max values changed + self._current_value = min(self._current_value, self._maximum) + self._current_value = max(self._current_value, self._minimum) + self.async_write_ha_state() diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index b2b4b2083e8..6044375d8a8 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -1,13 +1,24 @@ """Support to select an option from a list.""" import logging +import typing import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, +) +from homeassistant.core import callback +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -21,13 +32,24 @@ ATTR_OPTION = "option" ATTR_OPTIONS = "options" SERVICE_SELECT_OPTION = "select_option" - - SERVICE_SELECT_NEXT = "select_next" - SERVICE_SELECT_PREVIOUS = "select_previous" - SERVICE_SET_OPTIONS = "set_options" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Required(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, +} +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OPTIONS): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, +} def _cv_input_select(cfg): @@ -59,26 +81,52 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = await _async_process_config(config) + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, InputSelect.from_yaml + ) - async def reload_service_handler(service_call): - """Remove all entities and load new ones from config.""" - conf = await component.async_prepare_reload() + storage_collection = InputSelectStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputSelect + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) if conf is None: - return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -95,11 +143,15 @@ async def async_setup(hass, config): ) component.async_register_entity_service( - SERVICE_SELECT_NEXT, {}, lambda entity, call: entity.async_offset_index(1), + SERVICE_SELECT_NEXT, + {}, + callback(lambda entity, call: entity.async_offset_index(1)), ) component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, {}, lambda entity, call: entity.async_offset_index(-1), + SERVICE_SELECT_PREVIOUS, + {}, + callback(lambda entity, call: entity.async_offset_index(-1)), ) component.async_register_entity_service( @@ -112,35 +164,46 @@ async def async_setup(hass, config): "async_set_options", ) - if entities: - await component.async_add_entities(entities) return True -async def _async_process_config(config): - """Process config and create list of entities.""" - entities = [] +class InputSelectStorageCollection(collection.StorageCollection): + """Input storage based collection.""" - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - options = cfg.get(CONF_OPTIONS) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - entities.append(InputSelect(object_id, name, initial, options, icon)) + CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select)) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - return entities + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) + + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return _cv_input_select({**data, **update_data}) class InputSelect(RestoreEntity): """Representation of a select input.""" - def __init__(self, object_id, name, initial, options, icon): + def __init__(self, config: typing.Dict): """Initialize a select input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_option = initial - self._options = options - self._icon = icon + self._config = config + self.editable = True + self._current_option = config.get(CONF_INITIAL) + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputSelect": + """Return entity instance initialized from yaml storage.""" + input_select = cls(config) + input_select.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) + input_select.editable = False + return input_select async def async_added_to_hass(self): """Run when entity about to be added.""" @@ -162,12 +225,17 @@ class InputSelect(RestoreEntity): @property def name(self): """Return the name of the select input.""" - return self._name + return self._config.get(CONF_NAME) @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) + + @property + def _options(self) -> typing.List[str]: + """Return a list of selection options.""" + return self._config[CONF_OPTIONS] @property def state(self): @@ -177,9 +245,15 @@ class InputSelect(RestoreEntity): @property def state_attributes(self): """Return the state attributes.""" - return {ATTR_OPTIONS: self._options} + return {ATTR_OPTIONS: self._config[ATTR_OPTIONS], ATTR_EDITABLE: self.editable} - async def async_select_option(self, option): + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id for the entity.""" + return self._config[CONF_ID] + + @callback + def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning( @@ -189,17 +263,24 @@ class InputSelect(RestoreEntity): ) return self._current_option = option - await self.async_update_ha_state() + self.async_write_ha_state() - async def async_offset_index(self, offset): + @callback + def async_offset_index(self, offset): """Offset current index.""" current_index = self._options.index(self._current_option) new_index = (current_index + offset) % len(self._options) self._current_option = self._options[new_index] - await self.async_update_ha_state() + self.async_write_ha_state() - async def async_set_options(self, options): + @callback + def async_set_options(self, options): """Set options.""" self._current_option = options[0] - self._options = options - await self.async_update_ha_state() + self._config[CONF_OPTIONS] = options + self.async_write_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2049de7ab27..bdb3e8a4bc9 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -1,20 +1,27 @@ """Support to enter a value into a text box.""" import logging +import typing import voluptuous as vol from homeassistant.const import ( + ATTR_EDITABLE, ATTR_MODE, - ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, + CONF_ID, CONF_MODE, CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, ) +from homeassistant.core import callback +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType _LOGGER = logging.getLogger(__name__) @@ -26,16 +33,41 @@ CONF_MIN = "min" CONF_MIN_VALUE = 0 CONF_MAX = "max" CONF_MAX_VALUE = 100 +CONF_PATTERN = "pattern" +CONF_VALUE = "value" MODE_TEXT = "text" MODE_PASSWORD = "password" -ATTR_VALUE = "value" +ATTR_VALUE = CONF_VALUE ATTR_MIN = "min" ATTR_MAX = "max" -ATTR_PATTERN = "pattern" +ATTR_PATTERN = CONF_PATTERN SERVICE_SET_VALUE = "set_value" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), + vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), + vol.Optional(CONF_INITIAL, ""): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]), +} +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN): vol.Coerce(int), + vol.Optional(CONF_MAX): vol.Coerce(int), + vol.Optional(CONF_INITIAL): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_PATTERN): cv.string, + vol.Optional(CONF_MODE): vol.In([MODE_TEXT, MODE_PASSWORD]), +} def _cv_input_text(cfg): @@ -65,8 +97,8 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_PATTERN): cv.string, vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( [MODE_TEXT, MODE_PASSWORD] ), @@ -77,26 +109,52 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass, config): - """Set up an input text box.""" +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up an input text.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = await _async_process_config(config) + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, InputText.from_yaml + ) - async def reload_service_handler(service_call): - """Remove all entities and load new ones from config.""" - conf = await component.async_prepare_reload() + storage_collection = InputTextStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputText + ) + + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) if conf is None: - return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **(cfg or {})} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -110,52 +168,53 @@ async def async_setup(hass, config): SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" ) - if entities: - await component.async_add_entities(entities) return True -async def _async_process_config(config): - """Process config and create list of entities.""" - entities = [] +class InputTextStorageCollection(collection.StorageCollection): + """Input storage based collection.""" - for object_id, cfg in config[DOMAIN].items(): - if cfg is None: - cfg = {} - name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN, CONF_MIN_VALUE) - maximum = cfg.get(CONF_MAX, CONF_MAX_VALUE) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) - pattern = cfg.get(ATTR_PATTERN) - mode = cfg.get(CONF_MODE) + CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text)) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - entities.append( - InputText( - object_id, name, initial, minimum, maximum, icon, unit, pattern, mode - ) - ) + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) - return entities + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return _cv_input_text({**data, **update_data}) class InputText(RestoreEntity): """Represent a text box.""" - def __init__( - self, object_id, name, initial, minimum, maximum, icon, unit, pattern, mode - ): + def __init__(self, config: typing.Dict): """Initialize a text input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._current_value = initial - self._minimum = minimum - self._maximum = maximum - self._icon = icon - self._unit = unit - self._pattern = pattern - self._mode = mode + self._config = config + self.editable = True + self._current_value = config.get(CONF_INITIAL) + + @classmethod + def from_yaml(cls, config: typing.Dict) -> "InputText": + """Return entity instance initialized from yaml storage.""" + # set defaults for empty config + config = { + CONF_MAX: CONF_MAX_VALUE, + CONF_MIN: CONF_MIN_VALUE, + CONF_MODE: MODE_TEXT, + **config, + } + input_text = cls(config) + input_text.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) + input_text.editable = False + return input_text @property def should_poll(self): @@ -165,12 +224,22 @@ class InputText(RestoreEntity): @property def name(self): """Return the name of the text input entity.""" - return self._name + return self._config.get(CONF_NAME) @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) + + @property + def _maximum(self) -> int: + """Return max len of the text.""" + return self._config[CONF_MAX] + + @property + def _minimum(self) -> int: + """Return min len of the text.""" + return self._config[CONF_MIN] @property def state(self): @@ -180,16 +249,22 @@ class InputText(RestoreEntity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit + return self._config.get(CONF_UNIT_OF_MEASUREMENT) + + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id for the entity.""" + return self._config[CONF_ID] @property def state_attributes(self): """Return the state attributes.""" return { + ATTR_EDITABLE: self.editable, ATTR_MIN: self._minimum, ATTR_MAX: self._maximum, - ATTR_PATTERN: self._pattern, - ATTR_MODE: self._mode, + ATTR_PATTERN: self._config.get(CONF_PATTERN), + ATTR_MODE: self._config[CONF_MODE], } async def async_added_to_hass(self): @@ -216,4 +291,9 @@ class InputText(RestoreEntity): ) return self._current_value = value - await self.async_update_ha_state() + self.async_write_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index de15fbee66e..74d8274796b 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.5"], + "requirements": ["insteonplm==0.16.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index 025d08ac548..f0caf88808a 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/intesishome", "dependencies": [], "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.5"] + "requirements": ["pyintesishome==1.6"] } diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index d01bf3e8da4..cd66ce7461b 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -3,7 +3,7 @@ "name": "Instituto Português do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": ["pyipma==1.2.1"], + "requirements": ["pyipma==2.0.2"], "dependencies": [], - "codeowners": ["@dgomes"] + "codeowners": ["@dgomes", "@abmantis"] } diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index c088d76d165..7b07406d007 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -3,7 +3,8 @@ from datetime import timedelta import logging import async_timeout -from pyipma import Station +from pyipma.api import IPMA_API +from pyipma.location import Location import voluptuous as vol from homeassistant.components.weather import ( @@ -24,8 +25,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Instituto Português do Mar e Atmosfera" -ATTR_WEATHER_DESCRIPTION = "description" - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) CONDITION_CLASSES = { @@ -68,9 +67,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= _LOGGER.error("Latitude or longitude not set in Home Assistant config") return - station = await async_get_station(hass, latitude, longitude) + api = await async_get_api(hass) + location = await async_get_location(hass, api, latitude, longitude) - async_add_entities([IPMAWeather(station, config)], True) + async_add_entities([IPMAWeather(location, api, config)], True) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -78,61 +78,71 @@ async def async_setup_entry(hass, config_entry, async_add_entities): latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] - station = await async_get_station(hass, latitude, longitude) + api = await async_get_api(hass) + location = await async_get_location(hass, api, latitude, longitude) - async_add_entities([IPMAWeather(station, config_entry.data)], True) + async_add_entities([IPMAWeather(location, api, config_entry.data)], True) -async def async_get_station(hass, latitude, longitude): - """Retrieve weather station, station name to be used as the entity name.""" - +async def async_get_api(hass): + """Get the pyipma api object.""" websession = async_get_clientsession(hass) + return IPMA_API(websession) + + +async def async_get_location(hass, api, latitude, longitude): + """Retrieve pyipma location, location name to be used as the entity name.""" with async_timeout.timeout(10): - station = await Station.get(websession, float(latitude), float(longitude)) + location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( "Initializing for coordinates %s, %s -> station %s", latitude, longitude, - station.local, + location.station, ) - return station + return location class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, station, config): + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" - self._station_name = config.get(CONF_NAME, station.local) - self._station = station - self._condition = None + self._api = api + self._location_name = config.get(CONF_NAME, location.name) + self._location = location + self._observation = None self._forecast = None - self._description = None @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Condition and Forecast.""" with async_timeout.timeout(10): - _new_condition = await self._station.observation() - if _new_condition is None: - _LOGGER.warning("Could not update weather conditions") - return - self._condition = _new_condition + new_observation = await self._location.observation(self._api) + new_forecast = await self._location.forecast(self._api) + + if new_observation: + self._observation = new_observation + else: + _LOGGER.warning("Could not update weather observation") + + if new_forecast: + self._forecast = [f for f in new_forecast if f.forecasted_hours == 24] + else: + _LOGGER.warning("Could not update weather forecast") _LOGGER.debug( - "Updating station %s, condition %s", - self._station.local, - self._condition, + "Updated location %s, observation %s", + self._location.name, + self._observation, ) - self._forecast = await self._station.forecast() - self._description = self._forecast[0].description @property def unique_id(self) -> str: """Return a unique id.""" - return f"{self._station.latitude}, {self._station.longitude}" + return f"{self._location.station_latitude}, {self._location.station_longitude}" @property def attribution(self): @@ -142,7 +152,7 @@ class IPMAWeather(WeatherEntity): @property def name(self): """Return the name of the station.""" - return self._station_name + return self._location_name @property def condition(self): @@ -154,7 +164,7 @@ class IPMAWeather(WeatherEntity): ( k for k, v in CONDITION_CLASSES.items() - if self._forecast[0].idWeatherType in v + if self._forecast[0].weather_type in v ), None, ) @@ -162,42 +172,42 @@ class IPMAWeather(WeatherEntity): @property def temperature(self): """Return the current temperature.""" - if not self._condition: + if not self._observation: return None - return self._condition.temperature + return self._observation.temperature @property def pressure(self): """Return the current pressure.""" - if not self._condition: + if not self._observation: return None - return self._condition.pressure + return self._observation.pressure @property def humidity(self): """Return the name of the sensor.""" - if not self._condition: + if not self._observation: return None - return self._condition.humidity + return self._observation.humidity @property def wind_speed(self): """Return the current windspeed.""" - if not self._condition: + if not self._observation: return None - return self._condition.windspeed + return self._observation.wind_intensity_km @property def wind_bearing(self): """Return the current wind bearing (degrees).""" - if not self._condition: + if not self._observation: return None - return self._condition.winddirection + return self._observation.wind_direction @property def temperature_unit(self): @@ -207,33 +217,25 @@ class IPMAWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - if self._forecast: - fcdata_out = [] - for data_in in self._forecast: - data_out = {} - data_out[ATTR_FORECAST_TIME] = data_in.forecastDate - data_out[ATTR_FORECAST_CONDITION] = next( + if not self._forecast: + return [] + + fcdata_out = [ + { + ATTR_FORECAST_TIME: data_in.forecast_date, + ATTR_FORECAST_CONDITION: next( ( k for k, v in CONDITION_CLASSES.items() - if int(data_in.idWeatherType) in v + if int(data_in.weather_type) in v ), None, - ) - data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin - data_out[ATTR_FORECAST_TEMP] = data_in.tMax - data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + ), + ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_PRECIPITATION: data_in.precipitation_probability, + } + for data_in in self._forecast + ] - fcdata_out.append(data_out) - - return fcdata_out - - @property - def device_state_attributes(self): - """Return the state attributes.""" - data = dict() - - if self._description: - data[ATTR_WEATHER_DESCRIPTION] = self._description - - return data + return fcdata_out diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 21c31bbff08..09edca52895 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -50,11 +50,6 @@ TREND_INCREASING = "Increasing" TREND_SUBSIDING = "Subsiding" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up IQVIA sensors based on the old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up IQVIA sensors based on a config entry.""" iqvia = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] diff --git a/homeassistant/components/izone/.translations/de.json b/homeassistant/components/izone/.translations/de.json index 3c7ebfa937f..4a5bace5928 100644 --- a/homeassistant/components/izone/.translations/de.json +++ b/homeassistant/components/izone/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie iZone einrichten?", + "description": "M\u00f6chtest du iZone einrichten?", "title": "iZone" } }, diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 21c19da7b35..2e4644d7ef5 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -24,9 +24,15 @@ SENSOR_TYPES = { }, "time": { "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], - "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"], - "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"], + "talit": ["Talit and Tefillin", "mdi:calendar-clock"], + "gra_end_shma": ['Latest time for Shma Gr"a', "mdi:calendar-clock"], + "mga_end_shma": ['Latest time for Shma MG"A', "mdi:calendar-clock"], + "gra_end_tfila": ['Latest time for Tefilla MG"A', "mdi:calendar-clock"], + "mga_end_tfila": ['Latest time for Tefilla Gr"a', "mdi:calendar-clock"], + "big_mincha": ["Mincha Gedola", "mdi:calendar-clock"], + "small_mincha": ["Mincha Ketana", "mdi:calendar-clock"], "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], + "sunset": ["Shkia", "mdi:weather-sunset"], "first_stars": ["T'set Hakochavim", "mdi:weather-night"], "upcoming_shabbat_candle_lighting": [ "Upcoming Shabbat Candle Lighting", diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index b950b144cf9..a2769cd8eb6 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.5", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.6", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 177b2fccd13..dc91b94f5ef 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -5,7 +5,7 @@ from functools import partial import ipaddress import logging -from aiokef.aiokef import AsyncKefSpeaker +from aiokef import AsyncKefSpeaker from getmac import get_mac_address import voluptuous as vol @@ -36,6 +36,7 @@ DEFAULT_PORT = 50001 DEFAULT_MAX_VOLUME = 0.5 DEFAULT_VOLUME_STEP = 0.05 DEFAULT_INVERSE_SPEAKER_MODE = False +DEFAULT_SUPPORTS_ON = True DOMAIN = "kef" @@ -44,18 +45,10 @@ SCAN_INTERVAL = timedelta(seconds=30) SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] -SUPPORT_KEF = ( - SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_SELECT_SOURCE - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON -) - CONF_MAX_VOLUME = "maximum_volume" CONF_VOLUME_STEP = "volume_step" CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode" +CONF_SUPPORTS_ON = "supports_on" CONF_STANDBY_TIME = "standby_time" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -69,6 +62,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional( CONF_INVERSE_SPEAKER_MODE, default=DEFAULT_INVERSE_SPEAKER_MODE ): cv.boolean, + vol.Optional(CONF_SUPPORTS_ON, default=DEFAULT_SUPPORTS_ON): cv.boolean, vol.Optional(CONF_STANDBY_TIME): vol.In([20, 60]), } ) @@ -86,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= maximum_volume = config[CONF_MAX_VOLUME] volume_step = config[CONF_VOLUME_STEP] inverse_speaker_mode = config[CONF_INVERSE_SPEAKER_MODE] + supports_on = config[CONF_SUPPORTS_ON] standby_time = config.get(CONF_STANDBY_TIME) sources = SOURCES[speaker_type] @@ -117,6 +112,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= volume_step, standby_time, inverse_speaker_mode, + supports_on, sources, ioloop=hass.loop, unique_id=unique_id, @@ -141,6 +137,7 @@ class KefMediaPlayer(MediaPlayerDevice): volume_step, standby_time, inverse_speaker_mode, + supports_on, sources, ioloop, unique_id, @@ -158,6 +155,7 @@ class KefMediaPlayer(MediaPlayerDevice): ioloop=ioloop, ) self._unique_id = unique_id + self._supports_on = supports_on self._state = None self._muted = None @@ -210,7 +208,17 @@ class KefMediaPlayer(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - return SUPPORT_KEF + support_kef = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + | SUPPORT_SELECT_SOURCE + | SUPPORT_TURN_OFF + ) + if self._supports_on: + support_kef |= SUPPORT_TURN_ON + + return support_kef @property def source(self): @@ -243,6 +251,8 @@ class KefMediaPlayer(MediaPlayerDevice): async def async_turn_on(self): """Turn the media player on.""" + if not self._supports_on: + raise NotImplementedError() await self._speaker.turn_on() async def async_volume_up(self): diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index 8914641dd74..330813a7bff 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -48,9 +48,8 @@ class KiraRemote(Entity): _LOGGER.info("Sending Command: %s to %s", *code_tuple) self._kira.sendCode(code_tuple) - def async_send_command(self, command, **kwargs): - """Send a command to a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.send_command, command, **kwargs)) + async def async_send_command(self, command, **kwargs): + """Send a command to a device.""" + return await self.hass.async_add_job( + ft.partial(self.send_command, command, **kwargs) + ) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 819fb1794c3..554ae59f397 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -330,10 +330,7 @@ class KNXClimate(ClimateDevice): return list(filter(None, _presets)) async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set new preset mode. - - This method must be run in the event loop and returns a coroutine. - """ + """Set new preset mode.""" if self.device.mode.supports_operation_mode: knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index c7292309461..6eb539c19ce 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -269,8 +269,16 @@ class KNXLight(Light): update_white_value = ATTR_WHITE_VALUE in kwargs update_color_temp = ATTR_COLOR_TEMP in kwargs - # always only go one path for turning on (avoid conflicting changes - # and weird effects) + # avoid conflicting changes and weird effects + if not ( + self.is_on + or update_brightness + or update_color + or update_white_value + or update_color_temp + ): + await self.device.set_on() + if self.device.supports_brightness and (update_brightness and not update_color): # if we don't need to update the color, try updating brightness # directly if supported; don't do it if color also has to be @@ -279,7 +287,7 @@ class KNXLight(Light): elif (self.device.supports_rgbw or self.device.supports_color) and ( update_brightness or update_color or update_white_value ): - # change RGB color, white value )if supported), and brightness + # change RGB color, white value (if supported), and brightness # if brightness or hs_color was not yet set use the default value # to calculate RGB from as a fallback if brightness is None: @@ -290,29 +298,20 @@ class KNXLight(Light): white_value = DEFAULT_WHITE_VALUE rgb = color_util.color_hsv_to_RGB(*hs_color, brightness * 100 / 255) await self.device.set_color(rgb, white_value) - elif self.device.supports_color_temperature and update_color_temp: - # change color temperature without ON telegram + + if update_color_temp: kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) - if kelvin > self._max_kelvin: - kelvin = self._max_kelvin - elif kelvin < self._min_kelvin: - kelvin = self._min_kelvin - await self.device.set_color_temperature(kelvin) - elif self.device.supports_tunable_white and update_color_temp: - # calculate relative_ct from Kelvin to fit typical KNX devices - kelvin = min( - self._max_kelvin, - int(color_util.color_temperature_mired_to_kelvin(mireds)), - ) - relative_ct = int( - 255 - * (kelvin - self._min_kelvin) - / (self._max_kelvin - self._min_kelvin) - ) - await self.device.set_tunable_white(relative_ct) - else: - # no color/brightness change requested, so just turn it on - await self.device.set_on() + kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) + + if self.device.supports_color_temperature: + await self.device.set_color_temperature(kelvin) + elif self.device.supports_tunable_white: + relative_ct = int( + 255 + * (kelvin - self._min_kelvin) + / (self._max_kelvin - self._min_kelvin) + ) + await self.device.set_tunable_white(relative_ct) async def async_turn_off(self, **kwargs): """Turn the light off.""" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 13aa18d01ad..f326ba60375 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -668,20 +668,14 @@ class KodiDevice(MediaPlayerDevice): assert (await self.server.Input.ExecuteAction("volumedown")) == "OK" @cmd - def async_set_volume_level(self, volume): - """Set volume level, range 0..1. - - This method must be run in the event loop and returns a coroutine. - """ - return self.server.Application.SetVolume(int(volume * 100)) + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self.server.Application.SetVolume(int(volume * 100)) @cmd - def async_mute_volume(self, mute): - """Mute (true) or unmute (false) media player. - - This method must be run in the event loop and returns a coroutine. - """ - return self.server.Application.SetMute(mute) + async def async_mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + await self.server.Application.SetMute(mute) async def async_set_play_state(self, state): """Handle play/pause/toggle.""" @@ -691,28 +685,19 @@ class KodiDevice(MediaPlayerDevice): await self.server.Player.PlayPause(players[0]["playerid"], state) @cmd - def async_media_play_pause(self): - """Pause media on media player. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_set_play_state("toggle") + async def async_media_play_pause(self): + """Pause media on media player.""" + await self.async_set_play_state("toggle") @cmd - def async_media_play(self): - """Play media. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_set_play_state(True) + async def async_media_play(self): + """Play media.""" + await self.async_set_play_state(True) @cmd - def async_media_pause(self): - """Pause the media player. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_set_play_state(False) + async def async_media_pause(self): + """Pause the media player.""" + await self.async_set_play_state(False) @cmd async def async_media_stop(self): @@ -735,20 +720,14 @@ class KodiDevice(MediaPlayerDevice): await self.server.Player.GoTo(players[0]["playerid"], direction) @cmd - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self._goto("next") + async def async_media_next_track(self): + """Send next track command.""" + await self._goto("next") @cmd - def async_media_previous_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self._goto("previous") + async def async_media_previous_track(self): + """Send next track command.""" + await self._goto("previous") @cmd async def async_media_seek(self, position): @@ -772,17 +751,18 @@ class KodiDevice(MediaPlayerDevice): await self.server.Player.Seek(players[0]["playerid"], time) @cmd - def async_play_media(self, media_type, media_id, **kwargs): - """Send the play_media command to the media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" if media_type == "CHANNEL": - return self.server.Player.Open({"item": {"channelid": int(media_id)}}) - if media_type == "PLAYLIST": - return self.server.Player.Open({"item": {"playlistid": int(media_id)}}) - - return self.server.Player.Open({"item": {"file": str(media_id)}}) + await self.server.Player.Open({"item": {"channelid": int(media_id)}}) + elif media_type == "PLAYLIST": + await self.server.Player.Open({"item": {"playlistid": int(media_id)}}) + elif media_type == "DIRECTORY": + await self.server.Player.Open({"item": {"directory": str(media_id)}}) + elif media_type == "PLUGIN": + await self.server.Player.Open({"item": {"file": str(media_id)}}) + else: + await self.server.Player.Open({"item": {"file": str(media_id)}}) async def async_set_shuffle(self, shuffle): """Set shuffle mode, for the first player.""" diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index a72e8929cbc..d00d7d352f2 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,7 +2,7 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==3.1.0"], + "requirements": ["pylast==3.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 68d727626cf..3a830b9f4e6 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -1,4 +1,5 @@ """Sensor for Last.fm account status.""" +import hashlib import logging import re @@ -54,8 +55,10 @@ class LastfmSensor(Entity): def __init__(self, user, lastfm_api): """Initialize the sensor.""" + self._unique_id = hashlib.sha256(user.encode("utf-8")).hexdigest() self._user = lastfm_api.get_user(user) self._name = user + self._entity_id = user self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None @@ -63,6 +66,11 @@ class LastfmSensor(Entity): self._topplayed = None self._cover = None + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self._unique_id + @property def name(self): """Return the name of the sensor.""" @@ -71,7 +79,7 @@ class LastfmSensor(Entity): @property def entity_id(self): """Return the entity ID.""" - return f"sensor.lastfm_{self._name}" + return f"sensor.lastfm_{self._entity_id}" @property def state(self): diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index d27953749f6..1d9323907f2 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.condition import ConditionCheckerType from homeassistant.helpers.typing import ConfigType @@ -16,6 +16,7 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( ) +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: diff --git a/homeassistant/components/linky/.translations/cs.json b/homeassistant/components/linky/.translations/cs.json new file mode 100644 index 00000000000..f914f0f5a1c --- /dev/null +++ b/homeassistant/components/linky/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/da.json b/homeassistant/components/linky/.translations/da.json index cacad99de58..a0bcc5f9b61 100644 --- a/homeassistant/components/linky/.translations/da.json +++ b/homeassistant/components/linky/.translations/da.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigureret", "username_exists": "Kontoen er allerede konfigureret" }, "error": { diff --git a/homeassistant/components/linky/.translations/en.json b/homeassistant/components/linky/.translations/en.json index 6c655b83581..95964cb7805 100644 --- a/homeassistant/components/linky/.translations/en.json +++ b/homeassistant/components/linky/.translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account already configured", "username_exists": "Account already configured" }, "error": { diff --git a/homeassistant/components/linky/.translations/hu.json b/homeassistant/components/linky/.translations/hu.json new file mode 100644 index 00000000000..436e8b1fb7d --- /dev/null +++ b/homeassistant/components/linky/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ko.json b/homeassistant/components/linky/.translations/ko.json index 45172e70097..beac46255db 100644 --- a/homeassistant/components/linky/.translations/ko.json +++ b/homeassistant/components/linky/.translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json index c43f434562c..9951a5c97b4 100644 --- a/homeassistant/components/linky/.translations/no.json +++ b/homeassistant/components/linky/.translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "username_exists": "Kontoen er allerede konfigurert" }, "error": { diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index d4fa7ee4d11..7ab291ceff4 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "username_exists": "Konto jest ju\u017c skonfigurowane" }, "error": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index da34fbbdb62..5f952a29e78 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { diff --git a/homeassistant/components/linky/.translations/sv.json b/homeassistant/components/linky/.translations/sv.json new file mode 100644 index 00000000000..4e7be709482 --- /dev/null +++ b/homeassistant/components/linky/.translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot har redan konfigurerats." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index 1d382b43525..d21c007762c 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -47,6 +47,12 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Linky sensors.""" + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=entry.data[CONF_USERNAME] + ) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py index 8a2d307ceab..88fa725cc4a 100644 --- a/homeassistant/components/linky/config_flow.py +++ b/homeassistant/components/linky/config_flow.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from homeassistant.core import callback -from .const import DEFAULT_TIMEOUT, DOMAIN +from .const import DEFAULT_TIMEOUT +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -25,20 +25,6 @@ class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize Linky config flow.""" - self._username = None - self._password = None - self._timeout = None - - def _configuration_exists(self, username: str) -> bool: - """Return True if username exists in configuration.""" - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_USERNAME] == username: - return True - return False - - @callback def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" @@ -67,15 +53,16 @@ class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is None: return self._show_setup_form(user_input, None) - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - self._timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) - if self._configuration_exists(self._username): - errors[CONF_USERNAME] = "username_exists" - return self._show_setup_form(user_input, errors) + # Check if already configured + if self.unique_id is None: + await self.async_set_unique_id(username) + self._abort_if_unique_id_configured() - client = LinkyClient(self._username, self._password, None, self._timeout) + client = LinkyClient(username, password, None, timeout) try: await self.hass.async_add_executor_job(client.login) await self.hass.async_add_executor_job(client.fetch_data) @@ -99,20 +86,14 @@ class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): client.close_session() return self.async_create_entry( - title=self._username, + title=username, data={ - CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, - CONF_TIMEOUT: self._timeout, + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_TIMEOUT: timeout, }, ) async def async_step_import(self, user_input=None): - """Import a config entry. - - Only host was required in the yaml file all other fields are optional - """ - if self._configuration_exists(user_input[CONF_USERNAME]): - return self.async_abort(reason="username_exists") - + """Import a config entry.""" return await self.async_step_user(user_input) diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 4b5f9ab6cad..9beb9acc403 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -30,11 +30,6 @@ INDEX_LAST = -2 ATTRIBUTION = "Data provided by Enedis" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up the Linky platform.""" - pass - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities ) -> None: diff --git a/homeassistant/components/linky/strings.json b/homeassistant/components/linky/strings.json index e5aa04cad1f..dc4c0bb9651 100644 --- a/homeassistant/components/linky/strings.json +++ b/homeassistant/components/linky/strings.json @@ -12,14 +12,13 @@ } }, "error":{ - "username_exists": "Account already configured", "access": "Could not access to Enedis.fr, please check your internet connection", "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", "wrong_login": "Login error: please check your email & password", "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)" }, "abort":{ - "username_exists": "Account already configured" + "already_configured": "Account already configured" } } } diff --git a/homeassistant/components/local_ip/.translations/nl.json b/homeassistant/components/local_ip/.translations/nl.json index fdffd97427b..4f0d9a437db 100644 --- a/homeassistant/components/local_ip/.translations/nl.json +++ b/homeassistant/components/local_ip/.translations/nl.json @@ -4,8 +4,10 @@ "user": { "data": { "name": "Sensor Naam" - } + }, + "title": "Lokaal IP-adres" } - } + }, + "title": "Lokaal IP-adres" } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json index 1613d974449..de92b9680f0 100644 --- a/homeassistant/components/local_ip/.translations/ru.json +++ b/homeassistant/components/local_ip/.translations/ru.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" } diff --git a/homeassistant/components/locative/.translations/de.json b/homeassistant/components/locative/.translations/de.json index 14e0523fcf6..ff8cfd97b24 100644 --- a/homeassistant/components/locative/.translations/de.json +++ b/homeassistant/components/locative/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie den Locative Webhook wirklich einrichten?", + "description": "M\u00f6chtest du den Locative Webhook wirklich einrichten?", "title": "Locative Webhook einrichten" } }, diff --git a/homeassistant/components/lock/.translations/zh-Hans.json b/homeassistant/components/lock/.translations/zh-Hans.json new file mode 100644 index 00000000000..049d88ba3a3 --- /dev/null +++ b/homeassistant/components/lock/.translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "locked": "{entity_name} \u88ab\u9501\u5b9a", + "unlocked": "{entity_name} \u88ab\u89e3\u9501" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index c788a7c3e8c..92da3a03085 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -104,34 +104,25 @@ class LockDevice(Entity): """Lock the lock.""" raise NotImplementedError() - def async_lock(self, **kwargs): - """Lock the lock. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.lock, **kwargs)) + async def async_lock(self, **kwargs): + """Lock the lock.""" + await self.hass.async_add_job(ft.partial(self.lock, **kwargs)) def unlock(self, **kwargs): """Unlock the lock.""" raise NotImplementedError() - def async_unlock(self, **kwargs): - """Unlock the lock. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + await self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) def open(self, **kwargs): """Open the door latch.""" raise NotImplementedError() - def async_open(self, **kwargs): - """Open the door latch. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + async def async_open(self, **kwargs): + """Open the door latch.""" + await self.hass.async_add_job(ft.partial(self.open, **kwargs)) @property def state_attributes(self): diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 44791320669..a25018dc709 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -63,6 +63,7 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index d797fbbf4ba..4daadcd9c94 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -45,7 +45,7 @@ SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", "%"], SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"], - SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:mdi-download", "Pa"], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:download", "Pa"], SENSOR_PM10: ["PM10", "mdi:thought-bubble", VOLUME_MICROGRAMS_PER_CUBIC_METER], SENSOR_PM2_5: [ "PM2.5", diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 29f85c07a5f..6fc48081adc 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -24,11 +24,6 @@ from .const import ATTR_SENSOR_ID _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up an Luftdaten sensor based on existing config.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up a Luftdaten sensor based on a config entry.""" luftdaten = hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][entry.entry_id] diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 0381d932328..8526f6658c7 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -141,6 +141,7 @@ class Mailbox: self.hass = hass self.name = name + @callback def async_update(self): """Send event notification of updated mailbox.""" self.hass.bus.async_fire(EVENT) @@ -168,7 +169,7 @@ class Mailbox: """Return a list of the current messages.""" raise NotImplementedError() - def async_delete(self, msgid): + async def async_delete(self, msgid): """Delete the specified messages.""" raise NotImplementedError() @@ -236,7 +237,7 @@ class MailboxDeleteView(MailboxView): async def delete(self, request, platform, msgid): """Delete items.""" mailbox = self.get_mailbox(platform) - mailbox.async_delete(msgid) + await mailbox.async_delete(msgid) class MailboxMediaView(MailboxView): diff --git a/homeassistant/components/mailgun/.translations/de.json b/homeassistant/components/mailgun/.translations/de.json index 306757cd528..93412ca75f3 100644 --- a/homeassistant/components/mailgun/.translations/de.json +++ b/homeassistant/components/mailgun/.translations/de.json @@ -5,11 +5,11 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\n Lesen Sie in der [Dokumentation]({docs_url}) wie Sie Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurieren." + "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { - "description": "M\u00f6chten Sie Mailgun wirklich einrichten?", + "description": "M\u00f6chtest du Mailgun wirklich einrichten?", "title": "Mailgun-Webhook einrichten" } }, diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index f11dac357e6..00a82118ec4 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -29,7 +29,6 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change, track_point_in_time import homeassistant.util.dt as dt_util @@ -427,17 +426,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): self.hass, self.entity_id, self._async_state_changed_listener ) - @callback - def message_received(msg): + async def message_received(msg): """Run when new MQTT message has been received.""" if msg.payload == self._payload_disarm: - self.async_alarm_disarm(self._code) + await self.async_alarm_disarm(self._code) elif msg.payload == self._payload_arm_home: - self.async_alarm_arm_home(self._code) + await self.async_alarm_arm_home(self._code) elif msg.payload == self._payload_arm_away: - self.async_alarm_arm_away(self._code) + await self.async_alarm_arm_away(self._code) elif msg.payload == self._payload_arm_night: - self.async_alarm_arm_night(self._code) + await self.async_alarm_arm_night(self._code) else: _LOGGER.warning("Received unexpected payload: %s", msg.payload) return diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 59517e4f1bb..74f027fd076 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -2,7 +2,9 @@ "domain": "marytts", "name": "MaryTTS", "documentation": "https://www.home-assistant.io/integrations/marytts", - "requirements": [], + "requirements": [ + "speak2mary==1.4.0" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 742b5e87661..da8208e1883 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -1,31 +1,29 @@ """Support for the MaryTTS service.""" -import asyncio import logging -import re -import aiohttp -import async_timeout +from speak2mary import MaryTTS import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -SUPPORT_LANGUAGES = ["de", "en-GB", "en-US", "fr", "it", "lb", "ru", "sv", "te", "tr"] - -SUPPORT_CODEC = ["aiff", "au", "wav"] - CONF_VOICE = "voice" CONF_CODEC = "codec" +SUPPORT_LANGUAGES = MaryTTS.supported_locales() +SUPPORT_CODEC = MaryTTS.supported_codecs() +SUPPORT_OPTIONS = [CONF_EFFECT] +SUPPORT_EFFECTS = MaryTTS.supported_effects().keys() + DEFAULT_HOST = "localhost" DEFAULT_PORT = 59125 -DEFAULT_LANG = "en-US" +DEFAULT_LANG = "en_US" DEFAULT_VOICE = "cmu-slt-hsmm" -DEFAULT_CODEC = "wav" +DEFAULT_CODEC = "WAVE_FILE" +DEFAULT_EFFECTS = {} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -34,6 +32,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC), + vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECTS): { + vol.All(cv.string, vol.In(SUPPORT_EFFECTS)): cv.string + }, } ) @@ -49,57 +50,40 @@ class MaryTTSProvider(Provider): def __init__(self, hass, conf): """Init MaryTTS TTS service.""" self.hass = hass - self._host = conf.get(CONF_HOST) - self._port = conf.get(CONF_PORT) - self._codec = conf.get(CONF_CODEC) - self._voice = conf.get(CONF_VOICE) - self._language = conf.get(CONF_LANG) + self._mary = MaryTTS( + conf.get(CONF_HOST), + conf.get(CONF_PORT), + conf.get(CONF_CODEC), + conf.get(CONF_LANG), + conf.get(CONF_VOICE), + ) + self._effects = conf.get(CONF_EFFECT) self.name = "MaryTTS" @property def default_language(self): """Return the default language.""" - return self._language + return self._mary.locale @property def supported_languages(self): """Return list of supported languages.""" return SUPPORT_LANGUAGES + @property + def default_options(self): + """Return dict include default options.""" + return {CONF_EFFECT: self._effects} + + @property + def supported_options(self): + """Return a list of supported options.""" + return SUPPORT_OPTIONS + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from MaryTTS.""" - websession = async_get_clientsession(self.hass) + effects = options[CONF_EFFECT] - actual_language = re.sub("-", "_", language) + data = self._mary.speak(message, effects) - try: - with async_timeout.timeout(10): - url = f"http://{self._host}:{self._port}/process?" - - audio = self._codec.upper() - if audio == "WAV": - audio = "WAVE" - - url_param = { - "INPUT_TEXT": message, - "INPUT_TYPE": "TEXT", - "AUDIO": audio, - "VOICE": self._voice, - "OUTPUT_TYPE": "AUDIO", - "LOCALE": actual_language, - } - - request = await websession.get(url, params=url_param) - - if request.status != 200: - _LOGGER.error( - "Error %d on load url %s", request.status, request.url - ) - return (None, None) - data = await request.read() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for MaryTTS API") - return (None, None) - - return (self._codec, data) + return self._mary.codec, data diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4781b2c7693..28bbc92b850 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.01.01"], + "requirements": ["youtube_dl==2020.01.24"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/.translations/zh-Hans.json b/homeassistant/components/media_player/.translations/zh-Hans.json new file mode 100644 index 00000000000..c4020b8194b --- /dev/null +++ b/homeassistant/components/media_player/.translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u7a7a\u95f2", + "is_off": "{entity_name} \u5df2\u5173\u95ed", + "is_on": "{entity_name} \u5df2\u5f00\u542f", + "is_paused": "{entity_name} \u5df2\u6682\u505c", + "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b73208402f8..2911a143a3c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -173,6 +173,23 @@ SCHEMA_WEBSOCKET_GET_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.exten ) +def _rename_keys(**keys): + """Create validator that renames keys. + + Necessary because the service schema names do not match the command parameters. + + Async friendly. + """ + + def rename(value): + for to_key, from_key in keys.items(): + if from_key in value: + value[to_key] = value.pop(from_key) + return value + + return rename + + async def async_setup(hass, config): """Track states and offer events for media_players.""" component = hass.data[DOMAIN] = EntityComponent( @@ -238,30 +255,39 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_VOLUME_SET, - {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, - lambda entity, call: entity.async_set_volume_level( - volume=call.data[ATTR_MEDIA_VOLUME_LEVEL] + vol.All( + cv.make_entity_service_schema( + {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float} + ), + _rename_keys(volume=ATTR_MEDIA_VOLUME_LEVEL), ), + "async_set_volume_level", [SUPPORT_VOLUME_SET], ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, - {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}, - lambda entity, call: entity.async_mute_volume( - mute=call.data[ATTR_MEDIA_VOLUME_MUTED] + vol.All( + cv.make_entity_service_schema( + {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean} + ), + _rename_keys(mute=ATTR_MEDIA_VOLUME_MUTED), ), + "async_mute_volume", [SUPPORT_VOLUME_MUTE], ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, - { - vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( - vol.Coerce(float), vol.Range(min=0) - ) - }, - lambda entity, call: entity.async_media_seek( - position=call.data[ATTR_MEDIA_SEEK_POSITION] + vol.All( + cv.make_entity_service_schema( + { + vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( + vol.Coerce(float), vol.Range(min=0) + ) + } + ), + _rename_keys(position=ATTR_MEDIA_SEEK_POSITION), ), + "async_media_seek", [SUPPORT_SEEK], ) component.async_register_entity_service( @@ -278,12 +304,15 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_PLAY_MEDIA, - MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, - lambda entity, call: entity.async_play_media( - media_type=call.data[ATTR_MEDIA_CONTENT_TYPE], - media_id=call.data[ATTR_MEDIA_CONTENT_ID], - enqueue=call.data.get(ATTR_MEDIA_ENQUEUE), + vol.All( + cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), + _rename_keys( + media_type=ATTR_MEDIA_CONTENT_TYPE, + media_id=ATTR_MEDIA_CONTENT_ID, + enqueue=ATTR_MEDIA_ENQUEUE, + ), ), + "async_play_media", [SUPPORT_PLAY_MEDIA], ) component.async_register_entity_service( @@ -485,122 +514,89 @@ class MediaPlayerDevice(Entity): """Turn the media player on.""" raise NotImplementedError() - def async_turn_on(self): - """Turn the media player on. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_on) + async def async_turn_on(self): + """Turn the media player on.""" + await self.hass.async_add_job(self.turn_on) def turn_off(self): """Turn the media player off.""" raise NotImplementedError() - def async_turn_off(self): - """Turn the media player off. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_off) + async def async_turn_off(self): + """Turn the media player off.""" + await self.hass.async_add_job(self.turn_off) def mute_volume(self, mute): """Mute the volume.""" raise NotImplementedError() - def async_mute_volume(self, mute): - """Mute the volume. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.mute_volume, mute) + async def async_mute_volume(self, mute): + """Mute the volume.""" + await self.hass.async_add_job(self.mute_volume, mute) def set_volume_level(self, volume): """Set volume level, range 0..1.""" raise NotImplementedError() - def async_set_volume_level(self, volume): - """Set volume level, range 0..1. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_volume_level, volume) + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self.hass.async_add_job(self.set_volume_level, volume) def media_play(self): """Send play command.""" raise NotImplementedError() - def async_media_play(self): - """Send play command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_play) + async def async_media_play(self): + """Send play command.""" + await self.hass.async_add_job(self.media_play) def media_pause(self): """Send pause command.""" raise NotImplementedError() - def async_media_pause(self): - """Send pause command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_pause) + async def async_media_pause(self): + """Send pause command.""" + await self.hass.async_add_job(self.media_pause) def media_stop(self): """Send stop command.""" raise NotImplementedError() - def async_media_stop(self): - """Send stop command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_stop) + async def async_media_stop(self): + """Send stop command.""" + await self.hass.async_add_job(self.media_stop) def media_previous_track(self): """Send previous track command.""" raise NotImplementedError() - def async_media_previous_track(self): - """Send previous track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_previous_track) + async def async_media_previous_track(self): + """Send previous track command.""" + await self.hass.async_add_job(self.media_previous_track) def media_next_track(self): """Send next track command.""" raise NotImplementedError() - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_next_track) + async def async_media_next_track(self): + """Send next track command.""" + await self.hass.async_add_job(self.media_next_track) def media_seek(self, position): """Send seek command.""" raise NotImplementedError() - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.media_seek, position) + async def async_media_seek(self, position): + """Send seek command.""" + await self.hass.async_add_job(self.media_seek, position) def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" raise NotImplementedError() - def async_play_media(self, media_type, media_id, **kwargs): - """Play a piece of media. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job( + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + await self.hass.async_add_job( ft.partial(self.play_media, media_type, media_id, **kwargs) ) @@ -608,45 +604,33 @@ class MediaPlayerDevice(Entity): """Select input source.""" raise NotImplementedError() - def async_select_source(self, source): - """Select input source. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.select_source, source) + async def async_select_source(self, source): + """Select input source.""" + await self.hass.async_add_job(self.select_source, source) def select_sound_mode(self, sound_mode): """Select sound mode.""" raise NotImplementedError() - def async_select_sound_mode(self, sound_mode): - """Select sound mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.select_sound_mode, sound_mode) + async def async_select_sound_mode(self, sound_mode): + """Select sound mode.""" + await self.hass.async_add_job(self.select_sound_mode, sound_mode) def clear_playlist(self): """Clear players playlist.""" raise NotImplementedError() - def async_clear_playlist(self): - """Clear players playlist. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.clear_playlist) + async def async_clear_playlist(self): + """Clear players playlist.""" + await self.hass.async_add_job(self.clear_playlist) def set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" raise NotImplementedError() - def async_set_shuffle(self, shuffle): - """Enable/disable shuffle mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_shuffle, shuffle) + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + await self.hass.async_add_job(self.set_shuffle, shuffle) # No need to overwrite these. @property @@ -714,18 +698,17 @@ class MediaPlayerDevice(Entity): """Boolean if shuffle is supported.""" return bool(self.supported_features & SUPPORT_SHUFFLE_SET) - def async_toggle(self): - """Toggle the power on the media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_toggle(self): + """Toggle the power on the media player.""" if hasattr(self, "toggle"): # pylint: disable=no-member - return self.hass.async_add_job(self.toggle) + await self.hass.async_add_job(self.toggle) + return if self.state in [STATE_OFF, STATE_IDLE]: - return self.async_turn_on() - return self.async_turn_off() + await self.async_turn_on() + else: + await self.async_turn_off() async def async_volume_up(self): """Turn volume up for media player. @@ -753,18 +736,17 @@ class MediaPlayerDevice(Entity): if self.volume_level > 0 and self.supported_features & SUPPORT_VOLUME_SET: await self.async_set_volume_level(max(0, self.volume_level - 0.1)) - def async_media_play_pause(self): - """Play or pause the media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_play_pause(self): + """Play or pause the media player.""" if hasattr(self, "media_play_pause"): # pylint: disable=no-member - return self.hass.async_add_job(self.media_play_pause) + await self.hass.async_add_job(self.media_play_pause) + return if self.state == STATE_PLAYING: - return self.async_media_pause() - return self.async_media_play() + await self.async_media_pause() + else: + await self.async_media_play() @property def entity_picture(self): diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index a8091a6aed8..6faa6521b70 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -16,7 +16,7 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -95,6 +95,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/mikrotik/.translations/en.json b/homeassistant/components/mikrotik/.translations/en.json new file mode 100644 index 00000000000..590563993d6 --- /dev/null +++ b/homeassistant/components/mikrotik/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Mikrotik", + "step": { + "user": { + "title": "Set up Mikrotik Router", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "verify_ssl": "Use ssl" + } + } + }, + "error": { + "name_exists": "Name exists", + "cannot_connect": "Connection Unsuccessful", + "wrong_credentials": "Wrong Credentials" + }, + "abort": { + "already_configured": "Mikrotik is already configured" + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Enable ARP ping", + "force_dhcp": "Force scanning using DHCP", + "detection_time": "Consider home interval" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 3892f9cb20d..9a8ee7bdb45 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,42 +1,28 @@ -"""The mikrotik component.""" -import logging -import ssl - -import librouteros -from librouteros.login import login_plain, login_token +"""The Mikrotik component.""" import voluptuous as vol -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_METHOD, + CONF_NAME, CONF_PASSWORD, CONF_PORT, - CONF_SSL, CONF_USERNAME, + CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import load_platform from .const import ( + ATTR_MANUFACTURER, CONF_ARP_PING, - CONF_ENCODING, - CONF_LOGIN_METHOD, - CONF_TRACK_DEVICES, - DEFAULT_ENCODING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_API_PORT, + DEFAULT_DETECTION_TIME, + DEFAULT_NAME, DOMAIN, - HOSTS, - IDENTITY, - MIKROTIK_SERVICES, - MTK_LOGIN_PLAIN, - MTK_LOGIN_TOKEN, - NAME, ) - -_LOGGER = logging.getLogger(__name__) - -MTK_DEFAULT_API_PORT = "8728" -MTK_DEFAULT_API_SSL_PORT = "8729" +from .hub import MikrotikHub MIKROTIK_SCHEMA = vol.All( vol.Schema( @@ -44,13 +30,14 @@ MIKROTIK_SCHEMA = vol.All( vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_METHOD): cv.string, - vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): cv.port, + vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean, vol.Optional(CONF_ARP_PING, default=False): cv.boolean, + vol.Optional(CONF_FORCE_DHCP, default=False): cv.boolean, + vol.Optional( + CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME + ): cv.time_period, } ) ) @@ -60,143 +47,45 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the Mikrotik component.""" - hass.data[DOMAIN] = {HOSTS: {}} +async def async_setup(hass, config): + """Import the Mikrotik component from config.""" - for device in config[DOMAIN]: - host = device[CONF_HOST] - use_ssl = device.get(CONF_SSL) - user = device.get(CONF_USERNAME) - password = device.get(CONF_PASSWORD, "") - login = device.get(CONF_LOGIN_METHOD) - encoding = device.get(CONF_ENCODING) - track_devices = device.get(CONF_TRACK_DEVICES) - - if CONF_PORT in device: - port = device.get(CONF_PORT) - else: - if use_ssl: - port = MTK_DEFAULT_API_SSL_PORT - else: - port = MTK_DEFAULT_API_PORT - - if login == MTK_LOGIN_PLAIN: - login_method = (login_plain,) - elif login == MTK_LOGIN_TOKEN: - login_method = (login_token,) - else: - login_method = (login_plain, login_token) - - try: - api = MikrotikClient( - host, use_ssl, port, user, password, login_method, encoding + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - api.connect_to_device() - hass.data[DOMAIN][HOSTS][host] = {"config": device, "api": api} - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Mikrotik %s error %s", host, api_error) - continue - if track_devices: - hass.data[DOMAIN][HOSTS][host][DEVICE_TRACKER] = True - load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config) - - if not hass.data[DOMAIN][HOSTS]: - return False return True -class MikrotikClient: - """Handle all communication with the Mikrotik API.""" +async def async_setup_entry(hass, config_entry): + """Set up the Mikrotik component.""" - def __init__(self, host, use_ssl, port, user, password, login_method, encoding): - """Initialize the Mikrotik Client.""" - self._host = host - self._use_ssl = use_ssl - self._port = port - self._user = user - self._password = password - self._login_method = login_method - self._encoding = encoding - self._ssl_wrapper = None - self.hostname = None - self._client = None - self._connected = False + hub = MikrotikHub(hass, config_entry) + if not await hub.async_setup(): + return False - def connect_to_device(self): - """Connect to Mikrotik device.""" - self._connected = False - _LOGGER.debug("[%s] Connecting to Mikrotik device", self._host) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(DOMAIN, hub.serial_num)}, + manufacturer=ATTR_MANUFACTURER, + model=hub.model, + name=hub.hostname, + sw_version=hub.firmware, + ) - kwargs = { - "encoding": self._encoding, - "login_methods": self._login_method, - "port": self._port, - } + return True - if self._use_ssl: - if self._ssl_wrapper is None: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - self._ssl_wrapper = ssl_context.wrap_socket - kwargs["ssl_wrapper"] = self._ssl_wrapper - try: - self._client = librouteros.connect( - self._host, self._user, self._password, **kwargs - ) - self._connected = True - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Mikrotik %s: %s", self._host, api_error) - self._client = None - return False +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") - self.hostname = self.get_hostname() - _LOGGER.info("Mikrotik Connected to %s (%s)", self.hostname, self._host) - return self._connected + hass.data[DOMAIN].pop(config_entry.entry_id) - def get_hostname(self): - """Return device host name.""" - data = list(self.command(MIKROTIK_SERVICES[IDENTITY])) - return data[0][NAME] if data else None - - def connected(self): - """Return connected boolean.""" - return self._connected - - def command(self, cmd, params=None): - """Retrieve data from Mikrotik API.""" - if not self._connected or not self._client: - if not self.connect_to_device(): - return None - try: - if params: - response = self._client(cmd=cmd, **params) - else: - response = self._client(cmd=cmd) - except (librouteros.exceptions.ConnectionError,) as api_error: - _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - self.connect_to_device() - return None - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - ) as api_error: - _LOGGER.error( - "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", - self._host, - cmd, - api_error, - ) - return None - return response if response else None + return True diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py new file mode 100644 index 00000000000..c1a41abf0d0 --- /dev/null +++ b/homeassistant/components/mikrotik/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for Mikrotik.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback + +from .const import ( + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_API_PORT, + DEFAULT_DETECTION_TIME, + DEFAULT_NAME, + DOMAIN, +) +from .errors import CannotConnect, LoginError +from .hub import get_api + + +class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Mikrotik config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return MikrotikOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + + try: + await self.hass.async_add_executor_job(get_api, self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except LoginError: + errors[CONF_USERNAME] = "wrong_credentials" + errors[CONF_PASSWORD] = "wrong_credentials" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): int, + vol.Optional(CONF_VERIFY_SSL, default=False): bool, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config): + """Import Miktortik from config.""" + + import_config[CONF_DETECTION_TIME] = import_config[CONF_DETECTION_TIME].seconds + return await self.async_step_user(user_input=import_config) + + +class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Mikrotik options.""" + + def __init__(self, config_entry): + """Initialize Mikrotik options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Mikrotik options.""" + return await self.async_step_device_tracker() + + async def async_step_device_tracker(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_FORCE_DHCP, + default=self.config_entry.options.get(CONF_FORCE_DHCP, False), + ): bool, + vol.Optional( + CONF_ARP_PING, + default=self.config_entry.options.get(CONF_ARP_PING, False), + ): bool, + vol.Optional( + CONF_DETECTION_TIME, + default=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + ): int, + } + + return self.async_show_form( + step_id="device_tracker", data_schema=vol.Schema(options) + ) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index bd26b02fe1b..d66a441aaf7 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,32 +1,38 @@ """Constants used in the Mikrotik components.""" DOMAIN = "mikrotik" -MIKROTIK = DOMAIN -HOSTS = "hosts" -MTK_LOGIN_PLAIN = "plain" -MTK_LOGIN_TOKEN = "token" +DEFAULT_NAME = "Mikrotik" +DEFAULT_API_PORT = 8728 +DEFAULT_DETECTION_TIME = 300 + +ATTR_MANUFACTURER = "Mikrotik" +ATTR_SERIAL_NUMBER = "serial-number" +ATTR_FIRMWARE = "current-firmware" +ATTR_MODEL = "model" CONF_ARP_PING = "arp_ping" -CONF_TRACK_DEVICES = "track_devices" -CONF_LOGIN_METHOD = "login_method" -CONF_ENCODING = "encoding" -DEFAULT_ENCODING = "utf-8" +CONF_FORCE_DHCP = "force_dhcp" +CONF_DETECTION_TIME = "detection_time" + NAME = "name" INFO = "info" IDENTITY = "identity" ARP = "arp" + +CAPSMAN = "capsman" DHCP = "dhcp" WIRELESS = "wireless" -CAPSMAN = "capsman" +IS_WIRELESS = "is_wireless" MIKROTIK_SERVICES = { - INFO: "/system/routerboard/getall", - IDENTITY: "/system/identity/getall", ARP: "/ip/arp/getall", - DHCP: "/ip/dhcp-server/lease/getall", - WIRELESS: "/interface/wireless/registration-table/getall", CAPSMAN: "/caps-man/registration-table/getall", + DHCP: "/ip/dhcp-server/lease/getall", + IDENTITY: "/system/identity/getall", + INFO: "/system/routerboard/getall", + WIRELESS: "/interface/wireless/registration-table/getall", + IS_WIRELESS: "/interface/wireless/print", } ATTR_DEVICE_TRACKER = [ @@ -34,16 +40,8 @@ ATTR_DEVICE_TRACKER = [ "mac-address", "ssid", "interface", - "host-name", - "last-seen", - "rx-signal", "signal-strength", - "tx-ccq", "signal-to-noise", - "wmm-enabled", - "authentication-type", - "encryption", - "tx-rate-set", "rx-rate", "tx-rate", "uptime", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 92fcfac4ae4..e7c5e5655a0 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,191 +1,142 @@ """Support for Mikrotik routers as device tracker.""" import logging -from homeassistant.components.device_tracker import ( +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, - DeviceScanner, + SOURCE_TYPE_ROUTER, ) -from homeassistant.const import CONF_METHOD -from homeassistant.util import slugify +from homeassistant.core import callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.dt as dt_util -from .const import ( - ARP, - ATTR_DEVICE_TRACKER, - CAPSMAN, - CONF_ARP_PING, - DHCP, - HOSTS, - MIKROTIK, - MIKROTIK_SERVICES, - WIRELESS, -) +from .const import ATTR_MANUFACTURER, DOMAIN _LOGGER = logging.getLogger(__name__) -def get_scanner(hass, config): - """Validate the configuration and return MikrotikScanner.""" - for host in hass.data[MIKROTIK][HOSTS]: - if DEVICE_TRACKER not in hass.data[MIKROTIK][HOSTS][host]: - continue - hass.data[MIKROTIK][HOSTS][host].pop(DEVICE_TRACKER, None) - api = hass.data[MIKROTIK][HOSTS][host]["api"] - config = hass.data[MIKROTIK][HOSTS][host]["config"] - hostname = api.get_hostname() - scanner = MikrotikScanner(api, host, hostname, config) - return scanner if scanner.success_init else None +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up device tracker for Mikrotik component.""" + hub = hass.data[DOMAIN][config_entry.entry_id] + + tracked = {} + + registry = await entity_registry.async_get_registry(hass) + + # Restore clients that is not a part of active clients list. + for entity in registry.entities.values(): + + if ( + entity.config_entry_id == config_entry.entry_id + and entity.domain == DEVICE_TRACKER + ): + + if ( + entity.unique_id in hub.api.devices + or entity.unique_id not in hub.api.all_devices + ): + continue + hub.api.restore_device(entity.unique_id) + + @callback + def update_hub(): + """Update the status of the device.""" + update_items(hub, async_add_entities, tracked) + + async_dispatcher_connect(hass, hub.signal_update, update_hub) + + update_hub() -class MikrotikScanner(DeviceScanner): - """This class queries a Mikrotik device.""" +@callback +def update_items(hub, async_add_entities, tracked): + """Update tracked device state from the hub.""" + new_tracked = [] + for mac, device in hub.api.devices.items(): + if mac not in tracked: + tracked[mac] = MikrotikHubTracker(device, hub) + new_tracked.append(tracked[mac]) - def __init__(self, api, host, hostname, config): - """Initialize the scanner.""" - self.api = api - self.config = config - self.host = host - self.hostname = hostname - self.method = config.get(CONF_METHOD) - self.arp_ping = config.get(CONF_ARP_PING) - self.dhcp = None - self.devices_arp = {} - self.devices_dhcp = {} - self.device_tracker = None - self.success_init = self.api.connected() - - def get_extra_attributes(self, device): - """ - Get extra attributes of a device. - - Some known extra attributes that may be returned in the device tuple - include MAC address (mac), network device (dev), IP address - (ip), reachable status (reachable), associated router - (host), hostname if known (hostname) among others. - """ - return self.device_tracker.get(device) or {} - - def get_device_name(self, device): - """Get name for a device.""" - host = self.device_tracker.get(device, {}) - return host.get("host_name") - - def scan_devices(self): - """Scan for new devices and return a list with found device MACs.""" - self.update_device_tracker() - return list(self.device_tracker) - - def get_method(self): - """Determine the device tracker polling method.""" - if self.method: - _LOGGER.debug( - "Mikrotik %s: Manually selected polling method %s", - self.host, - self.method, - ) - return self.method - - capsman = self.api.command(MIKROTIK_SERVICES[CAPSMAN]) - if not capsman: - _LOGGER.debug( - "Mikrotik %s: Not a CAPsMAN controller. " - "Trying local wireless interfaces", - (self.host), - ) - else: - return CAPSMAN - - wireless = self.api.command(MIKROTIK_SERVICES[WIRELESS]) - if not wireless: - _LOGGER.info( - "Mikrotik %s: Wireless adapters not found. Try to " - "use DHCP lease table as presence tracker source. " - "Please decrease lease time as much as possible", - self.host, - ) - return DHCP - - return WIRELESS - - def update_device_tracker(self): - """Update device_tracker from Mikrotik API.""" - self.device_tracker = {} - if not self.method: - self.method = self.get_method() - - data = self.api.command(MIKROTIK_SERVICES[self.method]) - if data is None: - return - - if self.method != DHCP: - dhcp = self.api.command(MIKROTIK_SERVICES[DHCP]) - if dhcp is not None: - self.devices_dhcp = load_mac(dhcp) - - arp = self.api.command(MIKROTIK_SERVICES[ARP]) - self.devices_arp = load_mac(arp) - - for device in data: - mac = device.get("mac-address") - if self.method == DHCP: - if "active-address" not in device: - continue - - if self.arp_ping and self.devices_arp: - if mac not in self.devices_arp: - continue - ip_address = self.devices_arp[mac]["address"] - interface = self.devices_arp[mac]["interface"] - if not self.do_arp_ping(ip_address, interface): - continue - - attrs = {} - if mac in self.devices_dhcp and "host-name" in self.devices_dhcp[mac]: - hostname = self.devices_dhcp[mac].get("host-name") - if hostname: - attrs["host_name"] = hostname - - if self.devices_arp and mac in self.devices_arp: - attrs["ip_address"] = self.devices_arp[mac].get("address") - - for attr in ATTR_DEVICE_TRACKER: - if attr in device and device[attr] is not None: - attrs[slugify(attr)] = device[attr] - attrs["scanner_type"] = self.method - attrs["scanner_host"] = self.host - attrs["scanner_hostname"] = self.hostname - self.device_tracker[mac] = attrs - - def do_arp_ping(self, ip_address, interface): - """Attempt to arp ping MAC address via interface.""" - params = { - "arp-ping": "yes", - "interval": "100ms", - "count": 3, - "interface": interface, - "address": ip_address, - } - cmd = "/ping" - data = self.api.command(cmd, params) - if data is not None: - status = 0 - for result in data: - if "status" in result: - _LOGGER.debug( - "Mikrotik %s arp_ping error: %s", self.host, result["status"] - ) - status += 1 - if status == len(data): - return None - return data + if new_tracked: + async_add_entities(new_tracked) -def load_mac(devices=None): - """Load dictionary using MAC address as key.""" - if not devices: +class MikrotikHubTracker(ScannerEntity): + """Representation of network device.""" + + def __init__(self, device, hub): + """Initialize the tracked device.""" + self.device = device + self.hub = hub + self.unsub_dispatcher = None + + @property + def is_connected(self): + """Return true if the client is connected to the network.""" + if ( + self.device.last_seen + and (dt_util.utcnow() - self.device.last_seen) + < self.hub.option_detection_time + ): + return True + return False + + @property + def source_type(self): + """Return the source type of the client.""" + return SOURCE_TYPE_ROUTER + + @property + def name(self) -> str: + """Return the name of the client.""" + return self.device.name + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return self.device.mac + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.hub.available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if self.is_connected: + return self.device.attrs return None - mac_devices = {} - for device in devices: - if "mac-address" in device: - mac = device.pop("mac-address") - mac_devices[mac] = device - return mac_devices + + @property + def device_info(self): + """Return a client description for device registry.""" + info = { + "connections": {(CONNECTION_NETWORK_MAC, self.device.mac)}, + "manufacturer": ATTR_MANUFACTURER, + "identifiers": {(DOMAIN, self.device.mac)}, + "name": self.name, + "via_device": (DOMAIN, self.hub.serial_num), + } + return info + + async def async_added_to_hass(self): + """Client entity created.""" + _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) + self.unsub_dispatcher = async_dispatcher_connect( + self.hass, self.hub.signal_update, self.async_write_ha_state + ) + + async def async_update(self): + """Synchronize state with hub.""" + _LOGGER.debug( + "Updating Mikrotik tracked client %s (%s)", self.entity_id, self.unique_id + ) + await self.hub.request_update() + + async def will_remove_from_hass(self): + """Disconnect from dispatcher.""" + if self.unsub_dispatcher: + self.unsub_dispatcher() diff --git a/homeassistant/components/mikrotik/errors.py b/homeassistant/components/mikrotik/errors.py new file mode 100644 index 00000000000..22cd63d7468 --- /dev/null +++ b/homeassistant/components/mikrotik/errors.py @@ -0,0 +1,10 @@ +"""Errors for the Mikrotik component.""" +from homeassistant.exceptions import HomeAssistantError + + +class CannotConnect(HomeAssistantError): + """Unable to connect to the hub.""" + + +class LoginError(HomeAssistantError): + """Component got logged out.""" diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py new file mode 100644 index 00000000000..2243b6cc5ce --- /dev/null +++ b/homeassistant/components/mikrotik/hub.py @@ -0,0 +1,413 @@ +"""The Mikrotik router class.""" +from datetime import timedelta +import logging +import socket +import ssl + +import librouteros +from librouteros.login import plain as login_plain, token as login_token + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util import slugify +import homeassistant.util.dt as dt_util + +from .const import ( + ARP, + ATTR_DEVICE_TRACKER, + ATTR_FIRMWARE, + ATTR_MODEL, + ATTR_SERIAL_NUMBER, + CAPSMAN, + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_DETECTION_TIME, + DHCP, + IDENTITY, + INFO, + IS_WIRELESS, + MIKROTIK_SERVICES, + NAME, + WIRELESS, +) +from .errors import CannotConnect, LoginError + +_LOGGER = logging.getLogger(__name__) + + +class Device: + """Represents a network device.""" + + def __init__(self, mac, params): + """Initialize the network device.""" + self._mac = mac + self._params = params + self._last_seen = None + self._attrs = {} + self._wireless_params = None + + @property + def name(self): + """Return device name.""" + return self._params.get("host-name", self.mac) + + @property + def mac(self): + """Return device mac.""" + return self._mac + + @property + def last_seen(self): + """Return device last seen.""" + return self._last_seen + + @property + def attrs(self): + """Return device attributes.""" + attr_data = self._wireless_params if self._wireless_params else self._params + for attr in ATTR_DEVICE_TRACKER: + if attr in attr_data: + self._attrs[slugify(attr)] = attr_data[attr] + self._attrs["ip_address"] = self._params.get("active-address") + return self._attrs + + def update(self, wireless_params=None, params=None, active=False): + """Update Device params.""" + if wireless_params: + self._wireless_params = wireless_params + if params: + self._params = params + if active: + self._last_seen = dt_util.utcnow() + + +class MikrotikData: + """Handle all communication with the Mikrotik API.""" + + def __init__(self, hass, config_entry, api): + """Initialize the Mikrotik Client.""" + self.hass = hass + self.config_entry = config_entry + self.api = api + self._host = self.config_entry.data[CONF_HOST] + self.all_devices = {} + self.devices = {} + self.available = True + self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) + self.hostname = None + self.model = None + self.firmware = None + self.serial_number = None + + @staticmethod + def load_mac(devices=None): + """Load dictionary using MAC address as key.""" + if not devices: + return None + mac_devices = {} + for device in devices: + if "mac-address" in device: + mac = device["mac-address"] + mac_devices[mac] = device + return mac_devices + + @property + def arp_enabled(self): + """Return arp_ping option setting.""" + return self.config_entry.options[CONF_ARP_PING] + + @property + def force_dhcp(self): + """Return force_dhcp option setting.""" + return self.config_entry.options[CONF_FORCE_DHCP] + + def get_info(self, param): + """Return device model name.""" + cmd = IDENTITY if param == NAME else INFO + data = list(self.command(MIKROTIK_SERVICES[cmd])) + return data[0].get(param) if data else None + + def get_hub_details(self): + """Get Hub info.""" + self.hostname = self.get_info(NAME) + self.model = self.get_info(ATTR_MODEL) + self.firmware = self.get_info(ATTR_FIRMWARE) + self.serial_number = self.get_info(ATTR_SERIAL_NUMBER) + + def connect_to_hub(self): + """Connect to hub.""" + try: + self.api = get_api(self.hass, self.config_entry.data) + self.available = True + return True + except (LoginError, CannotConnect): + self.available = False + return False + + def get_list_from_interface(self, interface): + """Get devices from interface.""" + result = list(self.command(MIKROTIK_SERVICES[interface])) + return self.load_mac(result) if result else {} + + def restore_device(self, mac): + """Restore a missing device after restart.""" + self.devices[mac] = Device(mac, self.all_devices[mac]) + + def update_devices(self): + """Get list of devices with latest status.""" + arp_devices = {} + wireless_devices = {} + device_list = {} + try: + self.all_devices = self.get_list_from_interface(DHCP) + if self.support_wireless: + _LOGGER.debug("wireless is supported") + for interface in [CAPSMAN, WIRELESS]: + wireless_devices = self.get_list_from_interface(interface) + if wireless_devices: + _LOGGER.debug("Scanning wireless devices using %s", interface) + break + + if self.support_wireless and not self.force_dhcp: + device_list = wireless_devices + else: + device_list = self.all_devices + _LOGGER.debug("Falling back to DHCP for scanning devices") + + if self.arp_enabled: + arp_devices = self.get_list_from_interface(ARP) + + # get new hub firmware version if updated + self.firmware = self.get_info(ATTR_FIRMWARE) + + except (CannotConnect, socket.timeout, socket.error): + self.available = False + return + + if not device_list: + return + + for mac, params in device_list.items(): + if mac not in self.devices: + self.devices[mac] = Device(mac, self.all_devices.get(mac, {})) + else: + self.devices[mac].update(params=self.all_devices.get(mac, {})) + + if mac in wireless_devices: + # if wireless is supported then wireless_params are params + self.devices[mac].update( + wireless_params=wireless_devices[mac], active=True + ) + continue + # for wired devices or when forcing dhcp check for active-address + if not params.get("active-address"): + self.devices[mac].update(active=False) + continue + # ping check the rest of active devices if arp ping is enabled + active = True + if self.arp_enabled and mac in arp_devices: + active = self.do_arp_ping( + params.get("active-address"), arp_devices[mac].get("interface") + ) + self.devices[mac].update(active=active) + + def do_arp_ping(self, ip_address, interface): + """Attempt to arp ping MAC address via interface.""" + _LOGGER.debug("pinging - %s", ip_address) + params = { + "arp-ping": "yes", + "interval": "100ms", + "count": 3, + "interface": interface, + "address": ip_address, + } + cmd = "/ping" + data = list(self.command(cmd, params)) + if data is not None: + status = 0 + for result in data: + if "status" in result: + status += 1 + if status == len(data): + _LOGGER.debug( + "Mikrotik %s - %s arp_ping timed out", ip_address, interface + ) + return False + return True + + def command(self, cmd, params=None): + """Retrieve data from Mikrotik API.""" + try: + _LOGGER.info("Running command %s", cmd) + if params: + response = self.api(cmd=cmd, **params) + else: + response = self.api(cmd=cmd) + except ( + librouteros.exceptions.ConnectionClosed, + socket.error, + socket.timeout, + ) as api_error: + _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) + raise CannotConnect + except librouteros.exceptions.ProtocolError as api_error: + _LOGGER.warning( + "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", + self._host, + cmd, + api_error, + ) + return None + + return response if response else None + + def update(self): + """Update device_tracker from Mikrotik API.""" + if not self.available or not self.api: + if not self.connect_to_hub(): + return + _LOGGER.debug("updating network devices for host: %s", self._host) + self.update_devices() + + +class MikrotikHub: + """Mikrotik Hub Object.""" + + def __init__(self, hass, config_entry): + """Initialize the Mikrotik Client.""" + self.hass = hass + self.config_entry = config_entry + self._mk_data = None + self.progress = None + + @property + def host(self): + """Return the host of this hub.""" + return self.config_entry.data[CONF_HOST] + + @property + def hostname(self): + """Return the hostname of the hub.""" + return self._mk_data.hostname + + @property + def model(self): + """Return the model of the hub.""" + return self._mk_data.model + + @property + def firmware(self): + """Return the firware of the hub.""" + return self._mk_data.firmware + + @property + def serial_num(self): + """Return the serial number of the hub.""" + return self._mk_data.serial_number + + @property + def available(self): + """Return if the hub is connected.""" + return self._mk_data.available + + @property + def option_detection_time(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) + + @property + def signal_update(self): + """Event specific per Mikrotik entry to signal updates.""" + return f"mikrotik-update-{self.host}" + + @property + def api(self): + """Represent Mikrotik data object.""" + return self._mk_data + + async def async_add_options(self): + """Populate default options for Mikrotik.""" + if not self.config_entry.options: + options = { + CONF_ARP_PING: self.config_entry.data.pop(CONF_ARP_PING, False), + CONF_FORCE_DHCP: self.config_entry.data.pop(CONF_FORCE_DHCP, False), + CONF_DETECTION_TIME: self.config_entry.data.pop( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + } + + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + + async def request_update(self): + """Request an update.""" + if self.progress is not None: + await self.progress + return + + self.progress = self.hass.async_create_task(self.async_update()) + await self.progress + + self.progress = None + + async def async_update(self): + """Update Mikrotik devices information.""" + await self.hass.async_add_executor_job(self._mk_data.update) + async_dispatcher_send(self.hass, self.signal_update) + + async def async_setup(self): + """Set up the Mikrotik hub.""" + try: + api = await self.hass.async_add_executor_job( + get_api, self.hass, self.config_entry.data + ) + except CannotConnect: + raise ConfigEntryNotReady + except LoginError: + return False + + self._mk_data = MikrotikData(self.hass, self.config_entry, api) + await self.async_add_options() + await self.hass.async_add_executor_job(self._mk_data.get_hub_details) + await self.hass.async_add_executor_job(self._mk_data.update) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "device_tracker" + ) + ) + return True + + +def get_api(hass, entry): + """Connect to Mikrotik hub.""" + _LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST]) + + _login_method = (login_plain, login_token) + kwargs = {"login_methods": _login_method, "port": entry["port"]} + + if entry[CONF_VERIFY_SSL]: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + _ssl_wrapper = ssl_context.wrap_socket + kwargs["ssl_wrapper"] = _ssl_wrapper + + try: + api = librouteros.connect( + entry[CONF_HOST], entry[CONF_USERNAME], entry[CONF_PASSWORD], **kwargs, + ) + _LOGGER.debug("Connected to %s successfully", entry[CONF_HOST]) + return api + except ( + librouteros.exceptions.LibRouterosError, + socket.error, + socket.timeout, + ) as api_error: + _LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], api_error) + if "invalid user name or password" in str(api_error): + raise LoginError + raise CannotConnect diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 1f5bcf8163f..72f98a11709 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,8 +1,13 @@ { "domain": "mikrotik", - "name": "MikroTik", + "name": "Mikrotik", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mikrotik", - "requirements": ["librouteros==2.3.0"], + "requirements": [ + "librouteros==3.0.0" + ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ] +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/strings.json b/homeassistant/components/mikrotik/strings.json new file mode 100644 index 00000000000..590563993d6 --- /dev/null +++ b/homeassistant/components/mikrotik/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Mikrotik", + "step": { + "user": { + "title": "Set up Mikrotik Router", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "verify_ssl": "Use ssl" + } + } + }, + "error": { + "name_exists": "Name exists", + "cannot_connect": "Connection Unsuccessful", + "wrong_credentials": "Wrong Credentials" + }, + "abort": { + "already_configured": "Mikrotik is already configured" + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Enable ARP ping", + "force_dhcp": "Force scanning using DHCP", + "detection_time": "Consider home interval" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 875d217247c..8f880c74c6e 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -173,7 +173,7 @@ class MillHeater(ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self._heater.is_gen1 or self._heater.power_status == 1: + if self._heater.is_gen1 or self._heater.is_heating == 1: return HVAC_MODE_HEAT return HVAC_MODE_OFF diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 56594f3e2c3..fcf95da586e 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,11 @@ """Integrates Native Apps to Home Assistant.""" -from homeassistant.components.webhook import async_register as webhook_register +import asyncio + +from homeassistant.components import cloud +from homeassistant.components.webhook import ( + async_register as webhook_register, + async_unregister as webhook_unregister, +) from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -10,6 +16,7 @@ from .const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + CONF_CLOUDHOOK_URL, DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS, @@ -20,9 +27,9 @@ from .const import ( STORAGE_KEY, STORAGE_VERSION, ) +from .helpers import savable_state from .http_api import RegistrationsView from .webhook import handle_webhook -from .websocket_api import register_websocket_handlers PLATFORMS = "sensor", "binary_sensor", "device_tracker" @@ -49,7 +56,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): } hass.http.register_view(RegistrationsView()) - register_websocket_handlers(hass) for deleted_id in hass.data[DOMAIN][DATA_DELETED_IDS]: try: @@ -96,3 +102,34 @@ async def async_setup_entry(hass, entry): ) return True + + +async def async_unload_entry(hass, entry): + """Unload a mobile app entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) + + return True + + +async def async_remove_entry(hass, entry): + """Cleanup when entry is removed.""" + hass.data[DOMAIN][DATA_DELETED_IDS].append(entry.data[CONF_WEBHOOK_ID]) + store = hass.data[DOMAIN][DATA_STORE] + await store.async_save(savable_state(hass)) + + if CONF_CLOUDHOOK_URL in entry.data: + try: + await cloud.async_delete_cloudhook(hass, entry.data[CONF_WEBHOOK_ID]) + except cloud.CloudNotAvailable: + pass diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py deleted file mode 100644 index a18e5247bfa..00000000000 --- a/homeassistant/components/mobile_app/websocket_api.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Websocket API for mobile_app.""" -import voluptuous as vol - -from homeassistant.components.websocket_api import ( - ActiveConnection, - async_register_command, - async_response, - error_message, - result_message, - websocket_command, - ws_require_user, -) -from homeassistant.components.websocket_api.const import ( - ERR_INVALID_FORMAT, - ERR_NOT_FOUND, - ERR_UNAUTHORIZED, -) -from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType - -from .const import ( - CONF_CLOUDHOOK_URL, - CONF_USER_ID, - DATA_CONFIG_ENTRIES, - DATA_DELETED_IDS, - DATA_STORE, - DOMAIN, -) -from .helpers import safe_registration, savable_state - - -def register_websocket_handlers(hass: HomeAssistantType) -> bool: - """Register the websocket handlers.""" - async_register_command(hass, websocket_get_user_registrations) - - async_register_command(hass, websocket_delete_registration) - - return True - - -@ws_require_user() -@async_response -@websocket_command( - { - vol.Required("type"): "mobile_app/get_user_registrations", - vol.Optional(CONF_USER_ID): cv.string, - } -) -async def websocket_get_user_registrations( - hass: HomeAssistantType, connection: ActiveConnection, msg: dict -) -> None: - """Return all registrations or just registrations for given user ID.""" - user_id = msg.get(CONF_USER_ID, connection.user.id) - - if user_id != connection.user.id and not connection.user.is_admin: - # If user ID is provided and is not current user ID and current user - # isn't an admin user - connection.send_error(msg["id"], ERR_UNAUTHORIZED, "Unauthorized") - return - - user_registrations = [] - - for config_entry in hass.config_entries.async_entries(domain=DOMAIN): - registration = config_entry.data - if connection.user.is_admin or registration[CONF_USER_ID] is user_id: - user_registrations.append(safe_registration(registration)) - - connection.send_message(result_message(msg["id"], user_registrations)) - - -@ws_require_user() -@async_response -@websocket_command( - { - vol.Required("type"): "mobile_app/delete_registration", - vol.Required(CONF_WEBHOOK_ID): cv.string, - } -) -async def websocket_delete_registration( - hass: HomeAssistantType, connection: ActiveConnection, msg: dict -) -> None: - """Delete the registration for the given webhook_id.""" - user = connection.user - - webhook_id = msg.get(CONF_WEBHOOK_ID) - if webhook_id is None: - connection.send_error(msg["id"], ERR_INVALID_FORMAT, "Webhook ID not provided") - return - - config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] - - registration = config_entry.data - - if registration is None: - connection.send_error( - msg["id"], ERR_NOT_FOUND, "Webhook ID not found in storage" - ) - return - - if registration[CONF_USER_ID] != user.id and not user.is_admin: - return error_message( - msg["id"], ERR_UNAUTHORIZED, "User is not registration owner" - ) - - await hass.config_entries.async_remove(config_entry.entry_id) - - hass.data[DOMAIN][DATA_DELETED_IDS].append(webhook_id) - - store = hass.data[DOMAIN][DATA_STORE] - - try: - await store.async_save(savable_state(hass)) - except HomeAssistantError: - return error_message(msg["id"], "internal_error", "Error deleting registration") - - if CONF_CLOUDHOOK_URL in registration: - await hass.components.cloud.async_delete_cloudhook(webhook_id) - - connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/mqtt/.translations/sv.json b/homeassistant/components/mqtt/.translations/sv.json index 70e3720038d..c54ae6e3e16 100644 --- a/homeassistant/components/mqtt/.translations/sv.json +++ b/homeassistant/components/mqtt/.translations/sv.json @@ -22,7 +22,7 @@ "data": { "discovery": "Aktivera uppt\u00e4ckt" }, - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till MQTT Broker som tillhandah\u00e5lls av hass.io-till\u00e4gget {addon} ?", + "description": "Vill du konfigurera Home Assistant att ansluta till den MQTT-broker som tillhandah\u00e5lls av Hass.io-till\u00e4gget \"{addon}\"?", "title": "MQTT Broker via Hass.io till\u00e4gg" } }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a9d5ac93ebc..f64c643f0f4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -36,7 +36,7 @@ from homeassistant.exceptions import ( HomeAssistantError, Unauthorized, ) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType @@ -68,6 +68,7 @@ DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_HASS_CONFIG = "mqtt_hass_config" SERVICE_PUBLISH = "publish" +SERVICE_DUMP = "dump" CONF_EMBEDDED = "embedded" @@ -651,7 +652,7 @@ async def async_setup_entry(hass, entry): if result == CONNECTION_FAILED_RECOVERABLE: raise ConfigEntryNotReady - async def async_stop_mqtt(event: Event): + async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" await hass.data[DATA_MQTT].async_disconnect() @@ -683,6 +684,40 @@ async def async_setup_entry(hass, entry): DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA ) + async def async_dump_service(call: ServiceCall): + """Handle MQTT dump service calls.""" + messages = [] + + @callback + def collect_msg(msg): + messages.append((msg.topic, msg.payload.replace("\n", ""))) + + unsub = await async_subscribe(hass, call.data["topic"], collect_msg) + + def write_dump(): + with open(hass.config.path("mqtt_dump.txt"), "wt") as fp: + for msg in messages: + fp.write(",".join(msg) + "\n") + + async def finish_dump(_): + """Write dump to file.""" + unsub() + await hass.async_add_executor_job(write_dump) + + event.async_call_later(hass, call.data["duration"], finish_dump) + + hass.services.async_register( + DOMAIN, + SERVICE_DUMP, + async_dump_service, + schema=vol.Schema( + { + vol.Required("topic"): valid_subscribe_topic, + vol.Optional("duration", default=5): int, + } + ), + ) + if conf.get(CONF_DISCOVERY): await _async_setup_discovery( hass, conf, hass.data[DATA_MQTT_HASS_CONFIG], entry @@ -774,27 +809,21 @@ class MQTT: async def async_publish( self, topic: str, payload: PublishPayloadType, qos: int, retain: bool ) -> None: - """Publish a MQTT message. - - This method must be run in the event loop and returns a coroutine. - """ + """Publish a MQTT message.""" async with self._paho_lock: _LOGGER.debug("Transmitting message on %s: %s", topic, payload) - await self.hass.async_add_job( + await self.hass.async_add_executor_job( self._mqttc.publish, topic, payload, qos, retain ) async def async_connect(self) -> str: - """Connect to the host. Does process messages yet. - - This method is a coroutine. - """ + """Connect to the host. Does process messages yet.""" # pylint: disable=import-outside-toplevel import paho.mqtt.client as mqtt result: int = None try: - result = await self.hass.async_add_job( + result = await self.hass.async_add_executor_job( self._mqttc.connect, self.broker, self.port, self.keepalive ) except OSError as err: @@ -808,19 +837,15 @@ class MQTT: self._mqttc.loop_start() return CONNECTION_SUCCESS - @callback - def async_disconnect(self): - """Stop the MQTT client. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_disconnect(self): + """Stop the MQTT client.""" def stop(): """Stop the MQTT client.""" self._mqttc.disconnect() self._mqttc.loop_stop() - return self.hass.async_add_job(stop) + await self.hass.async_add_executor_job(stop) async def async_subscribe( self, @@ -865,7 +890,9 @@ class MQTT: """ async with self._paho_lock: result: int = None - result, _ = await self.hass.async_add_job(self._mqttc.unsubscribe, topic) + result, _ = await self.hass.async_add_executor_job( + self._mqttc.unsubscribe, topic + ) _raise_on_error(result) async def _async_perform_subscription(self, topic: str, qos: int) -> None: @@ -874,7 +901,9 @@ class MQTT: async with self._paho_lock: result: int = None - result, _ = await self.hass.async_add_job(self._mqttc.subscribe, topic, qos) + result, _ = await self.hass.async_add_executor_job( + self._mqttc.subscribe, topic, qos + ) _raise_on_error(result) def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: @@ -1010,10 +1039,7 @@ class MqttAttributes(Entity): self._attributes_config = config async def async_added_to_hass(self) -> None: - """Subscribe MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe MQTT events.""" await super().async_added_to_hass() await self._attributes_subscribe_topics() @@ -1080,10 +1106,7 @@ class MqttAvailability(Entity): self._avail_config = config async def async_added_to_hass(self) -> None: - """Subscribe MQTT events. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe MQTT events.""" await super().async_added_to_hass() await self._availability_subscribe_topics() diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 831c47c3621..6cf0865ff6a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,6 +1,4 @@ """Camera that loads a picture from an MQTT topic.""" - -import asyncio import logging import voluptuous as vol @@ -130,8 +128,7 @@ class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): self.hass, self._sub_state ) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return image response.""" return self._last_image diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 135503f2333..07cb711ebd0 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -232,10 +232,11 @@ class MqttFan( self._supported_features = 0 self._supported_features |= ( - self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None and SUPPORT_OSCILLATE + self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None + and SUPPORT_OSCILLATE ) self._supported_features |= ( - self._topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED + self._topic[CONF_SPEED_COMMAND_TOPIC] is not None and SUPPORT_SET_SPEED ) async def _subscribe_topics(self): diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 3ed2fb71b14..61ba5c392b1 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -1,5 +1,4 @@ """Support for a local MQTT broker.""" -import asyncio import logging import tempfile @@ -29,8 +28,7 @@ HBMQTT_CONFIG_SCHEMA = vol.Any( ) -@asyncio.coroutine -def async_start(hass, password, server_config): +async def async_start(hass, password, server_config): """Initialize MQTT Server. This method is a coroutine. @@ -47,17 +45,16 @@ def async_start(hass, password, server_config): server_config = gen_server_config broker = Broker(server_config, hass.loop) - yield from broker.start() + await broker.start() except BrokerException: _LOGGER.exception("Error initializing MQTT server") return False, None finally: passwd.close() - @asyncio.coroutine - def async_shutdown_mqtt_server(event): + async def async_shutdown_mqtt_server(event): """Shut down the MQTT server.""" - yield from broker.shutdown() + await broker.shutdown() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_mqtt_server) diff --git a/homeassistant/components/mqtt/services.yaml b/homeassistant/components/mqtt/services.yaml index e338e21802a..77b3e3b27a1 100644 --- a/homeassistant/components/mqtt/services.yaml +++ b/homeassistant/components/mqtt/services.yaml @@ -24,3 +24,14 @@ publish: description: If message should have the retain flag set. example: true default: false + +dump: + description: Dump messages on a topic selector to the 'mqtt_dump.txt' file in your config folder. + fields: + topic: + description: topic to listen to + example: "openzwave/#" + duration: + description: how long we should listen for messages in seconds + example: 5 + default: 5 diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index ff0063a380e..3da77d6d943 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -4,6 +4,7 @@ import logging from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY +from homeassistant.core import callback _LOGGER = logging.getLogger(__name__) @@ -81,6 +82,7 @@ class MyStromBinarySensor(BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state + @callback def async_on_update(self, value): """Receive an update.""" self._state = value diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index f60835b1146..dc6e8d0d8d4 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -20,11 +20,6 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) ATTR_GENERATED_AT = "generated_at" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Neato Camera.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato camera with config entry.""" dev = [] diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index fd5d8036f5f..70d273fe690 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -16,11 +16,6 @@ SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) BATTERY = "Battery" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Neato sensor.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Neato sensor using config entry.""" dev = [] diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 6aa0e11a43e..54149630ff2 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -18,11 +18,6 @@ SWITCH_TYPE_SCHEDULE = "schedule" SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Neato switches.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato switch with config entry.""" dev = [] diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index d8a3e4ded45..adff293301b 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -84,11 +84,6 @@ SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Neato vacuum.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato vacuum with config entry.""" dev = [] @@ -208,6 +203,13 @@ class NeatoConnectedVacuum(StateVacuumDevice): + " " + ACTION.get(self._state["action"]) ) + if ( + "boundary" in self._state["cleaning"] + and "name" in self._state["cleaning"]["boundary"] + ): + self._status_state += ( + " " + self._state["cleaning"]["boundary"]["name"] + ) else: self._status_state = robot_alert elif self._state["state"] == 3: diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 322452f5f59..8718843d73d 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -2,7 +2,7 @@ "domain": "nederlandse_spoorwegen", "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": ["nsapi==2.7.4"], + "requirements": ["nsapi==3.0.2"], "dependencies": [], - "codeowners": [] + "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 0b823962373..df37fad2aa3 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_EMAIL, CONF_NAME, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -37,25 +37,21 @@ ROUTE_SCHEMA = vol.Schema( ROUTES_SCHEMA = vol.All(cv.ensure_list, [ROUTE_SCHEMA]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_EMAIL): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_ROUTES): ROUTES_SCHEMA, - } + {vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ROUTES): ROUTES_SCHEMA} ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - nsapi = ns_api.NSAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) + nsapi = ns_api.NSAPI(config[CONF_API_KEY]) try: stations = nsapi.get_stations() except ( requests.exceptions.ConnectionError, requests.exceptions.HTTPError, ) as error: - _LOGGER.error("Couldn't fetch stations, API password correct?: %s", error) + _LOGGER.error("Couldn't fetch stations, API key correct?: %s", error) return sensors = [] @@ -128,40 +124,76 @@ class NSDepartureSensor(Entity): for k in self._trips[0].trip_parts: route.append(k.destination) - return { + # Static attributes + attributes = { "going": self._trips[0].going, - "departure_time_planned": self._trips[0].departure_time_planned.strftime( - "%H:%M" - ), - "departure_time_actual": self._trips[0].departure_time_actual.strftime( - "%H:%M" - ), - "departure_delay": self._trips[0].departure_time_planned - != self._trips[0].departure_time_actual, - "departure_platform": self._trips[0].trip_parts[0].stops[0].platform, - "departure_platform_changed": self._trips[0] - .trip_parts[0] - .stops[0] - .platform_changed, - "arrival_time_planned": self._trips[0].arrival_time_planned.strftime( - "%H:%M" - ), - "arrival_time_actual": self._trips[0].arrival_time_actual.strftime("%H:%M"), - "arrival_delay": self._trips[0].arrival_time_planned - != self._trips[0].arrival_time_actual, - "arrival_platform": self._trips[0].trip_parts[0].stops[-1].platform, - "arrival_platform_changed": self._trips[0] - .trip_parts[0] - .stops[-1] - .platform_changed, - "next": self._trips[1].departure_time_actual.strftime("%H:%M"), + "departure_time_planned": None, + "departure_time_actual": None, + "departure_delay": False, + "departure_platform_planned": self._trips[0].departure_platform_planned, + "departure_platform_actual": self._trips[0].departure_platform_actual, + "arrival_time_planned": None, + "arrival_time_actual": None, + "arrival_delay": False, + "arrival_platform_planned": self._trips[0].arrival_platform_planned, + "arrival_platform_actual": self._trips[0].arrival_platform_actual, + "next": None, "status": self._trips[0].status.lower(), "transfers": self._trips[0].nr_transfers, "route": route, - "remarks": [r.message for r in self._trips[0].trip_remarks], + "remarks": None, ATTR_ATTRIBUTION: ATTRIBUTION, } + # Planned departure attributes + if self._trips[0].departure_time_planned is not None: + attributes["departure_time_planned"] = self._trips[ + 0 + ].departure_time_planned.strftime("%H:%M") + + # Actual departure attributes + if self._trips[0].departure_time_actual is not None: + attributes["departure_time_actual"] = self._trips[ + 0 + ].departure_time_actual.strftime("%H:%M") + + # Delay departure attributes + if ( + attributes["departure_time_planned"] + and attributes["departure_time_actual"] + and attributes["departure_time_planned"] + != attributes["departure_time_actual"] + ): + attributes["departure_delay"] = True + + # Planned arrival attributes + if self._trips[0].arrival_time_planned is not None: + attributes["arrival_time_planned"] = self._trips[ + 0 + ].arrival_time_planned.strftime("%H:%M") + + # Actual arrival attributes + if self._trips[0].arrival_time_actual is not None: + attributes["arrival_time_actual"] = self._trips[ + 0 + ].arrival_time_actual.strftime("%H:%M") + + # Delay arrival attributes + if ( + attributes["arrival_time_planned"] + and attributes["arrival_time_actual"] + and attributes["arrival_time_planned"] != attributes["arrival_time_actual"] + ): + attributes["arrival_delay"] = True + + # Next attributes + if self._trips[1].departure_time_actual is not None: + attributes["next"] = self._trips[1].departure_time_actual.strftime("%H:%M") + elif self._trips[1].departure_time_planned is not None: + attributes["next"] = self._trips[1].departure_time_planned.strftime("%H:%M") + + return attributes + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the trip information.""" @@ -173,10 +205,15 @@ class NSDepartureSensor(Entity): self._heading, True, 0, + 2, ) if self._trips: - actual_time = self._trips[0].departure_time_actual - self._state = actual_time.strftime("%H:%M") + if self._trips[0].departure_time_actual is None: + planned_time = self._trips[0].departure_time_planned + self._state = planned_time.strftime("%H:%M") + else: + actual_time = self._trips[0].departure_time_actual + self._state = actual_time.strftime("%H:%M") except ( requests.exceptions.ConnectionError, requests.exceptions.HTTPError, diff --git a/homeassistant/components/netatmo/.translations/ca.json b/homeassistant/components/netatmo/.translations/ca.json new file mode 100644 index 00000000000..6961db6f520 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Netatmo.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/cs.json b/homeassistant/components/netatmo/.translations/cs.json new file mode 100644 index 00000000000..bab99c32124 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/da.json b/homeassistant/components/netatmo/.translations/da.json new file mode 100644 index 00000000000..8fec2890881 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere \u00e9n Netatmo-konto.", + "authorize_url_timeout": "Timeout ved generering af godkendelses-url.", + "missing_configuration": "Netatmo-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + }, + "create_entry": { + "default": "Korrekt godkendt med Netatmo." + }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/de.json b/homeassistant/components/netatmo/.translations/de.json new file mode 100644 index 00000000000..57e717429c4 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Netatmo-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Netatmo-Komponente ist nicht konfiguriert. Folge bitte der Dokumentation." + }, + "create_entry": { + "default": "Erfolgreich mit Netatmo authentifiziert." + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/en.json b/homeassistant/components/netatmo/.translations/en.json new file mode 100644 index 00000000000..9d69a3ece50 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Netatmo account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/es.json b/homeassistant/components/netatmo/.translations/es.json new file mode 100644 index 00000000000..7e39574d492 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta de Netatmo.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Netatmo no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/fr.json b/homeassistant/components/netatmo/.translations/fr.json new file mode 100644 index 00000000000..23f0bca1087 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Netatmo.", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", + "missing_configuration": "Le composant Netatmo n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie avec Netatmo." + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/it.json b/homeassistant/components/netatmo/.translations/it.json new file mode 100644 index 00000000000..f3e3dafcba4 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Netatmo.", + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "missing_configuration": "Il componente Netatmo non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/ko.json b/homeassistant/components/netatmo/.translations/ko.json new file mode 100644 index 00000000000..e360c16d69c --- /dev/null +++ b/homeassistant/components/netatmo/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Netatmo \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Netatmo \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "Netatmo \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/lb.json b/homeassistant/components/netatmo/.translations/lb.json new file mode 100644 index 00000000000..b7e3a18bdae --- /dev/null +++ b/homeassistant/components/netatmo/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Netatmo Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "Netatmo Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Netatmo authentifiz\u00e9iert." + }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/nl.json b/homeassistant/components/netatmo/.translations/nl.json new file mode 100644 index 00000000000..d9062850f2a --- /dev/null +++ b/homeassistant/components/netatmo/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Netatmo-component is niet geconfigureerd. Gelieve de documentatie volgen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd met Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/no.json b/homeassistant/components/netatmo/.translations/no.json new file mode 100644 index 00000000000..68a91633642 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Netatmo-konto.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Netatmo-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket autentisering med Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/pl.json b/homeassistant/components/netatmo/.translations/pl.json new file mode 100644 index 00000000000..35da44a9680 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Netatmo.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "missing_configuration": "Komponent Netatmo nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/ru.json b/homeassistant/components/netatmo/.translations/ru.json new file mode 100644 index 00000000000..c34fb331ceb --- /dev/null +++ b/homeassistant/components/netatmo/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Netatmo \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/sv.json b/homeassistant/components/netatmo/.translations/sv.json new file mode 100644 index 00000000000..29943f5e538 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Netatmo-konto." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/zh-Hant.json b/homeassistant/components/netatmo/.translations/zh-Hant.json new file mode 100644 index 00000000000..24124e6fb35 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Netatmo \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Netatmo \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Netatmo \u8a2d\u5099\u3002" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, + "title": "Netatmo" + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 6becedde611..ace12d3838c 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,286 +1,86 @@ -"""Support for the Netatmo devices.""" -from datetime import timedelta +"""The Netatmo integration.""" +import asyncio import logging -from urllib.error import HTTPError -import pyatmo import voluptuous as vol -from homeassistant.const import ( - CONF_API_KEY, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_URL, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv -from .const import DATA_NETATMO_AUTH, DOMAIN +from . import api, config_flow +from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN _LOGGER = logging.getLogger(__name__) -DATA_PERSONS = "netatmo_persons" -DATA_WEBHOOK_URL = "netatmo_webhook_url" - -CONF_SECRET_KEY = "secret_key" -CONF_WEBHOOKS = "webhooks" - -SERVICE_ADDWEBHOOK = "addwebhook" -SERVICE_DROPWEBHOOK = "dropwebhook" -SERVICE_SETSCHEDULE = "set_schedule" - -NETATMO_AUTH = None -NETATMO_WEBHOOK_URL = None - -DEFAULT_PERSON = "Unknown" -DEFAULT_DISCOVERY = True -DEFAULT_WEBHOOKS = False - -EVENT_PERSON = "person" -EVENT_MOVEMENT = "movement" -EVENT_HUMAN = "human" -EVENT_ANIMAL = "animal" -EVENT_VEHICLE = "vehicle" - -EVENT_BUS_PERSON = "netatmo_person" -EVENT_BUS_MOVEMENT = "netatmo_movement" -EVENT_BUS_HUMAN = "netatmo_human" -EVENT_BUS_ANIMAL = "netatmo_animal" -EVENT_BUS_VEHICLE = "netatmo_vehicle" -EVENT_BUS_OTHER = "netatmo_other" - -ATTR_ID = "id" -ATTR_PSEUDO = "pseudo" -ATTR_NAME = "name" -ATTR_EVENT_TYPE = "event_type" -ATTR_MESSAGE = "message" -ATTR_CAMERA_ID = "camera_id" -ATTR_HOME_NAME = "home_name" -ATTR_PERSONS = "persons" -ATTR_IS_KNOWN = "is_known" -ATTR_FACE_URL = "face_url" -ATTR_SNAPSHOT_URL = "snapshot_url" -ATTR_VIGNETTE_URL = "vignette_url" -ATTR_SCHEDULE = "schedule" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_SECRET_KEY): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) -SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string}) - -SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) - -SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) +PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"] -def setup(hass, config): - """Set up the Netatmo devices.""" +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Netatmo component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_PERSONS] = {} - hass.data[DATA_PERSONS] = {} - try: - auth = pyatmo.ClientAuth( - config[DOMAIN][CONF_API_KEY], - config[DOMAIN][CONF_SECRET_KEY], - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - "read_station read_camera access_camera " - "read_thermostat write_thermostat " - "read_presence access_presence read_homecoach", - ) - except HTTPError: - _LOGGER.error("Unable to connect to Netatmo API") - return False + if DOMAIN not in config: + return True - try: - home_data = pyatmo.HomeData(auth) - except pyatmo.NoDevice: - home_data = None - _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) - - # Store config to be used during entry setup - hass.data[DATA_NETATMO_AUTH] = auth - - if config[DOMAIN][CONF_DISCOVERY]: - for component in "camera", "sensor", "binary_sensor", "climate": - discovery.load_platform(hass, component, DOMAIN, {}, config) - - if config[DOMAIN][CONF_WEBHOOKS]: - webhook_id = hass.components.webhook.async_generate_id() - hass.data[DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url( - webhook_id - ) - hass.components.webhook.async_register( - DOMAIN, "Netatmo", webhook_id, handle_webhook - ) - auth.addwebhook(hass.data[DATA_WEBHOOK_URL]) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dropwebhook) - - def _service_addwebhook(service): - """Service to (re)add webhooks during runtime.""" - url = service.data.get(CONF_URL) - if url is None: - url = hass.data[DATA_WEBHOOK_URL] - _LOGGER.info("Adding webhook for URL: %s", url) - auth.addwebhook(url) - - hass.services.register( - DOMAIN, - SERVICE_ADDWEBHOOK, - _service_addwebhook, - schema=SCHEMA_SERVICE_ADDWEBHOOK, - ) - - def _service_dropwebhook(service): - """Service to drop webhooks during runtime.""" - _LOGGER.info("Dropping webhook") - auth.dropwebhook() - - hass.services.register( - DOMAIN, - SERVICE_DROPWEBHOOK, - _service_dropwebhook, - schema=SCHEMA_SERVICE_DROPWEBHOOK, - ) - - def _service_setschedule(service): - """Service to change current home schedule.""" - schedule_name = service.data.get(ATTR_SCHEDULE) - home_data.switchHomeSchedule(schedule=schedule_name) - _LOGGER.info("Set home schedule to %s", schedule_name) - - if home_data is not None: - hass.services.register( + config_flow.NetatmoFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, DOMAIN, - SERVICE_SETSCHEDULE, - _service_setschedule, - schema=SCHEMA_SERVICE_SETSCHEDULE, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Netatmo from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + hass.data[DOMAIN][entry.entry_id] = { + AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) ) return True -def dropwebhook(hass): - """Drop the webhook subscription.""" - auth = hass.data[DATA_NETATMO_AUTH] - auth.dropwebhook() - - -async def handle_webhook(hass, webhook_id, request): - """Handle webhook callback.""" - try: - data = await request.json() - except ValueError: - return None - - _LOGGER.debug("Got webhook data: %s", data) - published_data = { - ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE), - ATTR_HOME_NAME: data.get(ATTR_HOME_NAME), - ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID), - ATTR_MESSAGE: data.get(ATTR_MESSAGE), - } - if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: - for person in data[ATTR_PERSONS]: - published_data[ATTR_ID] = person.get(ATTR_ID) - published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get( - published_data[ATTR_ID], DEFAULT_PERSON - ) - published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) - published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - hass.bus.async_fire(EVENT_BUS_PERSON, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_HUMAN, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE: - hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data) - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - else: - hass.bus.async_fire(EVENT_BUS_OTHER, data) - - -class CameraData: - """Get the latest data from Netatmo.""" - - def __init__(self, hass, auth, home=None): - """Initialize the data object.""" - self._hass = hass - self.auth = auth - self.camera_data = None - self.camera_names = [] - self.module_names = [] - self.home = home - self.camera_type = None - - def get_camera_names(self): - """Return all camera available on the API as a list.""" - self.camera_names = [] - self.update() - if not self.home: - for home in self.camera_data.cameras: - for camera in self.camera_data.cameras[home].values(): - self.camera_names.append(camera["name"]) - else: - for camera in self.camera_data.cameras[self.home].values(): - self.camera_names.append(camera["name"]) - return self.camera_names - - def get_module_names(self, camera_name): - """Return all module available on the API as a list.""" - self.module_names = [] - self.update() - cam_id = self.camera_data.cameraByName(camera=camera_name, home=self.home)["id"] - for module in self.camera_data.modules.values(): - if cam_id == module["cam_id"]: - self.module_names.append(module["name"]) - return self.module_names - - def get_camera_type(self, camera=None, home=None, cid=None): - """Return camera type for a camera, cid has preference over camera.""" - self.camera_type = self.camera_data.cameraType( - camera=camera, home=home, cid=cid +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] ) - return self.camera_type + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) - def get_persons(self): - """Gather person data for webhooks.""" - for person_id, person_data in self.camera_data.persons.items(): - self._hass.data[DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Call the Netatmo API to update the data.""" - self.camera_data = pyatmo.CameraData(self.auth, size=100) - - @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) - def update_event(self): - """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) + return unload_ok diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py new file mode 100644 index 00000000000..9a34888fd72 --- /dev/null +++ b/homeassistant/components/netatmo/api.py @@ -0,0 +1,35 @@ +"""API for Netatmo bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe +import logging + +import pyatmo + +from homeassistant import config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmOAuth2): + """Provide Netatmo authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Netatmo Auth.""" + self.hass = hass + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(token=self.session.token) + + def refresh_tokens(self,) -> dict: + """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index a449b7bb43d..6d0de6dcceb 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,15 +1,12 @@ """Support for the Netatmo binary sensors.""" import logging -from pyatmo import NoDevice -import voluptuous as vol +import pyatmo -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_TIMEOUT -from homeassistant.helpers import config_validation as cv +from homeassistant.components.binary_sensor import BinarySensorDevice -from . import CameraData -from .const import DATA_NETATMO_AUTH +from .camera import CameraData +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -27,6 +24,8 @@ PRESENCE_SENSOR_TYPES = { } TAG_SENSOR_TYPES = {"Tag Vibration": "vibration", "Tag Open": "opening"} +SENSOR_TYPES = {"NACamera": WELCOME_SENSOR_TYPES, "NOC": PRESENCE_SENSOR_TYPES} + CONF_HOME = "home" CONF_CAMERAS = "cameras" CONF_WELCOME_SENSORS = "welcome_sensors" @@ -35,130 +34,75 @@ CONF_TAG_SENSORS = "tag_sensors" DEFAULT_TIMEOUT = 90 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_HOME): cv.string, - vol.Optional( - CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES) - ): vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the access to Netatmo binary sensor.""" - home = config.get(CONF_HOME) - timeout = config.get(CONF_TIMEOUT) - if timeout is None: - timeout = DEFAULT_TIMEOUT + auth = hass.data[DOMAIN][entry.entry_id][AUTH] - module_name = None + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] - auth = hass.data[DATA_NETATMO_AUTH] - - try: - data = CameraData(hass, auth, home) - if not data.get_camera_names(): + def get_camera_home_id(data, camera_id): + """Return the home id for a given camera id.""" + for home_id in data.camera_data.cameras: + for camera in data.camera_data.cameras[home_id].values(): + if camera["id"] == camera_id: + return home_id return None - except NoDevice: - return None - welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) - presence_sensors = config.get(CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) - tag_sensors = config.get(CONF_TAG_SENSORS, TAG_SENSOR_TYPES) + try: + data = CameraData(hass, auth) - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if camera_type == "NACamera": - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - for variable in welcome_sensors: - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, - ) - if camera_type == "NOC": - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - for variable in presence_sensors: - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, - ) + for camera in data.get_all_cameras(): + home_id = get_camera_home_id(data, camera_id=camera["id"]) - for module_name in data.get_module_names(camera_name): - for variable in tag_sensors: - camera_type = None - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, - ) + sensor_types = {} + sensor_types.update(SENSOR_TYPES[camera["type"]]) + + # Tags are only supported with Netatmo Welcome indoor cameras + if camera["type"] == "NACamera" and data.get_modules(camera["id"]): + sensor_types.update(TAG_SENSOR_TYPES) + + for sensor_name in sensor_types: + entities.append( + NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) + ) + except pyatmo.NoDevice: + _LOGGER.debug("No camera entities to add") + + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) class NetatmoBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Camera device.""" - def __init__( - self, data, camera_name, module_name, home, timeout, camera_type, sensor - ): + def __init__(self, data, camera_id, home_id, sensor_type, module_id=None): """Set up for access to the Netatmo camera events.""" self._data = data - self._camera_name = camera_name - self._module_name = module_name - self._home = home - self._timeout = timeout - if home: - self._name = f"{home} / {camera_name}" + self._camera_id = camera_id + self._module_id = module_id + self._sensor_type = sensor_type + camera_info = data.camera_data.cameraById(cid=camera_id) + self._camera_name = camera_info["name"] + self._camera_type = camera_info["type"] + self._home_id = home_id + self._home_name = self._data.camera_data.getHomeName(home_id=home_id) + self._timeout = DEFAULT_TIMEOUT + if module_id: + self._module_name = data.camera_data.moduleById(mid=module_id)["name"] + self._name = ( + f"{MANUFACTURER} {self._camera_name} {self._module_name} {sensor_type}" + ) + self._unique_id = ( + f"{self._camera_id}-{self._module_id}-" + f"{self._camera_type}-{sensor_type}" + ) else: - self._name = camera_name - if module_name: - self._name += f" / {module_name}" - self._sensor_name = sensor - self._name += f" {sensor}" - self._cameratype = camera_type + self._name = f"{MANUFACTURER} {self._camera_name} {sensor_type}" + self._unique_id = f"{self._camera_id}-{self._camera_type}-{sensor_type}" self._state = None @property @@ -167,13 +111,19 @@ class NetatmoBinarySensor(BinarySensorDevice): return self._name @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self._cameratype == "NACamera": - return WELCOME_SENSOR_TYPES.get(self._sensor_name) - if self._cameratype == "NOC": - return PRESENCE_SENSOR_TYPES.get(self._sensor_name) - return TAG_SENSOR_TYPES.get(self._sensor_name) + def unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._camera_id)}, + "name": self._camera_name, + "manufacturer": MANUFACTURER, + "model": self._camera_type, + } @property def is_on(self): @@ -183,43 +133,43 @@ class NetatmoBinarySensor(BinarySensorDevice): def update(self): """Request an update from the Netatmo API.""" self._data.update() - self._data.update_event() + self._data.update_event(camera_type=self._camera_type) - if self._cameratype == "NACamera": - if self._sensor_name == "Someone known": - self._state = self._data.camera_data.someoneKnownSeen( - self._home, self._camera_name, self._timeout + if self._camera_type == "NACamera": + if self._sensor_type == "Someone known": + self._state = self._data.camera_data.someone_known_seen( + cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Someone unknown": - self._state = self._data.camera_data.someoneUnknownSeen( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Someone unknown": + self._state = self._data.camera_data.someone_unknown_seen( + cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Motion": - self._state = self._data.camera_data.motionDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Motion": + self._state = self._data.camera_data.motion_detected( + cid=self._camera_id, exclude=self._timeout ) - elif self._cameratype == "NOC": - if self._sensor_name == "Outdoor motion": - self._state = self._data.camera_data.outdoormotionDetected( - self._home, self._camera_name, self._timeout + elif self._camera_type == "NOC": + if self._sensor_type == "Outdoor motion": + self._state = self._data.camera_data.outdoor_motion_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor human": - self._state = self._data.camera_data.humanDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor human": + self._state = self._data.camera_data.human_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor animal": - self._state = self._data.camera_data.animalDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor animal": + self._state = self._data.camera_data.animal_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor vehicle": - self._state = self._data.camera_data.carDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor vehicle": + self._state = self._data.camera_data.car_detected( + cid=self._camera_id, offset=self._timeout ) - if self._sensor_name == "Tag Vibration": - self._state = self._data.camera_data.moduleMotionDetected( - self._home, self._module_name, self._camera_name, self._timeout + if self._sensor_type == "Tag Vibration": + self._state = self._data.camera_data.module_motion_detected( + mid=self._module_id, cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Tag Open": - self._state = self._data.camera_data.moduleOpened( - self._home, self._module_name, self._camera_name, self._timeout + elif self._sensor_type == "Tag Open": + self._state = self._data.camera_data.module_opened( + mid=self._module_id, cid=self._camera_id, exclude=self._timeout ) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 546a5da3c15..08a3847c0b7 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,25 +1,28 @@ """Support for the Netatmo cameras.""" import logging -from pyatmo import NoDevice +import pyatmo import requests import voluptuous as vol from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - PLATFORM_SCHEMA, + DOMAIN as CAMERA_DOMAIN, SUPPORT_STREAM, Camera, ) -from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle -from . import CameraData -from .const import DATA_NETATMO_AUTH, DOMAIN +from .const import ( + ATTR_PSEUDO, + AUTH, + DATA_PERSONS, + DOMAIN, + MANUFACTURER, + MIN_TIME_BETWEEN_EVENT_UPDATES, + MIN_TIME_BETWEEN_UPDATES, +) _LOGGER = logging.getLogger(__name__) @@ -31,96 +34,61 @@ DEFAULT_QUALITY = "high" VALID_QUALITIES = ["high", "medium", "low", "poor"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_HOME): cv.string, - vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_QUALITY, default=DEFAULT_QUALITY): vol.All( - cv.string, vol.In(VALID_QUALITIES) - ), - } -) - _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} +SCHEMA_SERVICE_SETLIGHTAUTO = vol.Schema( + {vol.Optional(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN)} +) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up access to Netatmo cameras.""" - home = config.get(CONF_HOME) - verify_ssl = config.get(CONF_VERIFY_SSL, True) - quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) - auth = hass.data[DATA_NETATMO_AUTH] +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Netatmo camera platform.""" - try: - data = CameraData(hass, auth, home) - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - add_entities( - [ + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] + try: + camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) + for camera in camera_data.get_all_cameras(): + _LOGGER.debug("Setting up camera %s %s", camera["id"], camera["name"]) + entities.append( NetatmoCamera( - data, camera_name, home, camera_type, verify_ssl, quality + camera_data, camera["id"], camera["type"], True, DEFAULT_QUALITY ) - ] - ) - data.get_persons() - except NoDevice: - return None + ) + camera_data.update_persons() + except pyatmo.NoDevice: + _LOGGER.debug("No cameras found") + return entities - async def async_service_handler(call): - """Handle service call.""" - _LOGGER.debug( - "Service handler invoked with service=%s and data=%s", - call.service, - call.data, - ) - service = call.service - entity_id = call.data["entity_id"][0] - async_dispatcher_send(hass, f"{service}_{entity_id}") + async_add_entities(await hass.async_add_executor_job(get_entities), True) - hass.services.async_register( - DOMAIN, "set_light_auto", async_service_handler, CAMERA_SERVICE_SCHEMA - ) - hass.services.async_register( - DOMAIN, "set_light_on", async_service_handler, CAMERA_SERVICE_SCHEMA - ) - hass.services.async_register( - DOMAIN, "set_light_off", async_service_handler, CAMERA_SERVICE_SCHEMA - ) + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Netatmo camera platform.""" + return class NetatmoCamera(Camera): - """Representation of the images published from a Netatmo camera.""" + """Representation of a Netatmo camera.""" - def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): + def __init__(self, data, camera_id, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" super().__init__() self._data = data - self._camera_name = camera_name - self._home = home - if home: - self._name = f"{home} / {camera_name}" - else: - self._name = camera_name - self._cameratype = camera_type + self._camera_id = camera_id + self._camera_name = self._data.camera_data.get_camera(cid=camera_id).get("name") + self._name = f"{MANUFACTURER} {self._camera_name}" + self._camera_type = camera_type + self._unique_id = f"{self._camera_id}-{self._camera_type}" self._verify_ssl = verify_ssl self._quality = quality - # URLs. + # URLs self._vpnurl = None self._localurl = None - # Identifier - self._id = None - - # Monitoring status. + # Monitoring status self._status = None # SD Card status @@ -132,12 +100,6 @@ class NetatmoCamera(Camera): # Is local self._is_local = None - # VPN URL - self._vpn_url = None - - # Light mode status - self._light_mode_status = None - def camera_image(self): """Return a still image response from the camera.""" try: @@ -152,23 +114,21 @@ class NetatmoCamera(Camera): verify=self._verify_ssl, ) else: - _LOGGER.error("Welcome VPN URL is None") + _LOGGER.error("Welcome/Presence VPN URL is None") self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( + cid=self._camera_id ) return None except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome URL changed: %s", error) + _LOGGER.info("Welcome/Presence URL changed: %s", error) self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( + cid=self._camera_id ) return None return response.content - # Entity property overrides - @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -182,24 +142,26 @@ class NetatmoCamera(Camera): """Return the name of this Netatmo camera device.""" return self._name + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._camera_id)}, + "name": self._camera_name, + "manufacturer": MANUFACTURER, + "model": self._camera_type, + } + @property def device_state_attributes(self): """Return the Netatmo-specific camera state attributes.""" - - _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) - attr = {} - attr["id"] = self._id + attr["id"] = self._camera_id attr["status"] = self._status attr["sd_status"] = self._sd_status attr["alim_status"] = self._alim_status attr["is_local"] = self._is_local - attr["vpn_url"] = self._vpn_url - - if self.model == "Presence": - attr["light_mode_status"] = self._light_mode_status - - _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + attr["vpn_url"] = self._vpnurl return attr @@ -221,7 +183,7 @@ class NetatmoCamera(Camera): @property def brand(self): """Return the camera brand.""" - return "Netatmo" + return MANUFACTURER @property def motion_detection_enabled(self): @@ -243,173 +205,84 @@ class NetatmoCamera(Camera): @property def model(self): """Return the camera model.""" - if self._cameratype == "NOC": + if self._camera_type == "NOC": return "Presence" - if self._cameratype == "NACamera": + if self._camera_type == "NACamera": return "Welcome" return None - # Other Entity method overrides - - async def async_added_to_hass(self): - """Subscribe to signals and add camera to list.""" - _LOGGER.debug("Registering services for entity_id=%s", self.entity_id) - async_dispatcher_connect( - self.hass, f"set_light_auto_{self.entity_id}", self.set_light_auto - ) - async_dispatcher_connect( - self.hass, f"set_light_on_{self.entity_id}", self.set_light_on - ) - async_dispatcher_connect( - self.hass, f"set_light_off_{self.entity_id}", self.set_light_off - ) + @property + def unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id def update(self): """Update entity status.""" - _LOGGER.debug("Updating camera netatmo '%s'", self._name) - - # Refresh camera data. + # Refresh camera data self._data.update() - # URLs. - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=self._camera_name + camera = self._data.camera_data.get_camera(cid=self._camera_id) + + # URLs + self._vpnurl, self._localurl = self._data.camera_data.camera_urls( + cid=self._camera_id ) - # Identifier - self._id = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["id"] - - # Monitoring status. - self._status = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["status"] - - _LOGGER.debug("Status of '%s' = %s", self._name, self._status) + # Monitoring status + self._status = camera.get("status") # SD Card status - self._sd_status = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["sd_status"] + self._sd_status = camera.get("sd_status") # Power status - self._alim_status = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["alim_status"] + self._alim_status = camera.get("alim_status") # Is local - self._is_local = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["is_local"] - - # VPN URL - self._vpn_url = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["vpn_url"] + self._is_local = camera.get("is_local") self.is_streaming = self._alim_status == "on" - if self.model == "Presence": - # Light mode status - self._light_mode_status = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["light_mode_status"] - # Camera method overrides +class CameraData: + """Get the latest data from Netatmo.""" - def enable_motion_detection(self): - """Enable motion detection in the camera.""" - _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) - self._enable_motion_detection(True) + def __init__(self, hass, auth): + """Initialize the data object.""" + self._hass = hass + self.auth = auth + self.camera_data = None - def disable_motion_detection(self): - """Disable motion detection in camera.""" - _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) - self._enable_motion_detection(False) + def get_all_cameras(self): + """Return all camera available on the API as a list.""" + self.update() + cameras = [] + for camera in self.camera_data.cameras.values(): + cameras.extend(camera.values()) + return cameras - def _enable_motion_detection(self, enable): - """Enable or disable motion detection.""" - try: - if self._localurl: - requests.get( - f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", - timeout=10, - ) - elif self._vpnurl: - requests.get( - f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", - timeout=10, - verify=self._verify_ssl, - ) - else: - _LOGGER.error("Welcome/Presence VPN URL is None") - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name - ) - return None - except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome/Presence URL changed: %s", error) - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + def get_modules(self, camera_id): + """Return all modules for a given camera.""" + return self.camera_data.get_camera(camera_id).get("modules", []) + + def get_camera_type(self, camera_id): + """Return camera type for a camera, cid has preference over camera.""" + return self.camera_data.cameraType(cid=camera_id) + + def update_persons(self): + """Gather person data for webhooks.""" + for person_id, person_data in self.camera_data.persons.items(): + self._hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get( + ATTR_PSEUDO ) - return None - else: - self.async_schedule_update_ha_state(True) - # Netatmo Presence specific camera method. + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Call the Netatmo API to update the data.""" + self.camera_data = pyatmo.CameraData(self.auth, size=100) + self.update_persons() - def set_light_auto(self): - """Set flood light in automatic mode.""" - _LOGGER.debug( - "Set the flood light in automatic mode for the camera '%s'", self._name - ) - self._set_light_mode("auto") - - def set_light_on(self): - """Set flood light on.""" - _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) - self._set_light_mode("on") - - def set_light_off(self): - """Set flood light off.""" - _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) - self._set_light_mode("off") - - def _set_light_mode(self, mode): - """Set light mode ('auto', 'on', 'off').""" - if self.model == "Presence": - try: - config = f'{{"mode":"{mode}"}}' - if self._localurl: - requests.get( - f"{self._localurl}/command/floodlight_set_config?config={config}", - timeout=10, - ) - elif self._vpnurl: - requests.get( - f"{self._vpnurl}/command/floodlight_set_config?config={config}", - timeout=10, - verify=self._verify_ssl, - ) - else: - _LOGGER.error("Presence VPN URL is None") - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name - ) - return None - except requests.exceptions.RequestException as error: - _LOGGER.error("Presence URL changed: %s", error) - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name - ) - return None - else: - self.async_schedule_update_ha_state(True) - else: - _LOGGER.error("Unsupported camera model for light mode") + @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) + def update_event(self, camera_type): + """Call the Netatmo API to update the events.""" + self.camera_data.updateEvent(devicetype=camera_type) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9e320c303c8..f36328a5887 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -7,7 +7,7 @@ import pyatmo import requests import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -23,15 +23,21 @@ from homeassistant.components.climate.const import ( from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, - CONF_NAME, PRECISION_HALVES, STATE_OFF, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from .const import DATA_NETATMO_AUTH +from .const import ( + ATTR_HOME_NAME, + ATTR_SCHEDULE_NAME, + AUTH, + DOMAIN, + MANUFACTURER, + SERVICE_SETSCHEDULE, +) _LOGGER = logging.getLogger(__name__) @@ -85,63 +91,67 @@ CONF_ROOMS = "rooms" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) -HOME_CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA])} -) - DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" NA_VALVE = "NRV" +SCHEMA_SERVICE_SETSCHEDULE = vol.Schema( + { + vol.Required(ATTR_SCHEDULE_NAME): cv.string, + vol.Required(ATTR_HOME_NAME): cv.string, + } +) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NetAtmo Thermostat.""" - homes_conf = config.get(CONF_HOMES) - auth = hass.data[DATA_NETATMO_AUTH] +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Netatmo energy platform.""" + auth = hass.data[DOMAIN][entry.entry_id][AUTH] home_data = HomeData(auth) - try: - home_data.setup() - except pyatmo.NoDevice: - return - home_ids = [] - rooms = {} - if homes_conf is not None: - for home_conf in homes_conf: - home = home_conf[CONF_NAME] - home_id = home_data.homedata.gethomeId(home) - if home_conf[CONF_ROOMS] != []: - rooms[home_id] = home_conf[CONF_ROOMS] - home_ids.append(home_id) - else: - home_ids = home_data.get_home_ids() - - devices = [] - for home_id in home_ids: - _LOGGER.debug("Setting up %s ...", home_id) + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] try: - room_data = ThermostatData(auth, home_id) + home_data.setup() except pyatmo.NoDevice: - continue - for room_id in room_data.get_room_ids(): - room_name = room_data.homedata.rooms[home_id][room_id]["name"] - _LOGGER.debug("Setting up %s (%s) ...", room_name, room_id) - if home_id in rooms and room_name not in rooms[home_id]: - _LOGGER.debug("Excluding %s ...", room_name) + return + home_ids = home_data.get_all_home_ids() + + for home_id in home_ids: + _LOGGER.debug("Setting up home %s ...", home_id) + try: + room_data = ThermostatData(auth, home_id) + except pyatmo.NoDevice: continue - _LOGGER.debug("Adding devices for room %s (%s) ...", room_name, room_id) - devices.append(NetatmoThermostat(room_data, room_id)) - add_entities(devices, True) + for room_id in room_data.get_room_ids(): + room_name = room_data.homedata.rooms[home_id][room_id]["name"] + _LOGGER.debug("Setting up room %s (%s) ...", room_name, room_id) + entities.append(NetatmoThermostat(room_data, room_id)) + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + def _service_setschedule(service): + """Service to change current home schedule.""" + home_name = service.data.get(ATTR_HOME_NAME) + schedule_name = service.data.get(ATTR_SCHEDULE_NAME) + home_data.homedata.switchHomeSchedule(schedule=schedule_name, home=home_name) + _LOGGER.info("Set home (%s) schedule to %s", home_name, schedule_name) + + if home_data.homedata is not None: + hass.services.async_register( + DOMAIN, + SERVICE_SETSCHEDULE, + _service_setschedule, + schema=SCHEMA_SERVICE_SETSCHEDULE, + ) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Netatmo energy sensors.""" + return class NetatmoThermostat(ClimateDevice): @@ -153,7 +163,7 @@ class NetatmoThermostat(ClimateDevice): self._state = None self._room_id = room_id self._room_name = self._data.homedata.rooms[self._data.home_id][room_id]["name"] - self._name = f"netatmo_{self._room_name}" + self._name = f"{MANUFACTURER} {self._room_name}" self._current_temperature = None self._target_temperature = None self._preset = None @@ -168,6 +178,23 @@ class NetatmoThermostat(ClimateDevice): if self._module_type == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) + self._unique_id = f"{self._room_id}-{self._module_type}" + + @property + def device_info(self): + """Return the device info for the thermostat/valve.""" + return { + "identifiers": {(DOMAIN, self._room_id)}, + "name": self._room_name, + "manufacturer": MANUFACTURER, + "model": self._module_type, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def supported_features(self): """Return the list of supported features.""" @@ -330,7 +357,7 @@ class NetatmoThermostat(ClimateDevice): except KeyError as err: _LOGGER.error( "The thermostat in room %s seems to be out of reach. (%s)", - self._room_id, + self._room_name, err, ) self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -350,7 +377,7 @@ class HomeData: self.home = home self.home_id = None - def get_home_ids(self): + def get_all_home_ids(self): """Get all the home ids returned by NetAtmo API.""" if self.homedata is None: return [] @@ -426,8 +453,6 @@ class ThermostatData: except requests.exceptions.Timeout: _LOGGER.warning("Timed out when connecting to Netatmo server") return - _LOGGER.debug("Following is the debugging output for homestatus:") - _LOGGER.debug(self.homestatus.rawData) for room in self.homestatus.rooms: try: roomstatus = {} diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py new file mode 100644 index 00000000000..8f59382dd46 --- /dev/null +++ b/homeassistant/components/netatmo/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for Netatmo.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NetatmoFlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Netatmo OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": ( + " ".join( + [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", + ] + ) + ) + } + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + return await super().async_step_user(user_input) + + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + return await self.async_step_user() diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index c036a52991b..5d981dc23b4 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,5 +1,57 @@ """Constants used by the Netatmo component.""" -DOMAIN = "netatmo" +from datetime import timedelta -DATA_NETATMO = "netatmo" -DATA_NETATMO_AUTH = "netatmo_auth" +API = "api" + +DOMAIN = "netatmo" +MANUFACTURER = "Netatmo" + +AUTH = "netatmo_auth" +CONF_PUBLIC = "public_sensor_config" +CAMERA_DATA = "netatmo_camera" +HOME_DATA = "netatmo_home_data" + +OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" +OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" + +DATA_PERSONS = "netatmo_persons" + +NETATMO_WEBHOOK_URL = None + +DEFAULT_PERSON = "Unknown" +DEFAULT_DISCOVERY = True +DEFAULT_WEBHOOKS = False + +EVENT_PERSON = "person" +EVENT_MOVEMENT = "movement" +EVENT_HUMAN = "human" +EVENT_ANIMAL = "animal" +EVENT_VEHICLE = "vehicle" + +EVENT_BUS_PERSON = "netatmo_person" +EVENT_BUS_MOVEMENT = "netatmo_movement" +EVENT_BUS_HUMAN = "netatmo_human" +EVENT_BUS_ANIMAL = "netatmo_animal" +EVENT_BUS_VEHICLE = "netatmo_vehicle" +EVENT_BUS_OTHER = "netatmo_other" + +ATTR_ID = "id" +ATTR_PSEUDO = "pseudo" +ATTR_NAME = "name" +ATTR_EVENT_TYPE = "event_type" +ATTR_MESSAGE = "message" +ATTR_CAMERA_ID = "camera_id" +ATTR_HOME_ID = "home_id" +ATTR_HOME_NAME = "home_name" +ATTR_PERSONS = "persons" +ATTR_IS_KNOWN = "is_known" +ATTR_FACE_URL = "face_url" +ATTR_SNAPSHOT_URL = "snapshot_url" +ATTR_VIGNETTE_URL = "vignette_url" +ATTR_SCHEDULE_ID = "schedule_id" +ATTR_SCHEDULE_NAME = "schedule_name" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) +MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) + +SERVICE_SETSCHEDULE = "set_schedule" diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ff421363506..14ec2e61b9c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,21 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==3.1.0"], - "dependencies": ["webhook"], - "codeowners": [] -} + "requirements": [ + "pyatmo==3.2.2" + ], + "dependencies": [ + "webhook" + ], + "codeowners": [ + "@cgtobi" + ], + "config_flow": true, + "homekit": { + "models": [ + "Netatmo Relay", + "Presence", + "Welcome" + ] + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index d4d624061f5..82c3748d19b 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,29 +1,20 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging -import threading from time import time import pyatmo -import requests -import urllib3 -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MODE, - CONF_NAME, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import call_later from homeassistant.util import Throttle -from .const import DATA_NETATMO_AUTH, DOMAIN +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -38,13 +29,11 @@ CONF_LON_SW = "lon_sw" DEFAULT_MODE = "avg" MODE_TYPES = {"max", "avg"} -DEFAULT_NAME_PUBLIC = "Netatmo Public Data" - # This is the Netatmo data upload interval in seconds NETATMO_UPDATE_INTERVAL = 600 # NetAtmo Public Data is uploaded to server every 10 minutes -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=NETATMO_UPDATE_INTERVAL) SUPPORTED_PUBLIC_SENSOR_TYPES = [ "temperature", @@ -90,26 +79,6 @@ SENSOR_TYPES = { "health_idx": ["Health", "", "mdi:cloud", None], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_STATION): cv.string, - vol.Optional(CONF_MODULES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_AREAS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_LAT_NE): cv.latitude, - vol.Required(CONF_LAT_SW): cv.latitude, - vol.Required(CONF_LON_NE): cv.longitude, - vol.Required(CONF_LON_SW): cv.longitude, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(MODE_TYPES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME_PUBLIC): cv.string, - } - ], - ), - } -) - MODULE_TYPE_OUTDOOR = "NAModule1" MODULE_TYPE_WIND = "NAModule2" MODULE_TYPE_RAIN = "NAModule3" @@ -122,75 +91,47 @@ NETATMO_DEVICE_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the available Netatmo weather sensors.""" - dev = [] - auth = hass.data[DATA_NETATMO_AUTH] +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Netatmo weather and homecoach platform.""" + auth = hass.data[DOMAIN][entry.entry_id][AUTH] - if config.get(CONF_AREAS) is not None: - for area in config[CONF_AREAS]: - data = NetatmoPublicData( - auth, - lat_ne=area[CONF_LAT_NE], - lon_ne=area[CONF_LON_NE], - lat_sw=area[CONF_LAT_SW], - lon_sw=area[CONF_LON_SW], - ) - for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - dev.append( - NetatmoPublicSensor( - area[CONF_NAME], data, sensor_type, area[CONF_MODE] - ) - ) - else: + def find_entities(data): + """Find all entities.""" + all_module_infos = data.get_module_infos() + entities = [] + for module in all_module_infos.values(): + _LOGGER.debug("Adding module %s %s", module["module_name"], module["id"]) + for condition in data.station_data.monitoredConditions( + moduleId=module["id"] + ): + entities.append(NetatmoSensor(data, module, condition.lower())) + return entities - def find_devices(data): - """Find all devices.""" - all_module_infos = data.get_module_infos() - all_module_names = [e["module_name"] for e in all_module_infos.values()] - module_names = config.get(CONF_MODULES, all_module_names) - entities = [] - for module_name in module_names: - if module_name not in all_module_names: - _LOGGER.info("Module %s not found", module_name) - for module in all_module_infos.values(): - if module["module_name"] not in module_names: - continue - _LOGGER.debug( - "Adding module %s %s", module["module_name"], module["id"] - ) - for condition in data.station_data.monitoredConditions( - moduleId=module["id"] - ): - entities.append(NetatmoSensor(data, module, condition.lower())) - return entities - - def _retry(_data): - try: - entities = find_devices(_data) - except requests.exceptions.Timeout: - return call_later( - hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(_data) - ) - if entities: - add_entities(entities, True) + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: - data = NetatmoData(auth, data_class, config.get(CONF_STATION)) + dc_data = data_class(auth) + _LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__]) + data = NetatmoData(auth, dc_data) except pyatmo.NoDevice: - _LOGGER.info( - "No %s devices found", NETATMO_DEVICE_TYPES[data_class.__name__] + _LOGGER.debug( + "No %s entities found", NETATMO_DEVICE_TYPES[data_class.__name__] ) continue - try: - dev.extend(find_devices(data)) - except requests.exceptions.Timeout: - call_later(hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(data)) + entities.extend(find_entities(data)) - if dev: - add_entities(dev, True) + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Netatmo weather and homecoach platform.""" + return class NetatmoSensor(Entity): @@ -212,7 +153,7 @@ class NetatmoSensor(Entity): f"{module_info['station_name']} {module_info['module_name']}" ) - self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" + self._name = f"{MANUFACTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -237,6 +178,16 @@ class NetatmoSensor(Entity): """Return the device class of the sensor.""" return self._device_class + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._module_id)}, + "name": self.module_name, + "manufacturer": MANUFACTURER, + "model": self._module_type, + } + @property def state(self): """Return the state of the device.""" @@ -258,14 +209,15 @@ class NetatmoSensor(Entity): if self.netatmo_data.data is None: if self._state is None: return - _LOGGER.warning("No data found for %s", self.module_name) + _LOGGER.warning("No data from update") self._state = None return data = self.netatmo_data.data.get(self._module_id) if data is None: - _LOGGER.warning("No data found for %s", self.module_name) + _LOGGER.info("No data found for %s (%s)", self.module_name, self._module_id) + _LOGGER.error("data: %s", self.netatmo_data.data) self._state = None return @@ -420,7 +372,7 @@ class NetatmoSensor(Entity): elif data["health_idx"] == 4: self._state = "Unhealthy" except KeyError: - _LOGGER.error("No %s data found for %s", self.type, self.module_name) + _LOGGER.info("No %s data found for %s", self.type, self.module_name) self._state = None return @@ -433,7 +385,7 @@ class NetatmoPublicSensor(Entity): self.netatmo_data = data self.type = sensor_type self._mode = mode - self._name = "{} {}".format(area_name, SENSOR_TYPES[self.type][0]) + self._name = f"{MANUFACTURER} {area_name} {SENSOR_TYPES[self.type][0]}" self._area_name = area_name self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -455,6 +407,16 @@ class NetatmoPublicSensor(Entity): """Return the device class of the sensor.""" return self._device_class + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._area_name)}, + "name": self._area_name, + "manufacturer": MANUFACTURER, + "model": "public", + } + @property def state(self): """Return the state of the device.""" @@ -465,12 +427,17 @@ class NetatmoPublicSensor(Entity): """Return the unit of measurement of this entity.""" return self._unit_of_measurement + @property + def available(self): + """Return True if entity is available.""" + return bool(self._state) + def update(self): """Get the latest data from Netatmo API and updates the states.""" self.netatmo_data.update() if self.netatmo_data.data is None: - _LOGGER.warning("No data found for %s", self._name) + _LOGGER.info("No data found for %s", self._name) self._state = None return @@ -522,14 +489,21 @@ class NetatmoPublicData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" - data = pyatmo.PublicData( - self.auth, - LAT_NE=self.lat_ne, - LON_NE=self.lon_ne, - LAT_SW=self.lat_sw, - LON_SW=self.lon_sw, - filtering=True, - ) + try: + data = pyatmo.PublicData( + self.auth, + LAT_NE=self.lat_ne, + LON_NE=self.lon_ne, + LAT_SW=self.lat_sw, + LON_SW=self.lon_sw, + filtering=True, + ) + except pyatmo.NoDevice: + data = None + + if not data: + _LOGGER.debug("No data received when updating public station data") + return if data.CountStationInArea() == 0: _LOGGER.warning("No Stations available in this area.") @@ -541,83 +515,24 @@ class NetatmoPublicData: class NetatmoData: """Get the latest data from Netatmo.""" - def __init__(self, auth, data_class, station): + def __init__(self, auth, station_data): """Initialize the data object.""" - self.auth = auth - self.data_class = data_class self.data = {} - self.station_data = self.data_class(self.auth) - self.station = station - self.station_id = None - if station: - station_data = self.station_data.stationByName(self.station) - if station_data: - self.station_id = station_data.get("_id") + self.station_data = station_data self._next_update = time() - self._update_in_progress = threading.Lock() + self.auth = auth def get_module_infos(self): """Return all modules available on the API as a dict.""" - if self.station_id is not None: - return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Call the Netatmo API to update the data. + """Call the Netatmo API to update the data.""" + self.station_data = self.station_data.__class__(self.auth) - This method is not throttled by the builtin Throttle decorator - but with a custom logic, which takes into account the time - of the last update from the cloud. - """ - if time() < self._next_update or not self._update_in_progress.acquire(False): + data = self.station_data.lastData(exclude=3600, byId=True) + if not data: + _LOGGER.debug("No data received when updating station data") return - try: - try: - self.station_data = self.data_class(self.auth) - _LOGGER.debug("%s detected!", str(self.data_class.__name__)) - except pyatmo.NoDevice: - _LOGGER.warning( - "No Weather or HomeCoach devices found for %s", str(self.station) - ) - return - except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError): - _LOGGER.warning("Timed out when connecting to Netatmo server.") - return - - data = self.station_data.lastData( - station=self.station_id, exclude=3600, byId=True - ) - if not data: - self._next_update = time() + NETATMO_UPDATE_INTERVAL - return - self.data = data - - newinterval = 0 - try: - for module in self.data: - if "When" in self.data[module]: - newinterval = self.data[module]["When"] - break - except TypeError: - _LOGGER.debug("No %s modules found", self.data_class.__name__) - - if newinterval: - # Try and estimate when fresh data will be available - newinterval += NETATMO_UPDATE_INTERVAL - time() - if newinterval > NETATMO_UPDATE_INTERVAL - 30: - newinterval = NETATMO_UPDATE_INTERVAL - else: - if newinterval < NETATMO_UPDATE_INTERVAL / 2: - # Never hammer the Netatmo API more than - # twice per update interval - newinterval = NETATMO_UPDATE_INTERVAL / 2 - _LOGGER.info( - "Netatmo refresh interval reset to %d seconds", newinterval - ) - else: - # Last update time not found, fall back to default value - newinterval = NETATMO_UPDATE_INTERVAL - - self._next_update = time() + newinterval - finally: - self._update_in_progress.release() + self.data = data diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index d8fa223780a..46de69b5cb3 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,37 +1,10 @@ -addwebhook: - description: Add webhook during runtime (e.g. if it has been banned). - fields: - url: - description: URL for which to add the webhook. - example: https://yourdomain.com:443/api/webhook/webhook_id - -dropwebhook: - description: Drop active webhooks. - -set_light_auto: - description: Set the camera (Presence only) light in automatic mode. - fields: - entity_id: - description: Entity id. - example: 'camera.living_room' - -set_light_on: - description: Set the camera (Netatmo Presence only) light on. - fields: - entity_id: - description: Entity id. - example: 'camera.living_room' - -set_light_off: - description: Set the camera (Netatmo Presence only) light off. - fields: - entity_id: - description: Entity id. - example: 'camera.living_room' - +# Describes the format for available Netatmo services set_schedule: - description: Set the home heating schedule + description: Set the heating schedule. fields: - schedule: - description: Schedule name - example: Standard \ No newline at end of file + schedule_name: + description: Schedule name. + example: Standard + home_name: + description: Home name. + example: MyHome diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json new file mode 100644 index 00000000000..8cd4f51aee2 --- /dev/null +++ b/homeassistant/components/netatmo/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Netatmo", + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Netatmo account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Netatmo." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 3e87bcac53c..23b1034a5b3 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -110,7 +110,10 @@ class NetgearDeviceScanner(DeviceScanner): or dev.name in self.excluded_devices ) ) - if tracked: + + # when link_rate is None this means the router still knows about + # the device, but it is not in range. + if tracked and dev.link_rate is not None: devices.append(dev.mac) if ( self.tracked_accesspoints diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 35c928deb37..4865b0a9839 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -80,7 +80,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] if station_live is not None: - sensors.append(NMBSLiveBoard(api_client, station_live)) + sensors.append( + NMBSLiveBoard(api_client, station_live, station_from, station_to) + ) add_entities(sensors, True) @@ -88,23 +90,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NMBSLiveBoard(Entity): """Get the next train from a station's liveboard.""" - def __init__(self, api_client, live_station): + def __init__(self, api_client, live_station, station_from, station_to): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client - self._unique_id = f"nmbs_live_{self._station}" + self._station_from = station_from + self._station_to = station_to self._attrs = {} self._state = None @property def name(self): """Return the sensor default name.""" - return "NMBS Live" + return f"NMBS Live ({self._station})" @property def unique_id(self): """Return a unique ID.""" - return self._unique_id + unique_id = f"{self._station}_{self._station_from}_{self._station_to}" + + return f"nmbs_live_{unique_id}" @property def icon(self): @@ -130,6 +135,7 @@ class NMBSLiveBoard(Entity): attrs = { "departure": f"In {departure} minutes", + "departure_minutes": departure, "extra_train": int(self._attrs["isExtra"]) > 0, "vehicle_id": self._attrs["vehicle"], "monitored_station": self._station, @@ -138,6 +144,7 @@ class NMBSLiveBoard(Entity): if delay > 0: attrs["delay"] = f"{delay} minutes" + attrs["delay_minutes"] = delay return attrs @@ -200,6 +207,7 @@ class NMBSSensor(Entity): attrs = { "departure": f"In {departure} minutes", + "departure_minutes": departure, "destination": self._station_to, "direction": self._attrs["departure"]["direction"]["name"], "platform_arriving": self._attrs["arrival"]["platform"], @@ -224,6 +232,7 @@ class NMBSSensor(Entity): if delay > 0: attrs["delay"] = f"{delay} minutes" + attrs["delay_minutes"] = delay return attrs diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 8211fdc0828..1ea0b9aa6d5 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -177,10 +177,9 @@ class BaseNotificationService: """ raise NotImplementedError() - def async_send_message(self, message, **kwargs): + async def async_send_message(self, message, **kwargs): """Send a message. kwargs can contain ATTR_TITLE to specify a title. - This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job(partial(self.send_message, message, **kwargs)) + await self.hass.async_add_job(partial(self.send_message, message, **kwargs)) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index 85867b32bde..5bb4cb46ee0 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.8.1"] + "requirements": ["pynws==0.10.1"] } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index c22700f1cf8..a0ce1449479 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -177,7 +177,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= websession = async_get_clientsession(hass) # ID request as being from HA, pynws prepends the api_key in addition api_key_ha = f"{api_key} homeassistant" - nws = SimpleNWS(latitude, longitude, api_key_ha, mode, websession) + nws = SimpleNWS(latitude, longitude, api_key_ha, websession) _LOGGER.debug("Setting up station: %s", station) try: @@ -226,15 +226,24 @@ class NWSWeather(WeatherEntity): ) else: self.observation = self.nws.observation + _LOGGER.debug("Observation: %s", self.observation) _LOGGER.debug("Updating forecast") try: - await self.nws.update_forecast() + if self.mode == "daynight": + await self.nws.update_forecast() + else: + await self.nws.update_forecast_hourly() except ERRORS as status: _LOGGER.error( "Error updating forecast from station %s: %s", self.nws.station, status ) return - self._forecast = self.nws.forecast + if self.mode == "daynight": + self._forecast = self.nws.forecast + else: + self._forecast = self.nws.forecast_hourly + _LOGGER.debug("Forecast: %s", self._forecast) + _LOGGER.debug("Finished updating") @property def attribution(self): diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 222c1d87ec0..5d6ee47c3eb 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -5,9 +5,11 @@ from openhomedevice.Device import Device from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( + MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, @@ -94,13 +96,14 @@ class OpenhomeDevice(MediaPlayerDevice): self._source_names = source_names if self._source["type"] == "Radio": - self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY - if self._source["type"] in ("Playlist", "Cloud"): + self._supported_features |= SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA + if self._source["type"] in ("Playlist", "Spotify"): self._supported_features |= ( SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA ) if self._in_standby: @@ -123,6 +126,18 @@ class OpenhomeDevice(MediaPlayerDevice): """Put device in standby.""" self._device.SetStandby(True) + def play_media(self, media_type, media_id, **kwargs): + """Send the play_media command to the media player.""" + if not media_type == MEDIA_TYPE_MUSIC: + _LOGGER.error( + "Invalid media type %s. Only %s is supported", + media_type, + MEDIA_TYPE_MUSIC, + ) + return + track_details = {"title": "Home Assistant", "uri": media_id} + self._device.PlayMedia(track_details) + def media_pause(self): """Send pause command.""" self._device.Pause() diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 167fcdcd0e6..f130872da5f 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, CONF_SENSORS, ) from homeassistant.exceptions import ConfigEntryNotReady @@ -52,60 +51,6 @@ TYPE_SAFE_EXPOSURE_TIME_4 = "safe_exposure_time_type_4" TYPE_SAFE_EXPOSURE_TIME_5 = "safe_exposure_time_type_5" TYPE_SAFE_EXPOSURE_TIME_6 = "safe_exposure_time_type_6" -BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses")} - -BINARY_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ) - } -) - -SENSORS = { - TYPE_CURRENT_OZONE_LEVEL: ("Current Ozone Level", "mdi:vector-triangle", "du"), - TYPE_CURRENT_UV_INDEX: ("Current UV Index", "mdi:weather-sunny", "index"), - TYPE_CURRENT_UV_LEVEL: ("Current UV Level", "mdi:weather-sunny", None), - TYPE_MAX_UV_INDEX: ("Max UV Index", "mdi:weather-sunny", "index"), - TYPE_SAFE_EXPOSURE_TIME_1: ( - "Skin Type 1 Safe Exposure Time", - "mdi:timer", - "minutes", - ), - TYPE_SAFE_EXPOSURE_TIME_2: ( - "Skin Type 2 Safe Exposure Time", - "mdi:timer", - "minutes", - ), - TYPE_SAFE_EXPOSURE_TIME_3: ( - "Skin Type 3 Safe Exposure Time", - "mdi:timer", - "minutes", - ), - TYPE_SAFE_EXPOSURE_TIME_4: ( - "Skin Type 4 Safe Exposure Time", - "mdi:timer", - "minutes", - ), - TYPE_SAFE_EXPOSURE_TIME_5: ( - "Skin Type 5 Safe Exposure Time", - "mdi:timer", - "minutes", - ), - TYPE_SAFE_EXPOSURE_TIME_6: ( - "Skin Type 6 Safe Exposure Time", - "mdi:timer", - "minutes", - ), -} - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ) - } -) CONFIG_SCHEMA = vol.Schema( { @@ -115,8 +60,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_ELEVATION): float, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, } ) }, @@ -142,12 +85,7 @@ async def async_setup(hass, config): if identifier in configured_instances(hass): return True - data = { - CONF_API_KEY: conf[CONF_API_KEY], - CONF_BINARY_SENSORS: conf[CONF_BINARY_SENSORS], - CONF_SENSORS: conf[CONF_SENSORS], - } - + data = {CONF_API_KEY: conf[CONF_API_KEY]} if CONF_LATITUDE in conf: data[CONF_LATITUDE] = conf[CONF_LATITUDE] if CONF_LONGITUDE in conf: @@ -178,13 +116,7 @@ async def async_setup_entry(hass, config_entry): config_entry.data.get(CONF_LONGITUDE, hass.config.longitude), websession, altitude=config_entry.data.get(CONF_ELEVATION, hass.config.elevation), - ), - config_entry.data.get(CONF_BINARY_SENSORS, {}).get( - CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS) - ), - config_entry.data.get(CONF_SENSORS, {}).get( - CONF_MONITORED_CONDITIONS, list(SENSORS) - ), + ) ) await openuv.async_update() hass.data[DOMAIN][DATA_OPENUV_CLIENT][config_entry.entry_id] = openuv @@ -243,39 +175,49 @@ async def async_unload_entry(hass, config_entry): return True +async def async_migrate_entry(hass, config_entry): + """Migrate the config entry upon new versions.""" + version = config_entry.version + data = {**config_entry.data} + + _LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: Remove unused condition data: + if version == 1: + data.pop(CONF_BINARY_SENSORS, None) + data.pop(CONF_SENSORS, None) + version = config_entry.version = 2 + hass.config_entries.async_update_entry(config_entry, data=data) + _LOGGER.debug("Migration to version %s successful", version) + + return True + + class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, client, binary_sensor_conditions, sensor_conditions): + def __init__(self, client): """Initialize.""" - self.binary_sensor_conditions = binary_sensor_conditions self.client = client self.data = {} - self.sensor_conditions = sensor_conditions async def async_update_protection_data(self): """Update binary sensor (protection window) data.""" - - if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: - try: - resp = await self.client.uv_protection_window() - self.data[DATA_PROTECTION_WINDOW] = resp["result"] - except OpenUvError as err: - _LOGGER.error("Error during protection data update: %s", err) - self.data[DATA_PROTECTION_WINDOW] = {} - return + try: + resp = await self.client.uv_protection_window() + self.data[DATA_PROTECTION_WINDOW] = resp["result"] + except OpenUvError as err: + _LOGGER.error("Error during protection data update: %s", err) + self.data[DATA_PROTECTION_WINDOW] = {} async def async_update_uv_index_data(self): """Update sensor (uv index, etc) data.""" - - if any(c in self.sensor_conditions for c in SENSORS): - try: - data = await self.client.uv_index() - self.data[DATA_UV] = data - except OpenUvError as err: - _LOGGER.error("Error during uv index data update: %s", err) - self.data[DATA_UV] = {} - return + try: + data = await self.client.uv_index() + self.data[DATA_UV] = data + except OpenUvError as err: + _LOGGER.error("Error during uv index data update: %s", err) + self.data[DATA_UV] = {} async def async_update(self): """Update sensor/binary sensor data.""" @@ -289,9 +231,15 @@ class OpenUvEntity(Entity): def __init__(self, openuv): """Initialize.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._available = True self._name = None self.openuv = openuv + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 1e765abbbce..6bd3dda13fd 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_local, parse_datetime, utcnow from . import ( - BINARY_SENSORS, DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, @@ -17,15 +16,13 @@ from . import ( ) _LOGGER = logging.getLogger(__name__) + ATTR_PROTECTION_WINDOW_ENDING_TIME = "end_time" ATTR_PROTECTION_WINDOW_ENDING_UV = "end_uv" ATTR_PROTECTION_WINDOW_STARTING_TIME = "start_time" ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up an OpenUV sensor based on existing config.""" - pass +BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses")} async def async_setup_entry(hass, entry, async_add_entities): @@ -33,10 +30,10 @@ async def async_setup_entry(hass, entry, async_add_entities): openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] binary_sensors = [] - for sensor_type in openuv.binary_sensor_conditions: - name, icon = BINARY_SENSORS[sensor_type] + for kind, attrs in BINARY_SENSORS.items(): + name, icon = attrs binary_sensors.append( - OpenUvBinarySensor(openuv, sensor_type, name, icon, entry.entry_id) + OpenUvBinarySensor(openuv, kind, name, icon, entry.entry_id) ) async_add_entities(binary_sensors, True) @@ -100,8 +97,11 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): data = self.openuv.data[DATA_PROTECTION_WINDOW] if not data: + self._available = False return + self._available = True + for key in ("from_time", "to_time", "from_uv", "to_uv"): if not data.get(key): _LOGGER.info("Skipping update due to missing data: %s", key) diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 7dd8ed45a79..82873861fb1 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -32,7 +32,7 @@ def configured_instances(hass): class OpenUvFlowHandler(config_entries.ConfigFlow): """Handle an OpenUV config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index a482464e4d0..2df62bcc09f 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -9,7 +9,6 @@ from . import ( DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, - SENSORS, TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, @@ -43,10 +42,42 @@ UV_LEVEL_HIGH = "High" UV_LEVEL_MODERATE = "Moderate" UV_LEVEL_LOW = "Low" - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up an OpenUV sensor based on existing config.""" - pass +SENSORS = { + TYPE_CURRENT_OZONE_LEVEL: ("Current Ozone Level", "mdi:vector-triangle", "du"), + TYPE_CURRENT_UV_INDEX: ("Current UV Index", "mdi:weather-sunny", "index"), + TYPE_CURRENT_UV_LEVEL: ("Current UV Level", "mdi:weather-sunny", None), + TYPE_MAX_UV_INDEX: ("Max UV Index", "mdi:weather-sunny", "index"), + TYPE_SAFE_EXPOSURE_TIME_1: ( + "Skin Type 1 Safe Exposure Time", + "mdi:timer", + "minutes", + ), + TYPE_SAFE_EXPOSURE_TIME_2: ( + "Skin Type 2 Safe Exposure Time", + "mdi:timer", + "minutes", + ), + TYPE_SAFE_EXPOSURE_TIME_3: ( + "Skin Type 3 Safe Exposure Time", + "mdi:timer", + "minutes", + ), + TYPE_SAFE_EXPOSURE_TIME_4: ( + "Skin Type 4 Safe Exposure Time", + "mdi:timer", + "minutes", + ), + TYPE_SAFE_EXPOSURE_TIME_5: ( + "Skin Type 5 Safe Exposure Time", + "mdi:timer", + "minutes", + ), + TYPE_SAFE_EXPOSURE_TIME_6: ( + "Skin Type 6 Safe Exposure Time", + "mdi:timer", + "minutes", + ), +} async def async_setup_entry(hass, entry, async_add_entities): @@ -54,11 +85,9 @@ async def async_setup_entry(hass, entry, async_add_entities): openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] sensors = [] - for sensor_type in openuv.sensor_conditions: - name, icon, unit = SENSORS[sensor_type] - sensors.append( - OpenUvSensor(openuv, sensor_type, name, icon, unit, entry.entry_id) - ) + for kind, attrs in SENSORS.items(): + name, icon, unit = attrs + sensors.append(OpenUvSensor(openuv, kind, name, icon, unit, entry.entry_id)) async_add_entities(sensors, True) @@ -124,7 +153,14 @@ class OpenUvSensor(OpenUvEntity): async def async_update(self): """Update the state.""" - data = self.openuv.data[DATA_UV]["result"] + data = self.openuv.data[DATA_UV].get("result") + + if not data: + self._available = False + return + + self._available = True + if self._sensor_type == TYPE_CURRENT_OZONE_LEVEL: self._state = data["ozone"] elif self._sensor_type == TYPE_CURRENT_UV_INDEX: diff --git a/homeassistant/components/opnsense/__init__.py b/homeassistant/components/opnsense/__init__.py new file mode 100644 index 00000000000..608bca0f03b --- /dev/null +++ b/homeassistant/components/opnsense/__init__.py @@ -0,0 +1,77 @@ +"""Support for OPNSense Routers.""" +import logging + +from pyopnsense import diagnostics +from pyopnsense.exceptions import APIException +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform + +_LOGGER = logging.getLogger(__name__) + +CONF_API_SECRET = "api_secret" +CONF_TRACKER_INTERFACE = "tracker_interfaces" + +DOMAIN = "opnsense" + +OPNSENSE_DATA = DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_SECRET): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean, + vol.Optional(CONF_TRACKER_INTERFACE, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the opnsense component.""" + + conf = config[DOMAIN] + url = conf[CONF_URL] + api_key = conf[CONF_API_KEY] + api_secret = conf[CONF_API_SECRET] + verify_ssl = conf[CONF_VERIFY_SSL] + tracker_interfaces = conf[CONF_TRACKER_INTERFACE] + + interfaces_client = diagnostics.InterfaceClient( + api_key, api_secret, url, verify_ssl + ) + try: + interfaces_client.get_arp() + except APIException: + _LOGGER.exception("Failure while connecting to OPNsense API endpoint.") + return False + + if tracker_interfaces: + # Verify that specified tracker interfaces are valid + netinsight_client = diagnostics.NetworkInsightClient( + api_key, api_secret, url, verify_ssl + ) + interfaces = list(netinsight_client.get_interfaces().values()) + for interface in tracker_interfaces: + if interface not in interfaces: + _LOGGER.error( + "Specified OPNsense tracker interface %s is not found", interface + ) + return False + + hass.data[OPNSENSE_DATA] = { + "interfaces": interfaces_client, + CONF_TRACKER_INTERFACE: tracker_interfaces, + } + + load_platform(hass, "device_tracker", DOMAIN, tracker_interfaces, config) + return True diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py new file mode 100644 index 00000000000..c64e0b0679a --- /dev/null +++ b/homeassistant/components/opnsense/device_tracker.py @@ -0,0 +1,66 @@ +"""Device tracker support for OPNSense routers.""" +import logging + +from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.opnsense import CONF_TRACKER_INTERFACE, OPNSENSE_DATA + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_scanner(hass, config, discovery_info=None): + """Configure the OPNSense device_tracker.""" + interface_client = hass.data[OPNSENSE_DATA]["interfaces"] + scanner = OPNSenseDeviceScanner( + interface_client, hass.data[OPNSENSE_DATA][CONF_TRACKER_INTERFACE] + ) + return scanner + + +class OPNSenseDeviceScanner(DeviceScanner): + """This class queries a router running OPNsense.""" + + def __init__(self, client, interfaces): + """Initialize the scanner.""" + self.last_results = {} + self.client = client + self.interfaces = interfaces + + def _get_mac_addrs(self, devices): + """Create dict with mac address keys from list of devices.""" + out_devices = {} + for device in devices: + if not self.interfaces: + out_devices[device["mac"]] = device + elif device["intf_description"] in self.interfaces: + out_devices[device["mac"]] = device + return out_devices + + def scan_devices(self): + """Scan for new devices and return a list with found device IDs.""" + self.update_info() + return list(self.last_results) + + def get_device_name(self, device): + """Return the name of the given device or None if we don't know.""" + if device not in self.last_results: + return None + hostname = self.last_results[device].get("hostname") or None + return hostname + + def update_info(self): + """Ensure the information from the OPNSense router is up to date. + + Return boolean if scanning successful. + """ + + devices = self.client.get_arp() + self.last_results = self._get_mac_addrs(devices) + + def get_extra_attributes(self, device): + """Return the extra attrs of the given device.""" + if device not in self.last_results: + return None + mfg = self.last_results[device].get("manufacturer") + if mfg: + return {"manufacturer": mfg} + return {} diff --git a/homeassistant/components/opnsense/manifest.json b/homeassistant/components/opnsense/manifest.json new file mode 100644 index 00000000000..85831680102 --- /dev/null +++ b/homeassistant/components/opnsense/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "opnsense", + "name": "OPNSense", + "documentation": "https://www.home-assistant.io/integrations/opnsense", + "requirements": [ + "pyopnsense==0.2.0" + ], + "dependencies": [], + "codeowners": ["@mtreinish"] +} diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index 5ed1d12e730..6d93d0407c4 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/oru", "dependencies": [], "codeowners": ["@bvlaicu"], - "requirements": ["oru==0.1.9"] + "requirements": ["oru==0.1.11"] } diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py deleted file mode 100644 index 3882ba4bf7d..00000000000 --- a/homeassistant/components/owlet/__init__.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Support for Owlet baby monitors.""" -import logging - -from pyowlet.PyOwlet import PyOwlet -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform - -from .const import ( - SENSOR_BASE_STATION, - SENSOR_HEART_RATE, - SENSOR_MOVEMENT, - SENSOR_OXYGEN_LEVEL, -) - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "owlet" - -SENSOR_TYPES = [ - SENSOR_OXYGEN_LEVEL, - SENSOR_HEART_RATE, - SENSOR_BASE_STATION, - SENSOR_MOVEMENT, -] - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass, config): - """Set up owlet component.""" - - username = config[DOMAIN][CONF_USERNAME] - password = config[DOMAIN][CONF_PASSWORD] - name = config[DOMAIN].get(CONF_NAME) - - try: - device = PyOwlet(username, password) - except KeyError: - _LOGGER.error( - "Owlet authentication failed. Please verify your credentials are correct" - ) - return False - - device.update_properties() - - if not name: - name = f"{device.baby_name}'s Owlet" - - hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) - - load_platform(hass, "sensor", DOMAIN, {}, config) - load_platform(hass, "binary_sensor", DOMAIN, {}, config) - - return True - - -class OwletDevice: - """Represents a configured Owlet device.""" - - def __init__(self, device, name, monitor): - """Initialize device.""" - self.name = name - self.monitor = monitor - self.device = device diff --git a/homeassistant/components/owlet/binary_sensor.py b/homeassistant/components/owlet/binary_sensor.py deleted file mode 100644 index 48faa00cd9a..00000000000 --- a/homeassistant/components/owlet/binary_sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Support for Owlet binary sensors.""" -from datetime import timedelta - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util import dt as dt_util - -from . import DOMAIN as OWLET_DOMAIN -from .const import SENSOR_BASE_STATION, SENSOR_MOVEMENT - -SCAN_INTERVAL = timedelta(seconds=120) - -BINARY_CONDITIONS = { - SENSOR_BASE_STATION: {"name": "Base Station", "device_class": "power"}, - SENSOR_MOVEMENT: {"name": "Movement", "device_class": "motion"}, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up owlet binary sensor.""" - if discovery_info is None: - return - - device = hass.data[OWLET_DOMAIN] - - entities = [] - for condition in BINARY_CONDITIONS: - if condition in device.monitor: - entities.append(OwletBinarySensor(device, condition)) - - add_entities(entities, True) - - -class OwletBinarySensor(BinarySensorDevice): - """Representation of owlet binary sensor.""" - - def __init__(self, device, condition): - """Init owlet binary sensor.""" - self._device = device - self._condition = condition - self._state = None - self._base_on = False - self._prop_expiration = None - self._is_charging = None - - @property - def name(self): - """Return sensor name.""" - return "{} {}".format( - self._device.name, BINARY_CONDITIONS[self._condition]["name"] - ) - - @property - def is_on(self): - """Return current state of sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return BINARY_CONDITIONS[self._condition]["device_class"] - - def update(self): - """Update state of sensor.""" - self._base_on = self._device.device.base_station_on - self._prop_expiration = self._device.device.prop_expire_time - self._is_charging = self._device.device.charge_status > 0 - - # handle expired values - if self._prop_expiration < dt_util.now().timestamp(): - self._state = False - return - - if self._condition == "movement": - if not self._base_on or self._is_charging: - return False - - self._state = getattr(self._device.device, self._condition) diff --git a/homeassistant/components/owlet/const.py b/homeassistant/components/owlet/const.py deleted file mode 100644 index f145100dbc4..00000000000 --- a/homeassistant/components/owlet/const.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Constants for Owlet component.""" -SENSOR_OXYGEN_LEVEL = "oxygen_level" -SENSOR_HEART_RATE = "heart_rate" - -SENSOR_BASE_STATION = "base_station_on" -SENSOR_MOVEMENT = "movement" diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json deleted file mode 100644 index 632115a93cb..00000000000 --- a/homeassistant/components/owlet/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "owlet", - "name": "Owlet", - "documentation": "https://www.home-assistant.io/integrations/owlet", - "requirements": ["pyowlet==1.0.3"], - "dependencies": [], - "codeowners": ["@oblogic7"] -} diff --git a/homeassistant/components/owlet/sensor.py b/homeassistant/components/owlet/sensor.py deleted file mode 100644 index af88db475e5..00000000000 --- a/homeassistant/components/owlet/sensor.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for Owlet sensors.""" -from datetime import timedelta - -from homeassistant.helpers.entity import Entity -from homeassistant.util import dt as dt_util - -from . import DOMAIN as OWLET_DOMAIN -from .const import SENSOR_HEART_RATE, SENSOR_OXYGEN_LEVEL - -SCAN_INTERVAL = timedelta(seconds=120) - -SENSOR_CONDITIONS = { - SENSOR_OXYGEN_LEVEL: {"name": "Oxygen Level", "device_class": None}, - SENSOR_HEART_RATE: {"name": "Heart Rate", "device_class": None}, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up owlet binary sensor.""" - if discovery_info is None: - return - - device = hass.data[OWLET_DOMAIN] - - entities = [] - for condition in SENSOR_CONDITIONS: - if condition in device.monitor: - entities.append(OwletSensor(device, condition)) - - add_entities(entities, True) - - -class OwletSensor(Entity): - """Representation of Owlet sensor.""" - - def __init__(self, device, condition): - """Init owlet binary sensor.""" - self._device = device - self._condition = condition - self._state = None - self._prop_expiration = None - self.is_charging = None - self.battery_level = None - self.sock_off = None - self.sock_connection = None - self._movement = None - - @property - def name(self): - """Return sensor name.""" - return "{} {}".format( - self._device.name, SENSOR_CONDITIONS[self._condition]["name"] - ) - - @property - def state(self): - """Return current state of sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return SENSOR_CONDITIONS[self._condition]["device_class"] - - @property - def device_state_attributes(self): - """Return state attributes.""" - attributes = { - "battery_charging": self.is_charging, - "battery_level": self.battery_level, - "sock_off": self.sock_off, - "sock_connection": self.sock_connection, - } - - return attributes - - def update(self): - """Update state of sensor.""" - self.is_charging = self._device.device.charge_status - self.battery_level = self._device.device.batt_level - self.sock_off = self._device.device.sock_off - self.sock_connection = self._device.device.sock_connection - self._movement = self._device.device.movement - self._prop_expiration = self._device.device.prop_expire_time - - value = getattr(self._device.device, self._condition) - - if self._condition == "batt_level": - self._state = min(100, value) - return - - if ( - not self._device.device.base_station_on - or self._device.device.charge_status > 0 - or self._prop_expiration < dt_util.now().timestamp() - or self._movement - ): - value = None - - self._state = value diff --git a/homeassistant/components/owntracks/.translations/de.json b/homeassistant/components/owntracks/.translations/de.json index fbd9cec2f5a..ba7721ac0a4 100644 --- a/homeassistant/components/owntracks/.translations/de.json +++ b/homeassistant/components/owntracks/.translations/de.json @@ -4,11 +4,11 @@ "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "create_entry": { - "default": "\n\n\u00d6ffnen Sie unter Android [die OwnTracks-App]({android_url}) und gehen Sie zu {android_url} - > Verbindung. \u00c4ndern Sie die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: ` ` \n - Ger\u00e4te-ID: ` ` \n\n\u00d6ffnen Sie unter iOS [die OwnTracks-App]({ios_url}) und tippen Sie auf das Symbol (i) oben links - > Einstellungen. \u00c4ndern Sie die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: ` ` \n\n {secret} \n \n Weitere Informationen finden Sie in der [Dokumentation]({docs_url})." + "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: ` ` \n - Ger\u00e4te-ID: ` ` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: ` ` \n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "M\u00f6chten Sie OwnTracks wirklich einrichten?", + "description": "M\u00f6chtest du OwnTracks wirklich einrichten?", "title": "OwnTracks einrichten" } }, diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index d357843c42e..7fab391efc1 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -271,8 +271,17 @@ async def async_handle_waypoint(hass, name_base, waypoint): return zone = zone_comp.Zone( - hass, pretty_name, lat, lon, rad, zone_comp.ICON_IMPORT, False + { + zone_comp.CONF_NAME: pretty_name, + zone_comp.CONF_LATITUDE: lat, + zone_comp.CONF_LONGITUDE: lon, + zone_comp.CONF_RADIUS: rad, + zone_comp.CONF_ICON: zone_comp.ICON_IMPORT, + zone_comp.CONF_PASSIVE: False, + }, + False, ) + zone.hass = hass zone.entity_id = entity_id await zone.async_update_ha_state() diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index 548acbc3c1e..510d9dbf1a7 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -1,7 +1,7 @@ { "domain": "pcal9535a", "name": "PCAL9535A I/O Expander", - "documentation": "https://www.home-assistant.io/components/pcal9535a", + "documentation": "https://www.home-assistant.io/integrations/pcal9535a", "requirements": ["pcal9535a==0.7"], "dependencies": [], "codeowners": ["@Shulyaka"] diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c34fb89a718..dabcc046f7a 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -159,7 +159,6 @@ class PersonStorageCollection(collection.StorageCollection): ): """Initialize a person storage collection.""" super().__init__(store, logger, id_manager) - self.async_add_listener(self._collection_changed) self.yaml_collection = yaml_collection async def async_load(self) -> None: @@ -228,16 +227,6 @@ class PersonStorageCollection(collection.StorageCollection): if any(person for person in persons if person.get(CONF_USER_ID) == user_id): raise ValueError("User already taken") - async def _collection_changed( - self, change_type: str, item_id: str, config: Optional[dict] - ) -> None: - """Handle a collection change.""" - if change_type != collection.CHANGE_REMOVED: - return - - ent_reg = await entity_registry.async_get_registry(self.hass) - ent_reg.async_remove(ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) - async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List[dict]: """Validate YAML data that we can't validate via schema.""" @@ -294,6 +283,8 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): collection.attach_entity_component_collection( entity_component, storage_collection, lambda conf: Person(conf, True) ) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) await yaml_collection.async_load( await filter_yaml_data(hass, config.get(DOMAIN, [])) diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 5d55a0b6c1e..aa8c5e08dd6 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -31,7 +31,7 @@ "data": { "server": "Server" }, - "description": "Mehrere Server verf\u00fcgbar, w\u00e4hlen Sie einen aus:", + "description": "Mehrere Server verf\u00fcgbar, w\u00e4hle einen aus:", "title": "Plex-Server ausw\u00e4hlen" }, "start_website_auth": { @@ -43,7 +43,7 @@ "manual_setup": "Manuelle Einrichtung", "token": "Plex Token" }, - "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell.", + "description": "Fahre mit der Autorisierung unter plex.tv fort oder konfiguriere einen Server manuell.", "title": "Plex Server verbinden" } }, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 46b797976ab..d8155d1a43b 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -59,6 +59,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): server_id = config_entry.data[CONF_SERVER_IDENTIFIER] registry = await async_get_registry(hass) + @callback def async_new_media_players(new_entities): _async_add_entities( hass, registry, config_entry, async_add_entities, server_id, new_entities diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 67e94c70f5c..9b519f969e0 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -13,6 +13,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -66,7 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Add the Plugwise (Anna) Thermostate.""" + """Add the Plugwise (Anna) Thermostat.""" api = haanna.Haanna( config[CONF_USERNAME], config[CONF_PASSWORD], @@ -88,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ThermostatDevice(ClimateDevice): - """Representation of an Plugwise thermostat.""" + """Representation of the Plugwise thermostat.""" def __init__(self, api, name, min_temp, max_temp): """Set up the Plugwise API.""" @@ -96,14 +97,18 @@ class ThermostatDevice(ClimateDevice): self._min_temp = min_temp self._max_temp = max_temp self._name = name + self._direct_objects = None self._domain_objects = None self._outdoor_temperature = None self._selected_schema = None + self._last_active_schema = None self._preset_mode = None self._presets = None self._presets_list = None + self._boiler_status = None self._heating_status = None self._cooling_status = None + self._dhw_status = None self._schema_names = None self._schema_status = None self._current_temperature = None @@ -115,8 +120,8 @@ class ThermostatDevice(ClimateDevice): @property def hvac_action(self): - """Return the current action.""" - if self._heating_status: + """Return the current hvac action.""" + if self._heating_status or self._boiler_status or self._dhw_status: return CURRENT_HVAC_HEAT if self._cooling_status: return CURRENT_HVAC_COOL @@ -143,8 +148,10 @@ class ThermostatDevice(ClimateDevice): attributes = {} if self._outdoor_temperature: attributes["outdoor_temperature"] = self._outdoor_temperature - attributes["available_schemas"] = self._schema_names - attributes["selected_schema"] = self._selected_schema + if self._schema_names: + attributes["available_schemas"] = self._schema_names + if self._selected_schema: + attributes["selected_schema"] = self._selected_schema if self._boiler_temperature: attributes["boiler_temperature"] = self._boiler_temperature if self._water_pressure: @@ -162,7 +169,7 @@ class ThermostatDevice(ClimateDevice): @property def hvac_modes(self): """Return the available hvac modes list.""" - if self._heating_status is not None: + if self._heating_status is not None or self._boiler_status is not None: if self._cooling_status is not None: return HVAC_MODES_2 return HVAC_MODES_1 @@ -173,11 +180,11 @@ class ThermostatDevice(ClimateDevice): """Return current active hvac state.""" if self._schema_status: return HVAC_MODE_AUTO - if self._heating_status: + if self._heating_status or self._boiler_status or self._dhw_status: if self._cooling_status: return HVAC_MODE_HEAT_COOL return HVAC_MODE_HEAT - return None + return HVAC_MODE_OFF @property def target_temperature(self): @@ -193,9 +200,9 @@ class ThermostatDevice(ClimateDevice): def preset_mode(self): """Return the active selected schedule-name. - Or return the active preset, or return Temporary in case of a manual change - in the set-temperature with a weekschedule active, - or return Manual in case of a manual change and no weekschedule active. + Or, return the active preset, or return Temporary in case of a manual change + in the set-temperature with a weekschedule active. + Or return Manual in case of a manual change and no weekschedule active. """ if self._presets: presets = self._presets @@ -248,7 +255,7 @@ class ThermostatDevice(ClimateDevice): if hvac_mode == HVAC_MODE_AUTO: schema_mode = "true" self._api.set_schema_state( - self._domain_objects, self._selected_schema, schema_mode + self._domain_objects, self._last_active_schema, schema_mode ) def set_preset_mode(self, preset_mode): @@ -259,16 +266,22 @@ class ThermostatDevice(ClimateDevice): def update(self): """Update the data from the thermostat.""" _LOGGER.debug("Update called") + self._direct_objects = self._api.get_direct_objects() self._domain_objects = self._api.get_domain_objects() self._outdoor_temperature = self._api.get_outdoor_temperature( self._domain_objects ) self._selected_schema = self._api.get_active_schema_name(self._domain_objects) + self._last_active_schema = self._api.get_last_active_schema_name( + self._domain_objects + ) self._preset_mode = self._api.get_current_preset(self._domain_objects) self._presets = self._api.get_presets(self._domain_objects) self._presets_list = list(self._api.get_presets(self._domain_objects)) - self._heating_status = self._api.get_heating_status(self._domain_objects) - self._cooling_status = self._api.get_cooling_status(self._domain_objects) + self._boiler_status = self._api.get_boiler_status(self._direct_objects) + self._heating_status = self._api.get_heating_status(self._direct_objects) + self._cooling_status = self._api.get_cooling_status(self._direct_objects) + self._dhw_status = self._api.get_domestic_hot_water_status(self._direct_objects) self._schema_names = self._api.get_schema_names(self._domain_objects) self._schema_status = self._api.get_schema_state(self._domain_objects) self._current_temperature = self._api.get_current_temperature( diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index ccea2a67ead..5fc3d189b69 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], "codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"], - "requirements": ["haanna==0.13.5"] + "requirements": ["haanna==0.14.1"] } diff --git a/homeassistant/components/point/.translations/de.json b/homeassistant/components/point/.translations/de.json index fe3b781bfac..1072234a744 100644 --- a/homeassistant/components/point/.translations/de.json +++ b/homeassistant/components/point/.translations/de.json @@ -1,29 +1,29 @@ { "config": { "abort": { - "already_setup": "Sie k\u00f6nnen nur ein Point-Konto konfigurieren.", + "already_setup": "Du kannst nur ein Point-Konto konfigurieren.", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", - "no_flows": "Sie m\u00fcssen Point konfigurieren, bevor Sie sich damit authentifizieren k\u00f6nnen. [Bitte lesen Sie die Anweisungen] (https://www.home-assistant.io/components/point/)." + "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Mit Minut erfolgreich f\u00fcr Ihre Point-Ger\u00e4te authentifiziert" }, "error": { - "follow_link": "Bitte folgen Sie dem Link und authentifizieren Sie sich, bevor Sie auf Senden klicken", + "follow_link": "Bitte folgen dem Link und authentifiziere dich, bevor du auf Senden klickst", "no_token": "Nicht mit Minut authentifiziert" }, "step": { "auth": { - "description": "Folgen Sie dem Link unten und Akzeptieren Zugriff auf Ihr Minut-Konto. Kehren Sie dann zur\u00fcck und dr\u00fccken Sie unten auf Senden . \n\n [Link]({authorization_url})", + "description": "Folge dem Link unten und Akzeptiere Zugriff auf dei Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link]({authorization_url})", "title": "Point authentifizieren" }, "user": { "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hlen Sie \u00fcber welchen Authentifizierungsanbieter Sie sich mit Point authentifizieren m\u00f6chten.", + "description": "W\u00e4hle \u00fcber welchen Authentifizierungsanbieter du sich mit Point authentifizieren m\u00f6chtest.", "title": "Authentifizierungsanbieter" } }, diff --git a/homeassistant/components/postnl/__init__.py b/homeassistant/components/postnl/__init__.py deleted file mode 100644 index 96c3212c7a1..00000000000 --- a/homeassistant/components/postnl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The postnl component.""" diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json deleted file mode 100644 index 3394d7b867f..00000000000 --- a/homeassistant/components/postnl/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "postnl", - "name": "PostNL", - "documentation": "https://www.home-assistant.io/integrations/postnl", - "requirements": ["postnl_api==1.2.2"], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py deleted file mode 100644 index 2e1f8176835..00000000000 --- a/homeassistant/components/postnl/sensor.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Sensor for PostNL packages.""" -from datetime import timedelta -import logging - -from postnl_api import PostNL_API, UnauthorizedException -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Information provided by PostNL" - -DEFAULT_NAME = "postnl" - -ICON = "mdi:package-variant-closed" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the PostNL sensor platform.""" - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - name = config.get(CONF_NAME) - - try: - api = PostNL_API(username, password) - - except UnauthorizedException: - _LOGGER.exception("Can't connect to the PostNL webservice") - return - - add_entities([PostNLSensor(api, name)], True) - - -class PostNLSensor(Entity): - """Representation of a PostNL sensor.""" - - def __init__(self, api, name): - """Initialize the PostNL sensor.""" - self._name = name - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION, "shipments": []} - self._state = None - self._api = api - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update device state.""" - shipments = self._api.get_relevant_deliveries() - - self._attributes["shipments"] = [] - - for shipment in shipments: - self._attributes["shipments"].append(vars(shipment)) - - self._state = len(shipments) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 71d56cda18a..c20296a2c18 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -168,7 +168,10 @@ class PrometheusMetrics: return "".join( [ c - if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":" + if c in string.ascii_letters + or c in string.digits + or c == "_" + or c == ":" else f"u{hex(ord(c))}" for c in metric ] diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 246dc2d48ad..58cb50ee304 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -5,6 +5,7 @@ import time from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError +from requests.exceptions import SSLError import voluptuous as vol from homeassistant.const import ( @@ -18,6 +19,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) + DOMAIN = "proxmoxve" PROXMOX_CLIENTS = "proxmox_clients" CONF_REALM = "realm" @@ -94,6 +96,11 @@ def setup(hass, config): "Invalid credentials for proxmox instance %s:%d", host, port ) continue + except SSLError: + _LOGGER.error( + 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + ) + continue hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 4781478eabe..c61d296587c 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/proxmoxve", "dependencies": [], "codeowners": ["@k4ds3"], - "requirements": ["proxmoxer==1.0.3"] + "requirements": ["proxmoxer==1.0.4"] } diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 2053d2f4a80..6f4962a305d 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -4,8 +4,8 @@ "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", - "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", - "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" + "port_987_bind_error": "Konnte sich nicht an Port 987 binden. Weitere Informationen findest du in der [Dokumentation] (https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Geben Sie Ihre PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", + "description": "Geben deine PlayStation 4-Informationen ein. Navigiere f\u00fcr \"PIN\" auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN ein.", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP-Adresse (Leer lassen, wenn automatische Erkennung verwendet wird).", "mode": "Konfigurationsmodus" }, - "description": "W\u00e4hlen Sie den Modus f\u00fcr die Konfiguration aus. Das Feld IP-Adresse kann leer bleiben, wenn die automatische Erkennung ausgew\u00e4hlt wird, da Ger\u00e4te automatisch erkannt werden.", + "description": "W\u00e4hle den Modus f\u00fcr die Konfiguration aus. Das Feld IP-Adresse kann leer bleiben, wenn die automatische Erkennung ausgew\u00e4hlt wird, da Ger\u00e4te automatisch erkannt werden.", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 44523aea85a..17c0eb5838c 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -30,6 +30,8 @@ UDP_PORT = 987 TCP_PORT = 997 PORT_MSG = {UDP_PORT: "port_987_bind_error", TCP_PORT: "port_997_bind_error"} +PIN_LENGTH = 8 + @config_entries.HANDLERS.register(DOMAIN) class PlayStation4FlowHandler(config_entries.ConfigFlow): @@ -143,7 +145,8 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): if user_input is not None: self.region = user_input[CONF_REGION] self.name = user_input[CONF_NAME] - self.pin = str(user_input[CONF_CODE]) + # Assume pin had leading zeros, before coercing to int. + self.pin = str(user_input[CONF_CODE]).zfill(PIN_LENGTH) self.host = user_input[CONF_IP_ADDRESS] is_ready, is_login = await self.hass.async_add_executor_job( @@ -184,7 +187,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): list(regions) ) link_schema[vol.Required(CONF_CODE)] = vol.All( - vol.Strip, vol.Length(min=8, max=8), vol.Coerce(int) + vol.Strip, vol.Length(max=PIN_LENGTH), vol.Coerce(int) ) link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str diff --git a/homeassistant/components/ps4/const.py b/homeassistant/components/ps4/const.py index c0ab470691e..779da61ca48 100644 --- a/homeassistant/components/ps4/const.py +++ b/homeassistant/components/ps4/const.py @@ -8,7 +8,7 @@ DOMAIN = "ps4" GAMES_FILE = ".ps4-games.json" PS4_DATA = "ps4_data" -COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps") +COMMANDS = ("up", "down", "right", "left", "enter", "back", "option", "ps", "ps_hold") # Deprecated used for logger/backwards compatibility from 0.89 REGIONS = ["R1", "R2", "R3", "R4", "R5"] diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 7a52a99e08b..8551b3da3e6 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,7 +3,7 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.0.4"], + "requirements": ["pyps4-2ndscreen==1.0.6"], "dependencies": [], "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index bea90fa2892..33b5c556c7d 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -69,11 +69,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(device_list, update_before_add=True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Not Implemented.""" - pass - - class PS4Device(MediaPlayerDevice): """Representation of a PS4.""" diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index a10c5995d63..ec1adc7641b 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -22,7 +22,7 @@ CONF_TCP_TIMEOUT = "tcp_timeout" DEFAULT_BUFFER_SIZE = 1024 DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4713 +DEFAULT_PORT = 4712 DEFAULT_TCP_TIMEOUT = 3 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 064ad91b6b9..1930ff66f2e 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -67,7 +67,11 @@ class PushoverNotificationService(BaseNotificationService): # Replace the attachment identifier with file object. data[ATTR_ATTACHMENT] = response.content else: - _LOGGER.error("Image not found") + _LOGGER.error( + "Failed to download image %s, response code: %d", + data[ATTR_ATTACHMENT], + response.status_code, + ) # Remove attachment key to send without attachment. del data[ATTR_ATTACHMENT] except requests.exceptions.RequestException as ex_val: diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index d6bc3d6d579..a6beeaa187b 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -15,7 +15,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_HOME, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( @@ -34,8 +37,14 @@ ATTR_FAN_ACTION = "fan_action" CONF_HOLD_TEMP = "hold_temp" +PRESET_HOLIDAY = "holiday" + +PRESET_ALTERNATE = "alternate" + STATE_CIRCULATE = "circulate" +PRESET_MODES = [PRESET_HOME, PRESET_ALTERNATE, PRESET_AWAY, PRESET_HOLIDAY] + OPERATION_LIST = [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] CT30_FAN_OPERATION_LIST = [STATE_ON, HVAC_MODE_AUTO] CT80_FAN_OPERATION_LIST = [STATE_ON, STATE_CIRCULATE, HVAC_MODE_AUTO] @@ -55,6 +64,7 @@ TEMP_MODE_TO_CODE = {v: k for k, v in CODE_TO_TEMP_MODE.items()} # Programmed fan mode (circulate is supported by CT80 models) CODE_TO_FAN_MODE = {0: HVAC_MODE_AUTO, 1: STATE_CIRCULATE, 2: STATE_ON} + FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()} # Active thermostat state (is it heating or cooling?). In the future @@ -65,6 +75,10 @@ CODE_TO_TEMP_STATE = {0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVA # future this should probably made into a binary sensor for the fan. CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON} +PRESET_MODE_TO_CODE = {"home": 0, "alternate": 1, "away": 2, "holiday": 3} + +CODE_TO_PRESET_MODE = {0: "home", 1: "alternate", 2: "away", 3: "holiday"} + def round_temp(temperature): """Round a temperature to the resolution of the thermostat. @@ -82,7 +96,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE def setup_platform(hass, config, add_entities, discovery_info=None): @@ -128,6 +143,9 @@ class RadioThermostat(ClimateDevice): self._hold_temp = hold_temp self._hold_set = False self._prev_temp = None + self._preset_mode = None + self._program_mode = None + self._is_away = False # Fan circulate mode is only supported by the CT80 models. self._is_model_ct80 = isinstance(self.device, radiotherm.thermostat.CT80) @@ -216,6 +234,23 @@ class RadioThermostat(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temperature + @property + def preset_mode(self): + """Return the current preset mode, e.g., home, away, temp.""" + if self._program_mode == 0: + return PRESET_HOME + if self._program_mode == 1: + return PRESET_ALTERNATE + if self._program_mode == 2: + return PRESET_AWAY + if self._program_mode == 3: + return PRESET_HOLIDAY + + @property + def preset_modes(self): + """Return a list of available preset modes.""" + return PRESET_MODES + def update(self): """Update and validate the data from the thermostat.""" # Radio thermostats are very slow, and sometimes don't respond @@ -262,6 +297,8 @@ class RadioThermostat(ClimateDevice): self._fstate = CODE_TO_FAN_STATE[data["fstate"]] self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] + self._program_mode = data["program_mode"] + self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] self._current_operation = self._tmode if self._tmode == HVAC_MODE_COOL: @@ -327,3 +364,12 @@ class RadioThermostat(ClimateDevice): self.device.t_cool = self._target_temperature elif hvac_mode == HVAC_MODE_HEAT: self.device.t_heat = self._target_temperature + + def set_preset_mode(self, preset_mode): + """Set Preset mode (Home, Alternate, Away, Holiday).""" + if preset_mode in (PRESET_MODES): + self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] + else: + _LOGGER.error( + "preset_mode %s not in PRESET_MODES", preset_mode, + ) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 5dffecb0488..53f33f68eb9 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -3,23 +3,20 @@ import asyncio from datetime import timedelta import logging -from regenmaschine import login +from regenmaschine import Client from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_BINARY_SENSORS, CONF_IP_ADDRESS, - CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, - CONF_SENSORS, CONF_SSL, - CONF_SWITCHES, ) +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -30,23 +27,25 @@ from homeassistant.helpers.service import verify_domain_control from .config_flow import configured_instances from .const import ( DATA_CLIENT, + DATA_PROGRAMS, + DATA_PROVISION_SETTINGS, + DATA_RESTRICTIONS_CURRENT, + DATA_RESTRICTIONS_UNIVERSAL, + DATA_ZONES, + DATA_ZONES_DETAILS, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN, - PROVISION_SETTINGS, - RESTRICTIONS_CURRENT, - RESTRICTIONS_UNIVERSAL, + PROGRAM_UPDATE_TOPIC, + SENSOR_UPDATE_TOPIC, + ZONE_UPDATE_TOPIC, ) _LOGGER = logging.getLogger(__name__) DATA_LISTENER = "listener" -PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update" -SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update" -ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update" - CONF_CONTROLLERS = "controllers" CONF_PROGRAM_ID = "program_id" CONF_SECONDS = "seconds" @@ -57,82 +56,6 @@ DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" DEFAULT_ZONE_RUN = 60 * 10 -TYPE_FLOW_SENSOR = "flow_sensor" -TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter" -TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters" -TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index" -TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks" -TYPE_FREEZE = "freeze" -TYPE_FREEZE_PROTECTION = "freeze_protection" -TYPE_FREEZE_TEMP = "freeze_protect_temp" -TYPE_HOT_DAYS = "extra_water_on_hot_days" -TYPE_HOURLY = "hourly" -TYPE_MONTH = "month" -TYPE_RAINDELAY = "raindelay" -TYPE_RAINSENSOR = "rainsensor" -TYPE_WEEKDAY = "weekday" - -BINARY_SENSORS = { - TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump"), - TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel"), - TYPE_FREEZE_PROTECTION: ("Freeze Protection", "mdi:weather-snowy"), - TYPE_HOT_DAYS: ("Extra Water on Hot Days", "mdi:thermometer-lines"), - TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel"), - TYPE_MONTH: ("Month Restrictions", "mdi:cancel"), - TYPE_RAINDELAY: ("Rain Delay Restrictions", "mdi:cancel"), - TYPE_RAINSENSOR: ("Rain Sensor Restrictions", "mdi:cancel"), - TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel"), -} - -SENSORS = { - TYPE_FLOW_SENSOR_CLICK_M3: ( - "Flow Sensor Clicks", - "mdi:water-pump", - "clicks/m^3", - None, - ), - TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( - "Flow Sensor Consumed Liters", - "mdi:water-pump", - "liter", - None, - ), - TYPE_FLOW_SENSOR_START_INDEX: ( - "Flow Sensor Start Index", - "mdi:water-pump", - "index", - None, - ), - TYPE_FLOW_SENSOR_WATERING_CLICKS: ( - "Flow Sensor Clicks", - "mdi:water-pump", - "clicks", - None, - ), - TYPE_FREEZE_TEMP: ( - "Freeze Protect Temperature", - "mdi:thermometer", - "°C", - "temperature", - ), -} - -BINARY_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ) - } -) - -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ) - } -) - SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int}) SERVICE_ALTER_ZONE = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) @@ -156,9 +79,6 @@ SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) -SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int}) - - CONTROLLER_SCHEMA = vol.Schema( { vol.Required(CONF_IP_ADDRESS): cv.string, @@ -166,13 +86,10 @@ CONTROLLER_SCHEMA = vol.Schema( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA, - vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, - vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int, } ) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -213,34 +130,36 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up RainMachine as config entry.""" - _verify_domain_control = verify_domain_control(hass, DOMAIN) websession = aiohttp_client.async_get_clientsession(hass) + client = Client(websession) try: - client = await login( + await client.load_local( config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD], - websession, port=config_entry.data[CONF_PORT], ssl=config_entry.data[CONF_SSL], ) - rainmachine = RainMachine( - client, - config_entry.data.get(CONF_BINARY_SENSORS, {}).get( - CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS) - ), - config_entry.data.get(CONF_SENSORS, {}).get( - CONF_MONITORED_CONDITIONS, list(SENSORS) - ), - config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), - ) - await rainmachine.async_update() except RainMachineError as err: _LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady + else: + # regenmaschine can load multiple controllers at once, but we only grab the one + # we loaded above: + controller = next(iter(client.controllers.values())) + rainmachine = RainMachine( + hass, + controller, + config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), + config_entry.data[CONF_SCAN_INTERVAL], + ) + + # Update the data object, which at this point (prior to any sensors registering + # "interest" in the API), will focus on grabbing the latest program and zone data: + await rainmachine.async_update() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine for component in ("binary_sensor", "sensor", "switch"): @@ -248,83 +167,73 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_forward_entry_setup(config_entry, component) ) - async def refresh(event_time): - """Refresh RainMachine sensor data.""" - _LOGGER.debug("Updating RainMachine sensor data") - await rainmachine.async_update() - async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) - - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) - ) - @_verify_domain_control async def disable_program(call): """Disable a program.""" - await rainmachine.client.programs.disable(call.data[CONF_PROGRAM_ID]) - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.programs.disable(call.data[CONF_PROGRAM_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def disable_zone(call): """Disable a zone.""" - await rainmachine.client.zones.disable(call.data[CONF_ZONE_ID]) - async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) + await rainmachine.controller.zones.disable(call.data[CONF_ZONE_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def enable_program(call): """Enable a program.""" - await rainmachine.client.programs.enable(call.data[CONF_PROGRAM_ID]) - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.programs.enable(call.data[CONF_PROGRAM_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def enable_zone(call): """Enable a zone.""" - await rainmachine.client.zones.enable(call.data[CONF_ZONE_ID]) - async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) + await rainmachine.controller.zones.enable(call.data[CONF_ZONE_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def pause_watering(call): """Pause watering for a set number of seconds.""" - await rainmachine.client.watering.pause_all(call.data[CONF_SECONDS]) - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.watering.pause_all(call.data[CONF_SECONDS]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def start_program(call): """Start a particular program.""" - await rainmachine.client.programs.start(call.data[CONF_PROGRAM_ID]) - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.programs.start(call.data[CONF_PROGRAM_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def start_zone(call): """Start a particular zone for a certain amount of time.""" - await rainmachine.client.zones.start( + await rainmachine.controller.zones.start( call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME] ) - async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def stop_all(call): """Stop all watering.""" - await rainmachine.client.watering.stop_all() - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.watering.stop_all() + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def stop_program(call): """Stop a program.""" - await rainmachine.client.programs.stop(call.data[CONF_PROGRAM_ID]) - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.programs.stop(call.data[CONF_PROGRAM_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def stop_zone(call): """Stop a zone.""" - await rainmachine.client.zones.stop(call.data[CONF_ZONE_ID]) - async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) + await rainmachine.controller.zones.stop(call.data[CONF_ZONE_ID]) + await rainmachine.async_update_programs_and_zones() @_verify_domain_control async def unpause_watering(call): """Unpause watering.""" - await rainmachine.client.watering.unpause_all() - async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) + await rainmachine.controller.watering.unpause_all() + await rainmachine.async_update_programs_and_zones() for service, method, schema in [ ("disable_program", disable_program, SERVICE_ALTER_PROGRAM), @@ -364,64 +273,135 @@ async def async_unload_entry(hass, config_entry): class RainMachine: """Define a generic RainMachine object.""" - def __init__( - self, client, binary_sensor_conditions, sensor_conditions, default_zone_runtime - ): + def __init__(self, hass, controller, default_zone_runtime, scan_interval): """Initialize.""" - self.binary_sensor_conditions = binary_sensor_conditions - self.client = client + self._async_cancel_time_interval_listener = None + self._scan_interval_seconds = scan_interval + self.controller = controller self.data = {} self.default_zone_runtime = default_zone_runtime - self.device_mac = self.client.mac - self.sensor_conditions = sensor_conditions + self.device_mac = controller.mac + self.hass = hass + + self._api_category_count = { + DATA_PROVISION_SETTINGS: 0, + DATA_RESTRICTIONS_CURRENT: 0, + DATA_RESTRICTIONS_UNIVERSAL: 0, + } + self._api_category_locks = { + DATA_PROVISION_SETTINGS: asyncio.Lock(), + DATA_RESTRICTIONS_CURRENT: asyncio.Lock(), + DATA_RESTRICTIONS_UNIVERSAL: asyncio.Lock(), + } + + async def _async_update_listener_action(self, now): + """Define an async_track_time_interval action to update data.""" + await self.async_update() + + @callback + def async_deregister_sensor_api_interest(self, api_category): + """Decrement the number of entities with data needs from an API category.""" + # If this deregistration should leave us with no registration at all, remove the + # time interval: + if sum(self._api_category_count.values()) == 0: + if self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener() + self._async_cancel_time_interval_listener = None + return + + self._api_category_count[api_category] -= 1 + + async def async_fetch_from_api(self, api_category): + """Execute the appropriate coroutine to fetch particular data from the API.""" + if api_category == DATA_PROGRAMS: + data = await self.controller.programs.all(include_inactive=True) + elif api_category == DATA_PROVISION_SETTINGS: + data = await self.controller.provisioning.settings() + elif api_category == DATA_RESTRICTIONS_CURRENT: + data = await self.controller.restrictions.current() + elif api_category == DATA_RESTRICTIONS_UNIVERSAL: + data = await self.controller.restrictions.universal() + elif api_category == DATA_ZONES: + data = await self.controller.zones.all(include_inactive=True) + elif api_category == DATA_ZONES_DETAILS: + # This API call needs to be separate from the DATA_ZONES one above because, + # maddeningly, the DATA_ZONES_DETAILS API call doesn't include the current + # state of the zone: + data = await self.controller.zones.all(details=True, include_inactive=True) + + self.data[api_category] = data + + async def async_register_sensor_api_interest(self, api_category): + """Increment the number of entities with data needs from an API category.""" + # If this is the first registration we have, start a time interval: + if not self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener = async_track_time_interval( + self.hass, + self._async_update_listener_action, + timedelta(seconds=self._scan_interval_seconds), + ) + + self._api_category_count[api_category] += 1 + + # If a sensor registers interest in a particular API call and the data doesn't + # exist for it yet, make the API call and grab the data: + async with self._api_category_locks[api_category]: + if api_category not in self.data: + await self.async_fetch_from_api(api_category) async def async_update(self): + """Update all RainMachine data.""" + tasks = [self.async_update_programs_and_zones(), self.async_update_sensors()] + await asyncio.gather(*tasks) + + async def async_update_sensors(self): """Update sensor/binary sensor data.""" + _LOGGER.debug("Updating sensor data for RainMachine") + # Fetch an API category if there is at least one interested entity: tasks = {} - - if TYPE_FLOW_SENSOR in self.binary_sensor_conditions or any( - c in self.sensor_conditions - for c in ( - TYPE_FLOW_SENSOR_CLICK_M3, - TYPE_FLOW_SENSOR_CONSUMED_LITERS, - TYPE_FLOW_SENSOR_START_INDEX, - TYPE_FLOW_SENSOR_WATERING_CLICKS, - ) - ): - tasks[PROVISION_SETTINGS] = self.client.provisioning.settings() - - if any( - c in self.binary_sensor_conditions - for c in ( - TYPE_FREEZE, - TYPE_HOURLY, - TYPE_MONTH, - TYPE_RAINDELAY, - TYPE_RAINSENSOR, - TYPE_WEEKDAY, - ) - ): - tasks[RESTRICTIONS_CURRENT] = self.client.restrictions.current() - - if ( - any( - c in self.binary_sensor_conditions - for c in (TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS) - ) - or TYPE_FREEZE_TEMP in self.sensor_conditions - ): - tasks[RESTRICTIONS_UNIVERSAL] = self.client.restrictions.universal() + for category, count in self._api_category_count.items(): + if count == 0: + continue + tasks[category] = self.async_fetch_from_api(category) results = await asyncio.gather(*tasks.values(), return_exceptions=True) - for operation, result in zip(tasks, results): + for api_category, result in zip(tasks, results): if isinstance(result, RainMachineError): _LOGGER.error( - "There was an error while updating %s: %s", operation, result + "There was an error while updating %s: %s", api_category, result ) continue - self.data[operation] = result + async_dispatcher_send(self.hass, SENSOR_UPDATE_TOPIC) + + async def async_update_programs_and_zones(self): + """Update program and zone data. + + Program and zone updates always go together because of how linked they are: + programs affect zones and certain combinations of zones affect programs. + + Note that this call does not take into account interested entities when making + the API calls; we make the reasonable assumption that switches will always be + enabled. + """ + _LOGGER.debug("Updating program and zone data for RainMachine") + + tasks = { + DATA_PROGRAMS: self.async_fetch_from_api(DATA_PROGRAMS), + DATA_ZONES: self.async_fetch_from_api(DATA_ZONES), + DATA_ZONES_DETAILS: self.async_fetch_from_api(DATA_ZONES_DETAILS), + } + + results = await asyncio.gather(*tasks.values(), return_exceptions=True) + for api_category, result in zip(tasks, results): + if isinstance(result, RainMachineError): + _LOGGER.error( + "There was an error while updating %s: %s", api_category, result + ) + + async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + async_dispatcher_send(self.hass, ZONE_UPDATE_TOPIC) class RainMachineEntity(Entity): @@ -444,14 +424,14 @@ class RainMachineEntity(Entity): def device_info(self): """Return device registry information for this entity.""" return { - "identifiers": {(DOMAIN, self.rainmachine.client.mac)}, - "name": self.rainmachine.client.name, + "identifiers": {(DOMAIN, self.rainmachine.controller.mac)}, + "name": self.rainmachine.controller.name, "manufacturer": "RainMachine", "model": "Version {0} (API: {1})".format( - self.rainmachine.client.hardware_version, - self.rainmachine.client.api_version, + self.rainmachine.controller.hardware_version, + self.rainmachine.controller.api_version, ), - "sw_version": self.rainmachine.client.software_version, + "sw_version": self.rainmachine.controller.software_version, } @property @@ -464,6 +444,16 @@ class RainMachineEntity(Entity): """Return the name of the entity.""" return self._name + @property + def should_poll(self): + """Disable polling.""" + return False + + @callback + def _update_state(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" for handler in self._dispatcher_handlers: diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 1fe98482211..34b8de80b88 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -2,63 +2,110 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( - BINARY_SENSORS, +from . import RainMachineEntity +from .const import ( DATA_CLIENT, + DATA_PROVISION_SETTINGS, + DATA_RESTRICTIONS_CURRENT, + DATA_RESTRICTIONS_UNIVERSAL, DOMAIN as RAINMACHINE_DOMAIN, - PROVISION_SETTINGS, - RESTRICTIONS_CURRENT, - RESTRICTIONS_UNIVERSAL, SENSOR_UPDATE_TOPIC, - TYPE_FLOW_SENSOR, - TYPE_FREEZE, - TYPE_FREEZE_PROTECTION, - TYPE_HOT_DAYS, - TYPE_HOURLY, - TYPE_MONTH, - TYPE_RAINDELAY, - TYPE_RAINSENSOR, - TYPE_WEEKDAY, - RainMachineEntity, ) _LOGGER = logging.getLogger(__name__) +TYPE_FLOW_SENSOR = "flow_sensor" +TYPE_FREEZE = "freeze" +TYPE_FREEZE_PROTECTION = "freeze_protection" +TYPE_HOT_DAYS = "extra_water_on_hot_days" +TYPE_HOURLY = "hourly" +TYPE_MONTH = "month" +TYPE_RAINDELAY = "raindelay" +TYPE_RAINSENSOR = "rainsensor" +TYPE_WEEKDAY = "weekday" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine binary sensors based on the old way.""" - pass +BINARY_SENSORS = { + TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, DATA_PROVISION_SETTINGS), + TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, DATA_RESTRICTIONS_CURRENT), + TYPE_FREEZE_PROTECTION: ( + "Freeze Protection", + "mdi:weather-snowy", + True, + DATA_RESTRICTIONS_UNIVERSAL, + ), + TYPE_HOT_DAYS: ( + "Extra Water on Hot Days", + "mdi:thermometer-lines", + True, + DATA_RESTRICTIONS_UNIVERSAL, + ), + TYPE_HOURLY: ( + "Hourly Restrictions", + "mdi:cancel", + False, + DATA_RESTRICTIONS_CURRENT, + ), + TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, DATA_RESTRICTIONS_CURRENT), + TYPE_RAINDELAY: ( + "Rain Delay Restrictions", + "mdi:cancel", + False, + DATA_RESTRICTIONS_CURRENT, + ), + TYPE_RAINSENSOR: ( + "Rain Sensor Restrictions", + "mdi:cancel", + False, + DATA_RESTRICTIONS_CURRENT, + ), + TYPE_WEEKDAY: ( + "Weekday Restrictions", + "mdi:cancel", + False, + DATA_RESTRICTIONS_CURRENT, + ), +} async def async_setup_entry(hass, entry, async_add_entities): """Set up RainMachine binary sensors based on a config entry.""" rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] - - binary_sensors = [] - for sensor_type in rainmachine.binary_sensor_conditions: - name, icon = BINARY_SENSORS[sensor_type] - binary_sensors.append( - RainMachineBinarySensor(rainmachine, sensor_type, name, icon) - ) - - async_add_entities(binary_sensors, True) + async_add_entities( + [ + RainMachineBinarySensor( + rainmachine, sensor_type, name, icon, enabled_by_default, api_category + ) + for ( + sensor_type, + (name, icon, enabled_by_default, api_category), + ) in BINARY_SENSORS.items() + ], + ) class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): """A sensor implementation for raincloud device.""" - def __init__(self, rainmachine, sensor_type, name, icon): + def __init__( + self, rainmachine, sensor_type, name, icon, enabled_by_default, api_category + ): """Initialize the sensor.""" super().__init__(rainmachine) + self._api_category = api_category + self._enabled_by_default = enabled_by_default self._icon = icon self._name = name self._sensor_type = sensor_type self._state = None + @property + def entity_registry_enabled_default(self): + """Determine whether an entity is enabled by default.""" + return self._enabled_by_default + @property def icon(self) -> str: """Return the icon.""" @@ -69,11 +116,6 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): """Return the status of the sensor.""" return self._state - @property - def should_poll(self): - """Disable polling.""" - return False - @property def unique_id(self) -> str: """Return a unique, Home Assistant friendly identifier for this entity.""" @@ -83,39 +125,40 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): async def async_added_to_hass(self): """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - self._dispatcher_handlers.append( - async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update) + async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state) ) + await self.rainmachine.async_register_sensor_api_interest(self._api_category) + await self.async_update() async def async_update(self): """Update the state.""" if self._sensor_type == TYPE_FLOW_SENSOR: - self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( "useFlowSensor" ) elif self._sensor_type == TYPE_FREEZE: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["freeze"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["freeze"] elif self._sensor_type == TYPE_FREEZE_PROTECTION: - self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][ + self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][ "freezeProtectEnabled" ] elif self._sensor_type == TYPE_HOT_DAYS: - self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][ + self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][ "hotDaysExtraWatering" ] elif self._sensor_type == TYPE_HOURLY: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["hourly"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["hourly"] elif self._sensor_type == TYPE_MONTH: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["month"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["month"] elif self._sensor_type == TYPE_RAINDELAY: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["rainDelay"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainDelay"] elif self._sensor_type == TYPE_RAINSENSOR: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["rainSensor"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainSensor"] elif self._sensor_type == TYPE_WEEKDAY: - self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["weekDay"] + self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["weekDay"] + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listeners and deregister API interest.""" + super().async_will_remove_from_hass() + self.rainmachine.async_deregister_sensor_api_interest(self._api_category) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index c3612645a8f..b912f8d95ef 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -7,13 +7,17 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "rainmachine" DATA_CLIENT = "client" +DATA_PROGRAMS = "programs" +DATA_PROVISION_SETTINGS = "provision.settings" +DATA_RESTRICTIONS_CURRENT = "restrictions.current" +DATA_RESTRICTIONS_UNIVERSAL = "restrictions.universal" +DATA_ZONES = "zones" +DATA_ZONES_DETAILS = "zones_details" DEFAULT_PORT = 8080 DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) DEFAULT_SSL = True -PROVISION_SETTINGS = "provision.settings" -RESTRICTIONS_CURRENT = "restrictions.current" -RESTRICTIONS_UNIVERSAL = "restrictions.universal" - -TOPIC_UPDATE = "update_{0}" +PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update" +SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update" +ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update" diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 399e86b7db1..8487628a32b 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,70 +1,128 @@ """This platform provides support for sensor data from RainMachine.""" import logging -from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( +from . import RainMachineEntity +from .const import ( DATA_CLIENT, + DATA_PROVISION_SETTINGS, + DATA_RESTRICTIONS_UNIVERSAL, DOMAIN as RAINMACHINE_DOMAIN, - PROVISION_SETTINGS, - RESTRICTIONS_UNIVERSAL, SENSOR_UPDATE_TOPIC, - SENSORS, - TYPE_FLOW_SENSOR_CLICK_M3, - TYPE_FLOW_SENSOR_CONSUMED_LITERS, - TYPE_FLOW_SENSOR_START_INDEX, - TYPE_FLOW_SENSOR_WATERING_CLICKS, - TYPE_FREEZE_TEMP, - RainMachineEntity, ) _LOGGER = logging.getLogger(__name__) +TYPE_FLOW_SENSOR_CLICK_M3 = "flow_sensor_clicks_cubic_meter" +TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters" +TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index" +TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks" +TYPE_FREEZE_TEMP = "freeze_protect_temp" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine sensors based on the old way.""" - pass +SENSORS = { + TYPE_FLOW_SENSOR_CLICK_M3: ( + "Flow Sensor Clicks", + "mdi:water-pump", + "clicks/m^3", + None, + False, + DATA_PROVISION_SETTINGS, + ), + TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( + "Flow Sensor Consumed Liters", + "mdi:water-pump", + "liter", + None, + False, + DATA_PROVISION_SETTINGS, + ), + TYPE_FLOW_SENSOR_START_INDEX: ( + "Flow Sensor Start Index", + "mdi:water-pump", + "index", + None, + False, + DATA_PROVISION_SETTINGS, + ), + TYPE_FLOW_SENSOR_WATERING_CLICKS: ( + "Flow Sensor Clicks", + "mdi:water-pump", + "clicks", + None, + False, + DATA_PROVISION_SETTINGS, + ), + TYPE_FREEZE_TEMP: ( + "Freeze Protect Temperature", + "mdi:thermometer", + "°C", + "temperature", + True, + DATA_RESTRICTIONS_UNIVERSAL, + ), +} async def async_setup_entry(hass, entry, async_add_entities): """Set up RainMachine sensors based on a config entry.""" rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] - - sensors = [] - for sensor_type in rainmachine.sensor_conditions: - name, icon, unit, device_class = SENSORS[sensor_type] - sensors.append( - RainMachineSensor(rainmachine, sensor_type, name, icon, unit, device_class) - ) - - async_add_entities(sensors, True) + async_add_entities( + [ + RainMachineSensor( + rainmachine, + sensor_type, + name, + icon, + unit, + device_class, + enabled_by_default, + api_category, + ) + for ( + sensor_type, + (name, icon, unit, device_class, enabled_by_default, api_category), + ) in SENSORS.items() + ], + ) class RainMachineSensor(RainMachineEntity): """A sensor implementation for raincloud device.""" - def __init__(self, rainmachine, sensor_type, name, icon, unit, device_class): + def __init__( + self, + rainmachine, + sensor_type, + name, + icon, + unit, + device_class, + enabled_by_default, + api_category, + ): """Initialize.""" super().__init__(rainmachine) + self._api_category = api_category self._device_class = device_class + self._enabled_by_default = enabled_by_default self._icon = icon self._name = name self._sensor_type = sensor_type self._state = None self._unit = unit + @property + def entity_registry_enabled_default(self): + """Determine whether an entity is enabled by default.""" + return self._enabled_by_default + @property def icon(self) -> str: """Return the icon.""" return self._icon - @property - def should_poll(self): - """Disable polling.""" - return False - @property def state(self) -> str: """Return the name of the entity.""" @@ -84,43 +142,44 @@ class RainMachineSensor(RainMachineEntity): async def async_added_to_hass(self): """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - self._dispatcher_handlers.append( - async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update) + async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state) ) + await self.rainmachine.async_register_sensor_api_interest(self._api_category) + await self.async_update() async def async_update(self): """Update the sensor's state.""" if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3: - self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( "flowSensorClicksPerCubicMeter" ) elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: - clicks = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + clicks = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( "flowSensorWateringClicks" ) - clicks_per_m3 = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( - "flowSensorClicksPerCubicMeter" - ) + clicks_per_m3 = self.rainmachine.data[DATA_PROVISION_SETTINGS][ + "system" + ].get("flowSensorClicksPerCubicMeter") if clicks and clicks_per_m3: self._state = (clicks * 1000) / clicks_per_m3 else: self._state = None elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX: - self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( "flowSensorStartIndex" ) elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( "flowSensorWateringClicks" ) elif self._sensor_type == TYPE_FREEZE_TEMP: - self._state = self.rainmachine.data[RESTRICTIONS_UNIVERSAL][ + self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][ "freezeProtectTemp" ] + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listeners and deregister API interest.""" + super().async_will_remove_from_hass() + self.rainmachine.async_deregister_sensor_api_interest(self._api_category) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 36c5eefb3d6..2bf63dbf495 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -6,18 +6,17 @@ from regenmaschine.errors import RequestError from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_ID -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( +from . import RainMachineEntity +from .const import ( DATA_CLIENT, + DATA_PROGRAMS, + DATA_ZONES, + DATA_ZONES_DETAILS, DOMAIN as RAINMACHINE_DOMAIN, PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, - RainMachineEntity, ) _LOGGER = logging.getLogger(__name__) @@ -94,10 +93,8 @@ VEGETATION_MAP = { 99: "Other", } - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine switches sensor based on the old way.""" - pass +SWITCH_TYPE_PROGRAM = "program" +SWITCH_TYPE_ZONE = "zone" async def async_setup_entry(hass, entry, async_add_entities): @@ -105,16 +102,10 @@ async def async_setup_entry(hass, entry, async_add_entities): rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] entities = [] - - programs = await rainmachine.client.programs.all(include_inactive=True) - for program in programs: + for program in rainmachine.data[DATA_PROGRAMS]: entities.append(RainMachineProgram(rainmachine, program)) - - zones = await rainmachine.client.zones.all(include_inactive=True) - for zone in zones: - entities.append( - RainMachineZone(rainmachine, zone, rainmachine.default_zone_runtime) - ) + for zone in rainmachine.data[DATA_ZONES]: + entities.append(RainMachineZone(rainmachine, zone)) async_add_entities(entities, True) @@ -122,25 +113,31 @@ async def async_setup_entry(hass, entry, async_add_entities): class RainMachineSwitch(RainMachineEntity, SwitchDevice): """A class to represent a generic RainMachine switch.""" - def __init__(self, rainmachine, switch_type, obj): + def __init__(self, rainmachine, switch_data): """Initialize a generic RainMachine switch.""" super().__init__(rainmachine) - self._name = obj["name"] - self._obj = obj - self._rainmachine_entity_id = obj["uid"] - self._switch_type = switch_type + self._is_on = False + self._name = switch_data["name"] + self._switch_data = switch_data + self._rainmachine_entity_id = switch_data["uid"] + self._switch_type = None @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._obj.get("active")) + return self._switch_data["active"] @property def icon(self) -> str: """Return the icon.""" return "mdi:water" + @property + def is_on(self) -> bool: + """Return whether the program is running.""" + return self._is_on + @property def unique_id(self) -> str: """Return a unique, Home Assistant friendly identifier for this entity.""" @@ -150,173 +147,156 @@ class RainMachineSwitch(RainMachineEntity, SwitchDevice): self._rainmachine_entity_id, ) - @callback - def _program_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) + async def _async_run_switch_coroutine(self, api_coro) -> None: + """Run a coroutine to toggle the switch.""" + try: + resp = await api_coro + except RequestError as err: + _LOGGER.error( + 'Error while toggling %s "%s": %s', + self._switch_type, + self.unique_id, + err, + ) + return + + if resp["statusCode"] != 0: + _LOGGER.error( + 'Error while toggling %s "%s": %s', + self._switch_type, + self.unique_id, + resp["message"], + ) + return + + self.hass.async_create_task(self.rainmachine.async_update_programs_and_zones()) class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" - def __init__(self, rainmachine, obj): + def __init__(self, rainmachine, switch_data): """Initialize a generic RainMachine switch.""" - super().__init__(rainmachine, "program", obj) - - @property - def is_on(self) -> bool: - """Return whether the program is running.""" - return bool(self._obj.get("status")) + super().__init__(rainmachine, switch_data) + self._switch_type = SWITCH_TYPE_PROGRAM @property def zones(self) -> list: """Return a list of active zones associated with this program.""" - return [z for z in self._obj["wateringTimes"] if z["active"]] + return [z for z in self._switch_data["wateringTimes"] if z["active"]] async def async_added_to_hass(self): """Register callbacks.""" self._dispatcher_handlers.append( async_dispatcher_connect( - self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated + self.hass, PROGRAM_UPDATE_TOPIC, self._update_state ) ) async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - - try: - await self.rainmachine.client.programs.stop(self._rainmachine_entity_id) - async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RequestError as err: - _LOGGER.error( - 'Unable to turn off program "%s": %s', self.unique_id, str(err) - ) + await self._async_run_switch_coroutine( + self.rainmachine.controller.programs.stop(self._rainmachine_entity_id) + ) async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - - try: - await self.rainmachine.client.programs.start(self._rainmachine_entity_id) - async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RequestError as err: - _LOGGER.error( - 'Unable to turn on program "%s": %s', self.unique_id, str(err) - ) + await self._async_run_switch_coroutine( + self.rainmachine.controller.programs.start(self._rainmachine_entity_id) + ) async def async_update(self) -> None: """Update info for the program.""" + [self._switch_data] = [ + p + for p in self.rainmachine.data[DATA_PROGRAMS] + if p["uid"] == self._rainmachine_entity_id + ] + + self._is_on = bool(self._switch_data["status"]) try: - self._obj = await self.rainmachine.client.programs.get( - self._rainmachine_entity_id - ) + next_run = datetime.strptime( + "{0} {1}".format( + self._switch_data["nextRun"], self._switch_data["startTime"] + ), + "%Y-%m-%d %H:%M", + ).isoformat() + except ValueError: + next_run = None - try: - next_run = datetime.strptime( - "{0} {1}".format(self._obj["nextRun"], self._obj["startTime"]), - "%Y-%m-%d %H:%M", - ).isoformat() - except ValueError: - next_run = None - - self._attrs.update( - { - ATTR_ID: self._obj["uid"], - ATTR_NEXT_RUN: next_run, - ATTR_SOAK: self._obj.get("soak"), - ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get("status")], - ATTR_ZONES: ", ".join(z["name"] for z in self.zones), - } - ) - except RequestError as err: - _LOGGER.error( - 'Unable to update info for program "%s": %s', self.unique_id, str(err) - ) + self._attrs.update( + { + ATTR_ID: self._switch_data["uid"], + ATTR_NEXT_RUN: next_run, + ATTR_SOAK: self._switch_data.get("soak"), + ATTR_STATUS: PROGRAM_STATUS_MAP[self._switch_data["status"]], + ATTR_ZONES: ", ".join(z["name"] for z in self.zones), + } + ) class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" - def __init__(self, rainmachine, obj, zone_run_time): + def __init__(self, rainmachine, switch_data): """Initialize a RainMachine zone.""" - super().__init__(rainmachine, "zone", obj) - - self._properties_json = {} - self._run_time = zone_run_time - - @property - def is_on(self) -> bool: - """Return whether the zone is running.""" - return bool(self._obj.get("state")) + super().__init__(rainmachine, switch_data) + self._switch_type = SWITCH_TYPE_ZONE async def async_added_to_hass(self): """Register callbacks.""" self._dispatcher_handlers.append( async_dispatcher_connect( - self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated + self.hass, PROGRAM_UPDATE_TOPIC, self._update_state ) ) self._dispatcher_handlers.append( - async_dispatcher_connect( - self.hass, ZONE_UPDATE_TOPIC, self._program_updated - ) + async_dispatcher_connect(self.hass, ZONE_UPDATE_TOPIC, self._update_state) ) async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - - try: - await self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except RequestError as err: - _LOGGER.error('Unable to turn off zone "%s": %s', self.unique_id, str(err)) + await self._async_run_switch_coroutine( + self.rainmachine.controller.zones.stop(self._rainmachine_entity_id) + ) async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - - try: - await self.rainmachine.client.zones.start( - self._rainmachine_entity_id, self._run_time + await self._async_run_switch_coroutine( + self.rainmachine.controller.zones.start( + self._rainmachine_entity_id, self.rainmachine.default_zone_runtime ) - except RequestError as err: - _LOGGER.error('Unable to turn on zone "%s": %s', self.unique_id, str(err)) + ) async def async_update(self) -> None: """Update info for the zone.""" + [self._switch_data] = [ + z + for z in self.rainmachine.data[DATA_ZONES] + if z["uid"] == self._rainmachine_entity_id + ] + [details] = [ + z + for z in self.rainmachine.data[DATA_ZONES_DETAILS] + if z["uid"] == self._rainmachine_entity_id + ] - try: - self._obj = await self.rainmachine.client.zones.get( - self._rainmachine_entity_id - ) + self._is_on = bool(self._switch_data["state"]) - self._properties_json = await self.rainmachine.client.zones.get( - self._rainmachine_entity_id, details=True - ) - - self._attrs.update( - { - ATTR_ID: self._obj["uid"], - ATTR_AREA: self._properties_json.get("waterSense").get("area"), - ATTR_CURRENT_CYCLE: self._obj.get("cycle"), - ATTR_FIELD_CAPACITY: self._properties_json.get("waterSense").get( - "fieldCapacity" - ), - ATTR_NO_CYCLES: self._obj.get("noOfCycles"), - ATTR_PRECIP_RATE: self._properties_json.get("waterSense").get( - "precipitationRate" - ), - ATTR_RESTRICTIONS: self._obj.get("restriction"), - ATTR_SLOPE: SLOPE_TYPE_MAP.get(self._properties_json.get("slope")), - ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._properties_json.get("sun")), - ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get( - self._properties_json.get("group_id") - ), - ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get( - self._properties_json.get("sun") - ), - ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._obj.get("type")), - } - ) - except RequestError as err: - _LOGGER.error( - 'Unable to update info for zone "%s": %s', self.unique_id, str(err) - ) + self._attrs.update( + { + ATTR_ID: self._switch_data["uid"], + ATTR_AREA: details.get("waterSense").get("area"), + ATTR_CURRENT_CYCLE: self._switch_data.get("cycle"), + ATTR_FIELD_CAPACITY: details.get("waterSense").get("fieldCapacity"), + ATTR_NO_CYCLES: self._switch_data.get("noOfCycles"), + ATTR_PRECIP_RATE: details.get("waterSense").get("precipitationRate"), + ATTR_RESTRICTIONS: self._switch_data.get("restriction"), + ATTR_SLOPE: SLOPE_TYPE_MAP.get(details.get("slope")), + ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(details.get("sun")), + ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(details.get("group_id")), + ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(details.get("sun")), + ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._switch_data.get("type")), + } + ) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 4fd727ad450..9fa09f9a478 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.12"], + "requirements": ["sqlalchemy==1.3.13"], "dependencies": [], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index cb1cd032614..1c58366f6b5 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,7 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": ["praw==6.4.0"], + "requirements": ["praw==6.5.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 2abd5844001..a3feccaf10c 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -119,12 +119,9 @@ class RemoteDevice(ToggleEntity): """Send a command to a device.""" raise NotImplementedError() - def async_send_command(self, command, **kwargs): - """Send a command to a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job( + async def async_send_command(self, command, **kwargs): + """Send a command to a device.""" + await self.hass.async_add_executor_job( ft.partial(self.send_command, command, **kwargs) ) @@ -132,11 +129,6 @@ class RemoteDevice(ToggleEntity): """Learn a command from a device.""" raise NotImplementedError() - def async_learn_command(self, **kwargs): - """Learn a command from a device. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_executor_job( - ft.partial(self.learn_command, **kwargs) - ) + async def async_learn_command(self, **kwargs): + """Learn a command from a device.""" + await self.hass.async_add_executor_job(ft.partial(self.learn_command, **kwargs)) diff --git a/homeassistant/components/remote/reproduce_state.py b/homeassistant/components/remote/reproduce_state.py new file mode 100644 index 00000000000..1e5dee15683 --- /dev/null +++ b/homeassistant/components/remote/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Remote state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Remote states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 7dfbb964167..bb2ede6d555 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -55,6 +56,7 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the REST command component.""" + @callback def async_register_rest_command(name, command_config): """Create service for rest command.""" websession = async_get_clientsession(hass, command_config.get(CONF_VERIFY_SSL)) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 2e5875b9d08..b8665fae9ef 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -552,10 +552,10 @@ class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): elif command in ["off", "alloff"]: self._state = False - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" - return self._async_handle_command("turn_on") + await self._async_handle_command("turn_on") - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" - return self._async_handle_command("turn_off") + await self._async_handle_command("turn_off") diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index f41c4cde2f7..5db82a1d4e8 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -146,17 +146,17 @@ class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity): """Return True because covers can be stopped midway.""" return True - def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Turn the device close.""" - return self._async_handle_command("close_cover") + await self._async_handle_command("close_cover") - def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Turn the device open.""" - return self._async_handle_command("open_cover") + await self._async_handle_command("open_cover") - def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Turn the device stop.""" - return self._async_handle_command("stop_cover") + await self._async_handle_command("stop_cover") class InvertedRflinkCover(RflinkCover): diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index 28aea1adc31..77b6413f994 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.50"], + "requirements": ["rflink==0.0.51"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ring/.translations/ca.json b/homeassistant/components/ring/.translations/ca.json new file mode 100644 index 00000000000..d51de2b8667 --- /dev/null +++ b/homeassistant/components/ring/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/de.json b/homeassistant/components/ring/.translations/de.json new file mode 100644 index 00000000000..ca49bcef95f --- /dev/null +++ b/homeassistant/components/ring/.translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "2fa": { + "data": { + "2fa": "Zwei-Schritte-Code" + }, + "title": "Zwei-Schritte-Verifizierung" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Anmeldung mit Ring-Konto" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/es.json b/homeassistant/components/ring/.translations/es.json new file mode 100644 index 00000000000..6bdd20d361b --- /dev/null +++ b/homeassistant/components/ring/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dos factores" + }, + "title": "Autenticaci\u00f3n de dos factores" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Iniciar sesi\u00f3n con cuenta de Anillo" + } + }, + "title": "Anillo" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/fr.json b/homeassistant/components/ring/.translations/fr.json new file mode 100644 index 00000000000..c0397692bda --- /dev/null +++ b/homeassistant/components/ring/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "2fa": { + "data": { + "2fa": "Code \u00e0 deux facteurs" + }, + "title": "Authentification \u00e0 deux facteurs" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Connectez-vous avec votre compte Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/it.json b/homeassistant/components/ring/.translations/it.json new file mode 100644 index 00000000000..2e50ee0d583 --- /dev/null +++ b/homeassistant/components/ring/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "2fa": { + "data": { + "2fa": "Codice autenticazione" + }, + "title": "Autenticazione a due fattori" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Accedi con l'account Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/ko.json b/homeassistant/components/ring/.translations/ko.json new file mode 100644 index 00000000000..f566fb592d2 --- /dev/null +++ b/homeassistant/components/ring/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "2fa": { + "data": { + "2fa": "2\ub2e8\uacc4 \uc778\uc99d \ucf54\ub4dc" + }, + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Ring \uacc4\uc815\uc73c\ub85c \ub85c\uadf8\uc778" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/lb.json b/homeassistant/components/ring/.translations/lb.json new file mode 100644 index 00000000000..d004655eebc --- /dev/null +++ b/homeassistant/components/ring/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "2fa": { + "data": { + "2fa": "2-Faktor Code" + }, + "title": "2-Faktor-Authentifikatioun" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mam Ring Kont verbannen" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/nl.json b/homeassistant/components/ring/.translations/nl.json new file mode 100644 index 00000000000..1bb012bd25e --- /dev/null +++ b/homeassistant/components/ring/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "2fa": { + "title": "Tweestapsverificatie" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "title": "Aanmelden met Ring-account" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/no.json b/homeassistant/components/ring/.translations/no.json new file mode 100644 index 00000000000..27dd7438f4a --- /dev/null +++ b/homeassistant/components/ring/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "2fa": { + "data": { + "2fa": "To-faktorskode" + }, + "title": "To-faktor autentisering" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Logg p\u00e5 med din Ring-konto" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/pl.json b/homeassistant/components/ring/.translations/pl.json new file mode 100644 index 00000000000..f34903ff7d1 --- /dev/null +++ b/homeassistant/components/ring/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "2fa": { + "data": { + "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" + }, + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Zaloguj si\u0119 do konta Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/ru.json b/homeassistant/components/ring/.translations/ru.json new file mode 100644 index 00000000000..905f23845a9 --- /dev/null +++ b/homeassistant/components/ring/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Ring" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/sv.json b/homeassistant/components/ring/.translations/sv.json new file mode 100644 index 00000000000..fd9b66b10f0 --- /dev/null +++ b/homeassistant/components/ring/.translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "2fa": { + "data": { + "2fa": "Tv\u00e5faktorkod" + }, + "title": "Tv\u00e5faktorautentisering" + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "title": "Logga in med Ring-konto" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/zh-Hant.json b/homeassistant/components/ring/.translations/zh-Hant.json new file mode 100644 index 00000000000..6f5aaf434bb --- /dev/null +++ b/homeassistant/components/ring/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u96d9\u91cd\u9a57\u8b49\u78bc" + }, + "title": "\u96d9\u91cd\u9a57\u8b49" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u4ee5 Ring \u5e33\u865f\u767b\u5165" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 7b20ff948d1..321b668f8aa 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -1,5 +1,5 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" -from datetime import datetime, timedelta +from datetime import datetime import logging from homeassistant.components.binary_sensor import BinarySensorDevice @@ -10,8 +10,6 @@ from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) - # Sensor types: Name, category, device_class SENSOR_TYPES = { "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index c954553160e..a1fe057d9fb 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -185,22 +185,23 @@ class RussoundZoneDevice(MediaPlayerDevice): """ return float(self._zone_var("volume", 0)) / 50.0 - def async_turn_off(self): + async def async_turn_off(self): """Turn off the zone.""" - return self._russ.send_zone_event(self._zone_id, "ZoneOff") + await self._russ.send_zone_event(self._zone_id, "ZoneOff") - def async_turn_on(self): + async def async_turn_on(self): """Turn on the zone.""" - return self._russ.send_zone_event(self._zone_id, "ZoneOn") + await self._russ.send_zone_event(self._zone_id, "ZoneOn") - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" rvol = int(volume * 50.0) - return self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) + await self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) - def async_select_source(self, source): + async def async_select_source(self, source): """Select the source input for this zone.""" for source_id, name in self._sources: if name.lower() != source.lower(): continue - return self._russ.send_zone_event(self._zone_id, "SelectSource", source_id) + await self._russ.send_zone_event(self._zone_id, "SelectSource", source_id) + break diff --git a/homeassistant/components/safe_mode/__init__.py b/homeassistant/components/safe_mode/__init__.py new file mode 100644 index 00000000000..aef6834303b --- /dev/null +++ b/homeassistant/components/safe_mode/__init__.py @@ -0,0 +1,15 @@ +"""The Safe Mode integration.""" +from homeassistant.components import persistent_notification +from homeassistant.core import HomeAssistant + +DOMAIN = "safe_mode" + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Safe Mode component.""" + persistent_notification.async_create( + hass, + "Home Assistant is running in safe mode. Check [the error log](/developer-tools/logs) to see what went wrong.", + "Safe Mode", + ) + return True diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json new file mode 100644 index 00000000000..372ec51de37 --- /dev/null +++ b/homeassistant/components/safe_mode/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "safe_mode", + "name": "Safe Mode", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/safe_mode", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": ["frontend", "config", "persistent_notification", "cloud"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 704e9996d2d..30399640088 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -225,6 +225,7 @@ class SAJsensor(Entity): """Return the date when the sensor was last updated.""" return self._sensor.date + @callback def async_update_values(self, unknown_state=False): """Update this sensor.""" update = False diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json new file mode 100644 index 00000000000..beeb62d8bdb --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "name": "Nom" + }, + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json new file mode 100644 index 00000000000..60372837ffc --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dieser Samsung TV ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", + "auth_missing": "Home Assistant ist nicht authentifiziert, um eine Verbindung zu diesem Samsung TV herzustellen.", + "not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.", + "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host oder IP-Adresse", + "name": "Name" + }, + "description": "Gebe deine Samsung TV-Informationen ein. Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/fr.json b/homeassistant/components/samsungtv/.translations/fr.json new file mode 100644 index 00000000000..b880e41e5df --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ce t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", + "auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.", + "not_found": "Aucun t\u00e9l\u00e9viseur Samsung pris en charge trouv\u00e9 sur le r\u00e9seau.", + "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." + }, + "step": { + "confirm": { + "description": "Voulez vous installer la TV {model} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", + "title": "TV Samsung" + }, + "user": { + "data": { + "host": "H\u00f4te ou adresse IP", + "name": "Nom" + }, + "description": "Entrez les informations relatives \u00e0 votre t\u00e9l\u00e9viseur Samsung. Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification.", + "title": "TV Samsung" + } + }, + "title": "TV Samsung" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/hu.json b/homeassistant/components/samsungtv/.translations/hu.json new file mode 100644 index 00000000000..6d816ecb95a --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "not_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 t\u00e1mogatott Samsung TV-eszk\u00f6z.", + "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." + }, + "step": { + "confirm": { + "description": "Be\u00e1ll\u00edtja a Samsung TV {model} k\u00e9sz\u00fcl\u00e9ket? Ha soha nem csatlakozott home assistant-hez ezel\u0151tt, meg kell jelennie egy felugr\u00f3 ablaknak a TV-ben, ahol hiteles\u00edt\u00e9st k\u00e9r. A tv-k\u00e9sz\u00fcl\u00e9k manu\u00e1lis konfigur\u00e1ci\u00f3i fel\u00fcl\u00edr\u00f3dnak.", + "title": "Samsung TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json new file mode 100644 index 00000000000..2817c36989b --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", + "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\uc0bc\uc131 TV {model} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV \uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc744 \ub36e\uc5b4\uc501\ub2c8\ub2e4.", + "title": "\uc0bc\uc131 TV" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "name": "\uc774\ub984" + }, + "description": "\uc0bc\uc131 TV \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. Home Assistant \ub97c \uc5f0\uacb0 \ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV \uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4.", + "title": "\uc0bc\uc131 TV" + } + }, + "title": "\uc0bc\uc131 TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/nl.json b/homeassistant/components/samsungtv/.translations/nl.json new file mode 100644 index 00000000000..93bb5953e31 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "auth_missing": "Home Assistant is niet geverifieerd om verbinding te maken met deze Samsung TV.", + "not_found": "Geen ondersteunde Samsung TV-apparaten gevonden op het netwerk.", + "not_supported": "Deze Samsung TV-apparaten wordt momenteel niet ondersteund." + }, + "step": { + "confirm": { + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Hostnaam of IP-adres", + "name": "Naam" + }, + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/pl.json b/homeassistant/components/samsungtv/.translations/pl.json new file mode 100644 index 00000000000..e31aea01d46 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ten telewizor Samsung jest ju\u017c skonfigurowany.", + "already_in_progress": "Konfiguracja telewizora Samsung jest ju\u017c w toku.", + "auth_missing": "Home Assistant nie jest uwierzytelniony, aby po\u0142\u0105czy\u0107 si\u0119 z tym telewizorem Samsung.", + "not_found": "W sieci nie znaleziono obs\u0142ugiwanych telewizor\u00f3w Samsung.", + "not_supported": "Te telewizor Samsung nie jest obecnie obs\u0142ugiwany." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 telewizor Samsung {model}? Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie. R\u0119czne konfiguracje tego telewizora zostan\u0105 zast\u0105pione.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa" + }, + "description": "Wprowad\u017a informacje o telewizorze Samsung. Je\u015bli nigdy wcze\u015bniej ten telewizor nie by\u0142 \u0142\u0105czony z Home Assistant'em na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/sv.json b/homeassistant/components/samsungtv/.translations/sv.json new file mode 100644 index 00000000000..cf5636700aa --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "confirm": { + "title": "" + }, + "user": { + "data": { + "host": "V\u00e4rdnamn eller IP-adress", + "name": "Namn" + }, + "description": "Ange informationen f\u00f6r din Samsung TV. Om du aldrig har anslutit denna till Home Assistant tidigare borde du se en popup om autentisering p\u00e5 din TV.", + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/zh-Hant.json b/homeassistant/components/samsungtv/.translations/zh-Hant.json new file mode 100644 index 00000000000..272dffaa482 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e09\u661f\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u4e09\u661f\u96fb\u8996\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002", + "not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684\u4e09\u661f\u96fb\u8996\u3002", + "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4e09\u661f\u96fb\u8996 {model}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002\u624b\u52d5\u8a2d\u5b9a\u5c07\u6703\u8986\u84cb\u539f\u8a2d\u5b9a\u3002", + "title": "\u4e09\u661f\u96fb\u8996" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "name": "\u540d\u7a31" + }, + "description": "\u8f38\u5165\u4e09\u661f\u96fb\u8996\u8cc7\u8a0a\u3002\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", + "title": "\u4e09\u661f\u96fb\u8996" + } + }, + "title": "\u4e09\u661f\u96fb\u8996" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 6b4f0e31f02..bc49dc3156d 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1 +1,68 @@ """The Samsung TV integration.""" +import socket + +import voluptuous as vol + +from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +import homeassistant.helpers.config_validation as cv + +from .const import CONF_ON_ACTION, DEFAULT_NAME, DOMAIN + + +def ensure_unique_hosts(value): + """Validate that all configs have a unique host.""" + vol.Schema(vol.Unique("duplicate host entries found"))( + [socket.gethostbyname(entry[CONF_HOST]) for entry in value] + ) + return value + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + } + ) + ], + ensure_unique_hosts, + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Samsung TV integration.""" + if DOMAIN in config: + hass.data[DOMAIN] = {} + for entry_config in config[DOMAIN]: + ip_address = await hass.async_add_executor_job( + socket.gethostbyname, entry_config[CONF_HOST] + ) + hass.data[DOMAIN][ip_address] = { + CONF_ON_ACTION: entry_config.get(CONF_ON_ACTION) + } + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=entry_config + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up the Samsung TV platform.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) + ) + + return True diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py new file mode 100644 index 00000000000..debe7349b6c --- /dev/null +++ b/homeassistant/components/samsungtv/config_flow.py @@ -0,0 +1,194 @@ +"""Config flow for Samsung TV.""" +import socket +from urllib.parse import urlparse + +from samsungctl import Remote +from samsungctl.exceptions import AccessDenied, UnhandledResponse +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_UDN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_ID, + CONF_IP_ADDRESS, + CONF_METHOD, + CONF_NAME, + CONF_PORT, +) + +# pylint:disable=unused-import +from .const import CONF_MANUFACTURER, CONF_MODEL, DOMAIN, LOGGER, METHODS + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}) + +RESULT_AUTH_MISSING = "auth_missing" +RESULT_SUCCESS = "success" +RESULT_NOT_SUCCESSFUL = "not_successful" +RESULT_NOT_SUPPORTED = "not_supported" + + +def _get_ip(host): + if host is None: + return None + return socket.gethostbyname(host) + + +class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Samsung TV config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + + def __init__(self): + """Initialize flow.""" + self._host = None + self._ip = None + self._manufacturer = None + self._method = None + self._model = None + self._name = None + self._port = None + self._title = None + self._id = None + + def _get_entry(self): + return self.async_create_entry( + title=self._title, + data={ + CONF_HOST: self._host, + CONF_ID: self._id, + CONF_IP_ADDRESS: self._ip, + CONF_MANUFACTURER: self._manufacturer, + CONF_METHOD: self._method, + CONF_MODEL: self._model, + CONF_NAME: self._name, + CONF_PORT: self._port, + }, + ) + + def _try_connect(self): + """Try to connect and check auth.""" + for method in METHODS: + config = { + "name": "HomeAssistant", + "description": "HomeAssistant", + "id": "ha.component.samsung", + "host": self._host, + "method": method, + "port": self._port, + # We need this high timeout because waiting for auth popup is just an open socket + "timeout": 31, + } + try: + LOGGER.debug("Try config: %s", config) + with Remote(config.copy()): + LOGGER.debug("Working config: %s", config) + self._method = method + return RESULT_SUCCESS + except AccessDenied: + LOGGER.debug("Working but denied config: %s", config) + return RESULT_AUTH_MISSING + except UnhandledResponse: + LOGGER.debug("Working but unsupported config: %s", config) + return RESULT_NOT_SUPPORTED + except OSError as err: + LOGGER.debug("Failing config: %s, error: %s", config, err) + + LOGGER.debug("No working config found") + return RESULT_NOT_SUCCESSFUL + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + self._port = user_input.get(CONF_PORT) + + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + ip_address = await self.hass.async_add_executor_job( + _get_ip, user_input[CONF_HOST] + ) + + await self.async_set_unique_id(ip_address) + self._abort_if_unique_id_configured() + + self._host = user_input.get(CONF_HOST) + self._ip = self.context[CONF_IP_ADDRESS] = ip_address + self._name = user_input.get(CONF_NAME) + self._title = self._name + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result != RESULT_SUCCESS: + return self.async_abort(reason=result) + return self._get_entry() + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + async def async_step_ssdp(self, user_input=None): + """Handle a flow initialized by discovery.""" + host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname + ip_address = await self.hass.async_add_executor_job(_get_ip, host) + + self._host = host + self._ip = self.context[CONF_IP_ADDRESS] = ip_address + self._manufacturer = user_input.get(ATTR_UPNP_MANUFACTURER) + self._model = user_input.get(ATTR_UPNP_MODEL_NAME) + self._name = f"Samsung {self._model}" + self._id = user_input.get(ATTR_UPNP_UDN) + self._title = self._model + + # probably access denied + if self._id is None: + return self.async_abort(reason=RESULT_AUTH_MISSING) + if self._id.startswith("uuid:"): + self._id = self._id[5:] + + config_entry = await self.async_set_unique_id(ip_address) + if config_entry: + config_entry.data[CONF_ID] = self._id + config_entry.data[CONF_MANUFACTURER] = self._manufacturer + config_entry.data[CONF_MODEL] = self._model + self.hass.config_entries.async_update_entry(config_entry) + return self.async_abort(reason="already_configured") + + self.context["title_placeholders"] = {"model": self._model} + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + result = await self.hass.async_add_executor_job(self._try_connect) + + if result != RESULT_SUCCESS: + return self.async_abort(reason=result) + return self._get_entry() + + return self.async_show_form( + step_id="confirm", description_placeholders={"model": self._model} + ) + + async def async_step_reauth(self, user_input=None): + """Handle configuration by re-auth.""" + self._host = user_input[CONF_HOST] + self._id = user_input.get(CONF_ID) + self._ip = user_input[CONF_IP_ADDRESS] + self._manufacturer = user_input.get(CONF_MANUFACTURER) + self._model = user_input.get(CONF_MODEL) + self._name = user_input.get(CONF_NAME) + self._port = user_input.get(CONF_PORT) + self._title = self._model or self._name + + await self.async_set_unique_id(self._ip) + self.context["title_placeholders"] = {"model": self._title} + + return await self.async_step_confirm() diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 83d74743844..ea893390a5b 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -3,3 +3,11 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "samsungtv" + +DEFAULT_NAME = "Samsung TV" + +CONF_MANUFACTURER = "manufacturer" +CONF_MODEL = "model" +CONF_ON_ACTION = "turn_on_action" + +METHODS = ("websocket", "legacy") diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index d8db31db728..3adc3b52eb3 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -2,7 +2,17 @@ "domain": "samsungtv", "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "requirements": ["samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6"], + "requirements": [ + "samsungctl[websocket]==0.7.1" + ], + "ssdp": [ + { + "st": "urn:samsung.com:device:RemoteControlReceiver:1" + } + ], "dependencies": [], - "codeowners": ["@escoand"] + "codeowners": [ + "@escoand" + ], + "config_flow": true } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index fd900fedec1..8de42d157b7 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,18 +1,12 @@ """Support for interface with an Samsung TV.""" import asyncio from datetime import timedelta -import socket from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions import voluptuous as vol -import wakeonlan from websocket import WebSocketException -from homeassistant.components.media_player import ( - DEVICE_CLASS_TV, - PLATFORM_SCHEMA, - MediaPlayerDevice, -) +from homeassistant.components.media_player import DEVICE_CLASS_TV, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -27,27 +21,22 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( - CONF_BROADCAST_ADDRESS, CONF_HOST, - CONF_MAC, + CONF_ID, + CONF_IP_ADDRESS, + CONF_METHOD, CONF_NAME, CONF_PORT, - CONF_TIMEOUT, STATE_OFF, STATE_ON, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script from homeassistant.util import dt as dt_util -from .const import LOGGER - -DEFAULT_NAME = "Samsung TV Remote" -DEFAULT_TIMEOUT = 1 -DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" +from .const import CONF_MANUFACTURER, CONF_MODEL, CONF_ON_ACTION, DOMAIN, LOGGER KEY_PRESS_TIMEOUT = 1.2 -KNOWN_DEVICES_KEY = "samsungtv_known_devices" -METHODS = ("websocket", "legacy") SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( @@ -62,73 +51,40 @@ SUPPORT_SAMSUNGTV = ( | SUPPORT_PLAY_MEDIA ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_MAC): cv.string, - vol.Optional( - CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS - ): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, add_entities, discovery_info=None +): # pragma: no cover """Set up the Samsung TV platform.""" - known_devices = hass.data.get(KNOWN_DEVICES_KEY) - if known_devices is None: - known_devices = set() - hass.data[KNOWN_DEVICES_KEY] = known_devices + pass - uuid = None - # Is this a manual configuration? - if config.get(CONF_HOST) is not None: - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) - mac = config.get(CONF_MAC) - broadcast = config.get(CONF_BROADCAST_ADDRESS) - timeout = config.get(CONF_TIMEOUT) - elif discovery_info is not None: - tv_name = discovery_info.get("name") - model = discovery_info.get("model_name") - host = discovery_info.get("host") - name = f"{tv_name} ({model})" - if name.startswith("[TV]"): - name = name[4:] - port = None - timeout = DEFAULT_TIMEOUT - mac = None - broadcast = DEFAULT_BROADCAST_ADDRESS - uuid = discovery_info.get("udn") - if uuid and uuid.startswith("uuid:"): - uuid = uuid[len("uuid:") :] - # Only add a device once, so discovered devices do not override manual - # config. - ip_addr = socket.gethostbyname(host) - if ip_addr not in known_devices: - known_devices.add(ip_addr) - add_entities([SamsungTVDevice(host, port, name, timeout, mac, broadcast, uuid)]) - LOGGER.info("Samsung TV %s added as '%s'", host, name) - else: - LOGGER.info("Ignoring duplicate Samsung TV %s", host) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Samsung TV from a config entry.""" + ip_address = config_entry.data[CONF_IP_ADDRESS] + on_script = None + if ( + DOMAIN in hass.data + and ip_address in hass.data[DOMAIN] + and CONF_ON_ACTION in hass.data[DOMAIN][ip_address] + and hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + ): + turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] + on_script = Script(hass, turn_on_action) + async_add_entities([SamsungTVDevice(config_entry, on_script)]) class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - def __init__(self, host, port, name, timeout, mac, broadcast, uuid): + def __init__(self, config_entry, on_script): """Initialize the Samsung device.""" - - # Save a reference to the imported classes - self._name = name - self._mac = mac - self._broadcast = broadcast - self._uuid = uuid + self._config_entry = config_entry + self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) + self._model = config_entry.data.get(CONF_MODEL) + self._name = config_entry.data.get(CONF_NAME) + self._on_script = on_script + self._uuid = config_entry.data.get(CONF_ID) # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -141,60 +97,55 @@ class SamsungTVDevice(MediaPlayerDevice): # Generate a configuration for the Samsung library self._config = { "name": "HomeAssistant", - "description": name, + "description": "HomeAssistant", "id": "ha.component.samsung", - "method": None, - "port": port, - "host": host, - "timeout": timeout, + "method": config_entry.data[CONF_METHOD], + "port": config_entry.data.get(CONF_PORT), + "host": config_entry.data[CONF_HOST], + "timeout": 1, } - # Select method by port number, mainly for fallback - if self._config["port"] in (8001, 8002): - self._config["method"] = "websocket" - elif self._config["port"] == 55000: - self._config["method"] = "legacy" - def update(self): """Update state of device.""" - self.send_key("KEY") + if self._power_off_in_progress(): + self._state = STATE_OFF + else: + if self._remote is not None: + # Close the current remote connection + self._remote.close() + self._remote = None + + try: + self.get_remote() + if self._remote: + self._state = STATE_ON + except ( + samsung_exceptions.UnhandledResponse, + samsung_exceptions.AccessDenied, + ): + # We got a response so it's working. + self._state = STATE_ON + except (OSError, WebSocketException): + # Different reasons, e.g. hostname not resolveable + self._state = STATE_OFF def get_remote(self): """Create or return a remote control instance.""" - - # Try to find correct method automatically - if self._config["method"] not in METHODS: - for method in METHODS: - try: - self._config["method"] = method - LOGGER.debug("Try config: %s", self._config) - self._remote = SamsungRemote(self._config.copy()) - self._state = STATE_ON - LOGGER.debug("Found working config: %s", self._config) - break - except ( - samsung_exceptions.UnhandledResponse, - samsung_exceptions.AccessDenied, - ): - # We got a response so it's working. - self._state = STATE_ON - LOGGER.debug( - "Found working config without connection: %s", self._config - ) - break - except OSError as err: - LOGGER.debug("Failing config: %s error was: %s", self._config, err) - self._config["method"] = None - - # Unable to find working connection - if self._config["method"] is None: - self._remote = None - self._state = None - return None - if self._remote is None: # We need to create a new instance to reconnect. - self._remote = SamsungRemote(self._config.copy()) + try: + self._remote = SamsungRemote(self._config.copy()) + # This is only happening when the auth was switched to DENY + # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket + except samsung_exceptions.AccessDenied: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth"}, + data=self._config_entry.data, + ) + ) + raise return self._remote @@ -218,22 +169,12 @@ class SamsungTVDevice(MediaPlayerDevice): # BrokenPipe can occur when the commands is sent to fast # WebSocketException can occur when timed out self._remote = None - self._state = STATE_ON - except AttributeError: - # Auto-detect could not find working config yet - pass except (samsung_exceptions.UnhandledResponse, samsung_exceptions.AccessDenied): # We got a response so it's on. - self._state = STATE_ON - self._remote = None LOGGER.debug("Failed sending command %s", key, exc_info=True) - return except OSError: # Different reasons, e.g. hostname not resolveable - self._state = STATE_OFF - self._remote = None - if self._power_off_in_progress(): - self._state = STATE_OFF + pass def _power_off_in_progress(self): return ( @@ -256,6 +197,16 @@ class SamsungTVDevice(MediaPlayerDevice): """Return the state of the device.""" return self._state + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": self._manufacturer, + "model": self._model, + } + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" @@ -269,7 +220,7 @@ class SamsungTVDevice(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - if self._mac: + if self._on_script: return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON return SUPPORT_SAMSUNGTV @@ -344,21 +295,19 @@ class SamsungTVDevice(MediaPlayerDevice): return for digit in media_id: - await self.hass.async_add_job(self.send_key, f"KEY_{digit}") + await self.hass.async_add_executor_job(self.send_key, f"KEY_{digit}") await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) - await self.hass.async_add_job(self.send_key, "KEY_ENTER") + await self.hass.async_add_executor_job(self.send_key, "KEY_ENTER") - def turn_on(self): + async def async_turn_on(self): """Turn the media player on.""" - if self._mac: - wakeonlan.send_magic_packet(self._mac, ip_address=self._broadcast) - else: - self.send_key("KEY_POWERON") + if self._on_script: + await self._on_script.async_run() - async def async_select_source(self, source): + def select_source(self, source): """Select input source.""" if source not in SOURCES: LOGGER.error("Unsupported source") return - await self.hass.async_add_job(self.send_key, SOURCES[source]) + self.send_key(SOURCES[source]) diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json new file mode 100644 index 00000000000..2e36062669f --- /dev/null +++ b/homeassistant/components/samsungtv/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "Samsung TV: {model}", + "title": "Samsung TV", + "step": { + "user": { + "title": "Samsung TV", + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization.", + "data": { + "host": "Host or IP address", + "name": "Name" + } + }, + "confirm": { + "title": "Samsung TV", + "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization. Manual configurations for this TV will be overwritten." + } + }, + "abort": { + "already_in_progress": "Samsung TV configuration is already in progress.", + "already_configured": "This Samsung TV is already configured.", + "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", + "not_successful": "Unable to connect to this Samsung TV device.", + "not_supported": "This Samsung TV device is currently not supported." + } + } +} diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 75ec2bfd875..46b06b93698 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -61,10 +61,7 @@ async def async_setup(hass, config): await component.async_setup(config) # Ensure Home Assistant platform always loaded. - await component.async_setup_platform( - HA_DOMAIN, {"platform": "homeasistant", STATES: []} - ) - + await component.async_setup_platform(HA_DOMAIN, {"platform": HA_DOMAIN, STATES: []}) component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate") return True @@ -97,9 +94,6 @@ class Scene(Entity): """Activate scene. Try to get entities into requested state.""" raise NotImplementedError() - def async_activate(self): - """Activate scene. Try to get entities into requested state. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.activate) + async def async_activate(self): + """Activate scene. Try to get entities into requested state.""" + await self.hass.async_add_job(self.activate) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 1d180b54cfd..44684656372 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -1,6 +1,7 @@ """Support for scripts.""" import asyncio import logging +from typing import List import voluptuous as vol @@ -15,6 +16,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -69,9 +71,75 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) +@callback +def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all scripts that reference the entity.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + results = [] + + for script_entity in component.entities: + if entity_id in script_entity.script.referenced_entities: + results.append(script_entity.entity_id) + + return results + + +@callback +def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all entities in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + script_entity = component.get_entity(entity_id) + + if script_entity is None: + return [] + + return list(script_entity.script.referenced_entities) + + +@callback +def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]: + """Return all scripts that reference the device.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + results = [] + + for script_entity in component.entities: + if device_id in script_entity.script.referenced_devices: + results.append(script_entity.entity_id) + + return results + + +@callback +def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: + """Return all devices in a scene.""" + if DOMAIN not in hass.data: + return [] + + component = hass.data[DOMAIN] + + script_entity = component.get_entity(entity_id) + + if script_entity is None: + return [] + + return list(script_entity.script.referenced_devices) + + async def async_setup(hass, config): """Load the scripts from the configuration.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent(_LOGGER, DOMAIN, hass) await _async_process_config(hass, config, component) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py new file mode 100644 index 00000000000..a3bbd3844aa --- /dev/null +++ b/homeassistant/components/search/__init__.py @@ -0,0 +1,241 @@ +"""The Search integration.""" +from collections import defaultdict, deque +import logging + +import voluptuous as vol + +from homeassistant.components import automation, group, script, websocket_api +from homeassistant.components.homeassistant import scene +from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.helpers import device_registry, entity_registry + +DOMAIN = "search" +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Search component.""" + websocket_api.async_register_command(hass, websocket_search_related) + return True + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "search/related", + vol.Required("item_type"): vol.In( + ( + "area", + "automation", + "config_entry", + "device", + "entity", + "group", + "scene", + "script", + ) + ), + vol.Required("item_id"): str, + } +) +async def websocket_search_related(hass, connection, msg): + """Handle search.""" + searcher = Searcher( + hass, + await device_registry.async_get_registry(hass), + await entity_registry.async_get_registry(hass), + ) + connection.send_result( + msg["id"], searcher.async_search(msg["item_type"], msg["item_id"]) + ) + + +class Searcher: + """Find related things. + + Few rules: + Scenes, scripts, automations and config entries will only be expanded if they are + the entry point. They won't be expanded if we process them. This is because they + turn the results into garbage. + """ + + # These types won't be further explored. Config entries + Output types. + DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry", "area"} + # These types exist as an entity and so need cleanup in results + EXIST_AS_ENTITY = {"script", "scene", "automation", "group"} + + def __init__( + self, + hass: HomeAssistant, + device_reg: device_registry.DeviceRegistry, + entity_reg: entity_registry.EntityRegistry, + ): + """Search results.""" + self.hass = hass + self._device_reg = device_reg + self._entity_reg = entity_reg + self.results = defaultdict(set) + self._to_resolve = deque() + + @callback + def async_search(self, item_type, item_id): + """Find results.""" + _LOGGER.debug("Searching for %s/%s", item_type, item_id) + self.results[item_type].add(item_id) + self._to_resolve.append((item_type, item_id)) + + while self._to_resolve: + search_type, search_id = self._to_resolve.popleft() + getattr(self, f"_resolve_{search_type}")(search_id) + + # Clean up entity_id items, from the general "entity" type result, + # that are also found in the specific entity domain type. + for result_type in self.EXIST_AS_ENTITY: + self.results["entity"] -= self.results[result_type] + + # Remove entry into graph from search results. + to_remove_item_type = item_type + if item_type == "entity": + domain = split_entity_id(item_id)[0] + + if domain in self.EXIST_AS_ENTITY: + to_remove_item_type = domain + + self.results[to_remove_item_type].remove(item_id) + + # Filter out empty sets. + return {key: val for key, val in self.results.items() if val} + + @callback + def _add_or_resolve(self, item_type, item_id): + """Add an item to explore.""" + if item_id in self.results[item_type]: + return + + self.results[item_type].add(item_id) + + if item_type not in self.DONT_RESOLVE: + self._to_resolve.append((item_type, item_id)) + + @callback + def _resolve_area(self, area_id) -> None: + """Resolve an area.""" + for device in device_registry.async_entries_for_area(self._device_reg, area_id): + self._add_or_resolve("device", device.id) + + @callback + def _resolve_device(self, device_id) -> None: + """Resolve a device.""" + device_entry = self._device_reg.async_get(device_id) + # Unlikely entry doesn't exist, but let's guard for bad data. + if device_entry is not None: + if device_entry.area_id: + self._add_or_resolve("area", device_entry.area_id) + + for config_entry_id in device_entry.config_entries: + self._add_or_resolve("config_entry", config_entry_id) + + # We do not resolve device_entry.via_device_id because that + # device is not related data-wise inside HA. + + for entity_entry in entity_registry.async_entries_for_device( + self._entity_reg, device_id + ): + self._add_or_resolve("entity", entity_entry.entity_id) + + for entity_id in script.scripts_with_device(self.hass, device_id): + self._add_or_resolve("entity", entity_id) + + for entity_id in automation.automations_with_device(self.hass, device_id): + self._add_or_resolve("entity", entity_id) + + @callback + def _resolve_entity(self, entity_id) -> None: + """Resolve an entity.""" + # Extra: Find automations and scripts that reference this entity. + + for entity in scene.scenes_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + + for entity in group.groups_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + + for entity in automation.automations_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + + for entity in script.scripts_with_entity(self.hass, entity_id): + self._add_or_resolve("entity", entity) + + # Find devices + entity_entry = self._entity_reg.async_get(entity_id) + if entity_entry is not None: + if entity_entry.device_id: + self._add_or_resolve("device", entity_entry.device_id) + + if entity_entry.config_entry_id is not None: + self._add_or_resolve("config_entry", entity_entry.config_entry_id) + + domain = split_entity_id(entity_id)[0] + + if domain in self.EXIST_AS_ENTITY: + self._add_or_resolve(domain, entity_id) + + @callback + def _resolve_automation(self, automation_entity_id) -> None: + """Resolve an automation. + + Will only be called if automation is an entry point. + """ + for entity in automation.entities_in_automation( + self.hass, automation_entity_id + ): + self._add_or_resolve("entity", entity) + + for device in automation.devices_in_automation(self.hass, automation_entity_id): + self._add_or_resolve("device", device) + + @callback + def _resolve_script(self, script_entity_id) -> None: + """Resolve a script. + + Will only be called if script is an entry point. + """ + for entity in script.entities_in_script(self.hass, script_entity_id): + self._add_or_resolve("entity", entity) + + for device in script.devices_in_script(self.hass, script_entity_id): + self._add_or_resolve("device", device) + + @callback + def _resolve_group(self, group_entity_id) -> None: + """Resolve a group. + + Will only be called if group is an entry point. + """ + for entity_id in group.get_entity_ids(self.hass, group_entity_id): + self._add_or_resolve("entity", entity_id) + + @callback + def _resolve_scene(self, scene_entity_id) -> None: + """Resolve a scene. + + Will only be called if scene is an entry point. + """ + for entity in scene.entities_in_scene(self.hass, scene_entity_id): + self._add_or_resolve("entity", entity) + + @callback + def _resolve_config_entry(self, config_entry_id) -> None: + """Resolve a config entry. + + Will only be called if config entry is an entry point. + """ + for device_entry in device_registry.async_entries_for_config_entry( + self._device_reg, config_entry_id + ): + self._add_or_resolve("device", device_entry.id) + + for entity_entry in entity_registry.async_entries_for_config_entry( + self._entity_reg, config_entry_id + ): + self._add_or_resolve("entity", entity_entry.entity_id) diff --git a/homeassistant/components/search/manifest.json b/homeassistant/components/search/manifest.json new file mode 100644 index 00000000000..581a702f514 --- /dev/null +++ b/homeassistant/components/search/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "search", + "name": "Search", + "documentation": "https://www.home-assistant.io/integrations/search", + "requirements": [], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": ["websocket_api"], + "after_dependencies": ["scene", "group", "automation", "script"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index 7d5a322ba18..0680bab7b48 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -3,24 +3,24 @@ "condition_type": { "is_battery_level": "{entity_name} Batteriestand", "is_humidity": "{entity_name} Feuchtigkeit", - "is_illuminance": "{entity_name} Beleuchtungsst\u00e4rke", - "is_power": "{entity_name} Leistung", + "is_illuminance": "Aktuelle {entity_name} Helligkeit", + "is_power": "Aktuelle {entity_name} Leistung", "is_pressure": "{entity_name} Druck", - "is_signal_strength": "{entity_name} Signalst\u00e4rke", - "is_temperature": "{entity_name} Temperatur", + "is_signal_strength": "Aktuelle {entity_name} Signalst\u00e4rke", + "is_temperature": "Aktuelle {entity_name} Temperatur", "is_timestamp": "Aktueller Zeitstempel von {entity_name}", - "is_value": "{entity_name} Wert" + "is_value": "Aktueller {entity_name} Wert" }, "trigger_type": { - "battery_level": "{entity_name} Batteriestatus", - "humidity": "{entity_name} Feuchtigkeit", - "illuminance": "{entity_name} Beleuchtungsst\u00e4rke", - "power": "{entity_name} Leistung", - "pressure": "{entity_name} Druck", - "signal_strength": "{entity_name} Signalst\u00e4rke", - "temperature": "{entity_name} Temperatur", - "timestamp": "{entity_name} Zeitstempel", - "value": "{entity_name} Wert" + "battery_level": "{entity_name} Batteriestatus\u00e4nderungen", + "humidity": "{entity_name} Feuchtigkeits\u00e4nderungen", + "illuminance": "{entity_name} Helligkeits\u00e4nderungen", + "power": "{entity_name} Leistungs\u00e4nderungen", + "pressure": "{entity_name} Druck\u00e4nderungen", + "signal_strength": "{entity_name} Signalst\u00e4rke\u00e4nderungen", + "temperature": "{entity_name} Temperatur\u00e4nderungen", + "timestamp": "{entity_name} Zeitstempel\u00e4nderungen", + "value": "{entity_name} Wert\u00e4nderungen" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json index 7c6bed1e033..0a1b0d13cb0 100644 --- a/homeassistant/components/sensor/.translations/it.json +++ b/homeassistant/components/sensor/.translations/it.json @@ -8,7 +8,7 @@ "is_pressure": "Pressione attuale di {entity_name}", "is_signal_strength": "Potenza del segnale attuale di {entity_name}", "is_temperature": "Temperatura attuale di {entity_name}", - "is_timestamp": "Data e ora attuali di {nome_entit\u00e0}", + "is_timestamp": "Data e ora attuali di {entity_name}", "is_value": "Valore attuale di {entity_name}" }, "trigger_type": { diff --git a/homeassistant/components/sensor/.translations/zh-Hans.json b/homeassistant/components/sensor/.translations/zh-Hans.json new file mode 100644 index 00000000000..12059aa6b1b --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u5f53\u524d\u7684\u7535\u6c60\u7535\u91cf", + "is_humidity": "{entity_name} \u5f53\u524d\u7684\u6e7f\u5ea6", + "is_illuminance": "{entity_name} \u5f53\u524d\u7684\u5149\u7167\u5f3a\u5ea6", + "is_power": "{entity_name} \u5f53\u524d\u7684\u529f\u7387", + "is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b", + "is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6", + "is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6", + "is_timestamp": "{entity_name} \u5f53\u524d\u7684\u65f6\u95f4\u6233", + "is_value": "{entity_name} \u5f53\u524d\u7684\u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u7684\u7535\u6c60\u7535\u91cf\u53d8\u5316", + "humidity": "{entity_name} \u7684\u6e7f\u5ea6\u53d8\u5316", + "illuminance": "{entity_name} \u7684\u5149\u7167\u5f3a\u5ea6\u53d8\u5316", + "power": "{entity_name} \u7684\u529f\u7387\u53d8\u5316", + "pressure": "{entity_name} \u7684\u538b\u529b\u53d8\u5316", + "signal_strength": "{entity_name} \u7684\u4fe1\u53f7\u5f3a\u5ea6\u53d8\u5316", + "temperature": "{entity_name} \u7684\u6e29\u5ea6\u53d8\u5316", + "timestamp": "{entity_name} \u7684\u65f6\u95f4\u6233\u53d8\u5316", + "value": "{entity_name} \u7684\u503c\u53d8\u5316" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 7417765f9f4..bb0348eb6a7 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -22,7 +22,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -128,6 +128,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/sentry/.translations/de.json b/homeassistant/components/sentry/.translations/de.json index c1cd6496220..ea1e3f674ae 100644 --- a/homeassistant/components/sentry/.translations/de.json +++ b/homeassistant/components/sentry/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Geben Sie Ihren Sentry-DSN ein", + "description": "Gebe deine Sentry-DSN ein", "title": "Sentry" } }, diff --git a/homeassistant/components/sentry/.translations/fr.json b/homeassistant/components/sentry/.translations/fr.json new file mode 100644 index 00000000000..7702874866a --- /dev/null +++ b/homeassistant/components/sentry/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "bad_dsn": "DSN invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "description": "Entrez votre DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/nl.json b/homeassistant/components/sentry/.translations/nl.json new file mode 100644 index 00000000000..7e198e836d7 --- /dev/null +++ b/homeassistant/components/sentry/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 9c73de34af8..8ce23248832 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.const import __version__ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv @@ -51,6 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): dsn=conf.get(CONF_DSN), environment=conf.get(CONF_ENVIRONMENT), integrations=[sentry_logging], + release=f"homeassistant-{__version__}", ) return True diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 007f6ef1d99..1b04da721b1 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.21.1"], + "requirements": ["shodan==1.21.3"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 856ea0784ba..50d317c9095 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -153,15 +153,14 @@ class ShoppingData: self.items = [itm for itm in self.items if not itm["complete"]] self.hass.async_add_job(self.save) - @asyncio.coroutine - def async_load(self): + async def async_load(self): """Load items.""" def load(): """Load the items synchronously.""" return load_json(self.hass.config.path(PERSISTENCE), default=[]) - self.items = yield from self.hass.async_add_job(load) + self.items = await self.hass.async_add_executor_job(load) def save(self): """Save the items.""" diff --git a/homeassistant/components/sighthound/__init__.py b/homeassistant/components/sighthound/__init__.py new file mode 100644 index 00000000000..f80e739310e --- /dev/null +++ b/homeassistant/components/sighthound/__init__.py @@ -0,0 +1 @@ +"""The sighthound integration.""" diff --git a/homeassistant/components/sighthound/image_processing.py b/homeassistant/components/sighthound/image_processing.py new file mode 100644 index 00000000000..175b1edc4c6 --- /dev/null +++ b/homeassistant/components/sighthound/image_processing.py @@ -0,0 +1,120 @@ +"""Person detection using Sighthound cloud service.""" +import logging + +import simplehound.core as hound +import voluptuous as vol + +from homeassistant.components.image_processing import ( + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +EVENT_PERSON_DETECTED = "sighthound.person_detected" + +ATTR_BOUNDING_BOX = "bounding_box" +ATTR_PEOPLE = "people" +CONF_ACCOUNT_TYPE = "account_type" +DEV = "dev" +PROD = "prod" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_ACCOUNT_TYPE, default=DEV): vol.In([DEV, PROD]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the platform.""" + # Validate credentials by processing image. + api_key = config[CONF_API_KEY] + account_type = config[CONF_ACCOUNT_TYPE] + api = hound.cloud(api_key, account_type) + try: + api.detect(b"Test") + except hound.SimplehoundException as exc: + _LOGGER.error("Sighthound error %s setup aborted", exc) + return + + entities = [] + for camera in config[CONF_SOURCE]: + sighthound = SighthoundEntity( + api, camera[CONF_ENTITY_ID], camera.get(CONF_NAME) + ) + entities.append(sighthound) + add_entities(entities) + + +class SighthoundEntity(ImageProcessingEntity): + """Create a sighthound entity.""" + + def __init__(self, api, camera_entity, name): + """Init.""" + self._api = api + self._camera = camera_entity + if name: + self._name = name + else: + camera_name = split_entity_id(camera_entity)[1] + self._name = f"sighthound_{camera_name}" + self._state = None + self._image_width = None + self._image_height = None + + def process_image(self, image): + """Process an image.""" + detections = self._api.detect(image) + people = hound.get_people(detections) + self._state = len(people) + + metadata = hound.get_metadata(detections) + self._image_width = metadata["image_width"] + self._image_height = metadata["image_height"] + for person in people: + self.fire_person_detected_event(person) + + def fire_person_detected_event(self, person): + """Send event with detected total_persons.""" + self.hass.bus.fire( + EVENT_PERSON_DETECTED, + { + ATTR_ENTITY_ID: self.entity_id, + ATTR_BOUNDING_BOX: hound.bbox_to_tf_style( + person["boundingBox"], self._image_width, self._image_height + ), + }, + ) + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return ATTR_PEOPLE diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json new file mode 100644 index 00000000000..737aa01c21f --- /dev/null +++ b/homeassistant/components/sighthound/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "sighthound", + "name": "Sighthound", + "documentation": "https://www.home-assistant.io/integrations/sighthound", + "requirements": [ + "simplehound==0.3" + ], + "dependencies": [], + "codeowners": [ + "@robmarkcole" + ] +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 63ac0ca973c..b3d3baff16f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,22 +1,14 @@ """Support for SimpliSafe alarm systems.""" import asyncio -from datetime import timedelta import logging from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError -from simplipy.system.v3 import LevelMap as V3Volume +from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_CODE, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TOKEN, - CONF_USERNAME, - STATE_HOME, -) +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -30,7 +22,10 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.service import verify_domain_control +from homeassistant.helpers.service import ( + async_register_admin_service, + verify_domain_control, +) from .config_flow import configured_instances from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE @@ -41,24 +36,21 @@ CONF_ACCOUNTS = "accounts" DATA_LISTENER = "listener" -ATTR_ARMED_LIGHT_STATE = "armed_light_state" -ATTR_ARRIVAL_STATE = "arrival_state" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" +ATTR_LIGHT = "light" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" -ATTR_SECONDS = "seconds" ATTR_SYSTEM_ID = "system_id" -ATTR_TRANSITION = "transition" -ATTR_VOLUME = "volume" -ATTR_VOLUME_PROPERTY = "volume_property" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" -STATE_AWAY = "away" -STATE_ENTRY = "entry" -STATE_EXIT = "exit" - -VOLUME_PROPERTY_ALARM = "alarm" -VOLUME_PROPERTY_CHIME = "chime" -VOLUME_PROPERTY_VOICE_PROMPT = "voice_prompt" +VOLUMES = [VOLUME_OFF, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_HIGH] SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) @@ -66,28 +58,33 @@ SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( {vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string} ) -SERVICE_SET_DELAY_SCHEMA = SERVICE_BASE_SCHEMA.extend( - { - vol.Required(ATTR_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)), - vol.Required(ATTR_TRANSITION): vol.In((STATE_ENTRY, STATE_EXIT)), - vol.Required(ATTR_SECONDS): cv.positive_int, - } -) - -SERVICE_SET_LIGHT_SCHEMA = SERVICE_BASE_SCHEMA.extend( - {vol.Required(ATTR_ARMED_LIGHT_STATE): cv.boolean} -) - SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( {vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string} ) -SERVICE_SET_VOLUME_SCHEMA = SERVICE_BASE_SCHEMA.extend( +SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_VOLUME_PROPERTY): vol.In( - (VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT) + vol.Optional(ATTR_ALARM_DURATION): vol.All( + cv.time_period, lambda value: value.seconds, vol.Range(min=30, max=480) + ), + vol.Optional(ATTR_ALARM_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), + vol.Optional(ATTR_CHIME_VOLUME): vol.All(vol.Coerce(int), vol.In(VOLUMES)), + vol.Optional(ATTR_ENTRY_DELAY_AWAY): vol.All( + cv.time_period, lambda value: value.seconds, vol.Range(min=30, max=255) + ), + vol.Optional(ATTR_ENTRY_DELAY_HOME): vol.All( + cv.time_period, lambda value: value.seconds, vol.Range(max=255) + ), + vol.Optional(ATTR_EXIT_DELAY_AWAY): vol.All( + cv.time_period, lambda value: value.seconds, vol.Range(min=45, max=255) + ), + vol.Optional(ATTR_EXIT_DELAY_HOME): vol.All( + cv.time_period, lambda value: value.seconds, vol.Range(max=255) + ), + vol.Optional(ATTR_LIGHT): cv.boolean, + vol.Optional(ATTR_VOICE_PROMPT_VOLUME): vol.All( + vol.Coerce(int), vol.In(VOLUMES) ), - vol.Required(ATTR_VOLUME): cv.string, } ) @@ -96,7 +93,6 @@ ACCOUNT_CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CODE): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, } ) @@ -156,7 +152,6 @@ async def async_setup(hass, config): CONF_USERNAME: account[CONF_USERNAME], CONF_PASSWORD: account[CONF_PASSWORD], CONF_CODE: account.get(CONF_CODE), - CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL], }, ) ) @@ -198,7 +193,7 @@ async def async_setup_entry(hass, config_entry): async_dispatcher_send(hass, TOPIC_UPDATE) hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + hass, refresh, DEFAULT_SCAN_INTERVAL ) # Register the base station for each system: @@ -246,47 +241,6 @@ async def async_setup_entry(hass, config_entry): _LOGGER.error("Error during service call: %s", err) return - @verify_system_exists - @v3_only - @_verify_domain_control - async def set_alarm_duration(call): - """Set the duration of a running alarm.""" - system = systems[call.data[ATTR_SYSTEM_ID]] - try: - await system.set_alarm_duration(call.data[ATTR_SECONDS]) - except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) - return - - @verify_system_exists - @v3_only - @_verify_domain_control - async def set_delay(call): - """Set the delay duration for entry/exit, away/home (any combo).""" - system = systems[call.data[ATTR_SYSTEM_ID]] - coro = getattr( - system, - f"set_{call.data[ATTR_TRANSITION]}_delay_{call.data[ATTR_ARRIVAL_STATE]}", - ) - - try: - await coro(call.data[ATTR_SECONDS]) - except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) - return - - @verify_system_exists - @v3_only - @_verify_domain_control - async def set_armed_light(call): - """Turn the base station light on/off.""" - system = systems[call.data[ATTR_SYSTEM_ID]] - try: - await system.set_light(call.data[ATTR_ARMED_LIGHT_STATE]) - except SimplipyError as err: - _LOGGER.error("Error during service call: %s", err) - return - @verify_system_exists @_verify_domain_control async def set_pin(call): @@ -301,30 +255,31 @@ async def async_setup_entry(hass, config_entry): @verify_system_exists @v3_only @_verify_domain_control - async def set_volume_property(call): - """Set a volume parameter in an appropriate service call.""" + async def set_system_properties(call): + """Set one or more system parameters.""" system = systems[call.data[ATTR_SYSTEM_ID]] try: - volume = V3Volume[call.data[ATTR_VOLUME]] - except KeyError: - _LOGGER.error("Unknown volume string: %s", call.data[ATTR_VOLUME]) - return + await system.set_properties( + { + prop: value + for prop, value in call.data.items() + if prop != ATTR_SYSTEM_ID + } + ) except SimplipyError as err: _LOGGER.error("Error during service call: %s", err) return - else: - coro = getattr(system, f"set_{call.data[ATTR_VOLUME_PROPERTY]}_volume") - await coro(volume) for service, method, schema in [ ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), - ("set_alarm_duration", set_alarm_duration, SERVICE_SET_DELAY_SCHEMA), - ("set_delay", set_delay, SERVICE_SET_DELAY_SCHEMA), - ("set_armed_light", set_armed_light, SERVICE_SET_LIGHT_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), - ("set_volume_property", set_volume_property, SERVICE_SET_VOLUME_SCHEMA), + ( + "set_system_properties", + set_system_properties, + SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA, + ), ]: - hass.services.async_register(DOMAIN, service, method, schema=schema) + async_register_admin_service(hass, DOMAIN, service, method, schema=schema) return True @@ -352,6 +307,7 @@ class SimpliSafe: """Initialize.""" self._api = api self._config_entry = config_entry + self._emergency_refresh_token_used = False self._hass = hass self.last_event_data = {} self.systems = systems @@ -360,7 +316,30 @@ class SimpliSafe: """Update a system.""" try: await system.update() - latest_event = await system.get_latest_event() + except InvalidCredentialsError: + # SimpliSafe's cloud is a little shaky. At times, a 500 or 502 will + # seemingly harm simplisafe-python's existing access token _and_ refresh + # token, thus preventing the integration from recovering. However, the + # refresh token stored in the config entry escapes unscathed (again, + # apparently); so, if we detect that we're in such a situation, try a last- + # ditch effort by re-authenticating with the stored token: + if self._emergency_refresh_token_used: + # If we've already tried this, log the error, suggest a HASS restart, + # and stop the time tracker: + _LOGGER.error( + "SimpliSafe authentication disconnected. Please restart HASS." + ) + remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop( + self._config_entry.entry_id + ) + remove_listener() + return + + _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + self._emergency_refresh_token_used = True + return await self._api.refresh_access_token( + self._config_entry.data[CONF_TOKEN] + ) except SimplipyError as err: _LOGGER.error( 'SimpliSafe error while updating "%s": %s', system.address, err @@ -370,12 +349,12 @@ class SimpliSafe: _LOGGER.error('Unknown error while updating "%s": %s', system.address, err) return - self.last_event_data[system.system_id] = latest_event + self.last_event_data[system.system_id] = await system.get_latest_event() - if self._api.refresh_token_dirty: - _async_save_refresh_token( - self._hass, self._config_entry, self._api.refresh_token - ) + # If we've reached this point using an emergency refresh token, we're in the + # clear and we can discard it: + if self._emergency_refresh_token_used: + self._emergency_refresh_token_used = False async def async_update(self): """Get updated data from SimpliSafe.""" @@ -383,6 +362,11 @@ class SimpliSafe: await asyncio.gather(*tasks) + if self._api.refresh_token_dirty: + _async_save_refresh_token( + self._hass, self._config_entry, self._api.refresh_token + ) + class SimpliSafeEntity(Entity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 05dad43955c..362c0244749 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -4,6 +4,7 @@ import re from simplipy.entity import EntityTypes from simplipy.system import SystemStates +from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OFF from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -18,7 +19,9 @@ from homeassistant.const import ( CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, ) from homeassistant.util.dt import utc_from_timestamp @@ -27,7 +30,6 @@ from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) -ATTR_ALARM_ACTIVE = "alarm_active" ATTR_ALARM_DURATION = "alarm_duration" ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" @@ -48,10 +50,12 @@ ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a SimpliSafe alarm control panel based on existing config.""" - pass +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} async def async_setup_entry(hass, entry, async_add_entities): @@ -77,14 +81,13 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._simplisafe = simplisafe self._state = None - self._attrs.update({ATTR_ALARM_ACTIVE: self._system.alarm_going_off}) if self._system.version == 3: self._attrs.update( { ATTR_ALARM_DURATION: self._system.alarm_duration, - ATTR_ALARM_VOLUME: self._system.alarm_volume.name, + ATTR_ALARM_VOLUME: VOLUME_STRING_MAP[self._system.alarm_volume], ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, - ATTR_CHIME_VOLUME: self._system.chime_volume.name, + ATTR_CHIME_VOLUME: VOLUME_STRING_MAP[self._system.chime_volume], ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, @@ -92,7 +95,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): ATTR_GSM_STRENGTH: self._system.gsm_strength, ATTR_LIGHT: self._system.light, ATTR_RF_JAMMING: self._system.rf_jamming, - ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name, + ATTR_VOICE_PROMPT_VOLUME: VOLUME_STRING_MAP[ + self._system.voice_prompt_volume + ], ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, ATTR_WIFI_STRENGTH: self._system.wifi_strength, } @@ -152,10 +157,10 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): async def async_update(self): """Update alarm status.""" - event_data = self._simplisafe.last_event_data[self._system.system_id] + last_event = self._simplisafe.last_event_data[self._system.system_id] - if event_data.get("pinName"): - self._changed_by = event_data["pinName"] + if last_event.get("pinName"): + self._changed_by = last_event["pinName"] if self._system.state == SystemStates.error: self._online = False @@ -163,21 +168,23 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._online = True - if self._system.state == SystemStates.off: - self._state = STATE_ALARM_DISARMED - elif self._system.state in (SystemStates.home, SystemStates.home_count): - self._state = STATE_ALARM_ARMED_HOME + if self._system.alarm_going_off: + self._state = STATE_ALARM_TRIGGERED + elif self._system.state == SystemStates.away: + self._state = STATE_ALARM_ARMED_AWAY elif self._system.state in ( - SystemStates.away, SystemStates.away_count, SystemStates.exit_delay, + SystemStates.home_count, ): - self._state = STATE_ALARM_ARMED_AWAY + self._state = STATE_ALARM_ARMING + elif self._system.state == SystemStates.home: + self._state = STATE_ALARM_ARMED_HOME + elif self._system.state == SystemStates.off: + self._state = STATE_ALARM_DISARMED else: self._state = None - last_event = self._simplisafe.last_event_data[self._system.system_id] - try: last_event_sensor_type = EntityTypes(last_event["sensorType"]).name except ValueError: diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 6e1082948d3..9c93cd18626 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -6,17 +6,11 @@ from simplipy.errors import SimplipyError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_CODE, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TOKEN, - CONF_USERNAME, -) +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import DOMAIN @callback @@ -72,13 +66,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow): except SimplipyError: return await self._show_form({"base": "invalid_credentials"}) - scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - return self.async_create_entry( title=user_input[CONF_USERNAME], - data={ - CONF_USERNAME: username, - CONF_TOKEN: simplisafe.refresh_token, - CONF_SCAN_INTERVAL: scan_interval.seconds, - }, + data={CONF_USERNAME: username, CONF_TOKEN: simplisafe.refresh_token}, ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index ccb822e7f45..f95db72d45a 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==5.3.6"], + "requirements": ["simplisafe-python==6.1.0"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index d8a4973b49e..6d01dfd8e46 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -10,41 +10,6 @@ remove_pin: label_or_pin: description: The label/value to remove. example: Test PIN -set_alarm_duration: - description: "Set the duration (in seconds) of an active alarm" - fields: - system_id: - description: The SimpliSafe system ID to affect - example: 123987 - seconds: - description: The number of seconds to sound the alarm - example: 120 -set_delay: - description: > - Set a duration for how long the base station should delay when transitioning - between states - fields: - system_id: - description: The SimpliSafe system ID to affect - example: 123987 - arrival_state: - description: The target "arrival" state (away, home) - example: away - transition: - description: The system state transition to affect (entry, exit) - example: exit - seconds: - description: "The number of seconds to delay" - example: 120 -set_light: - description: "Turn the base station light on/off" - fields: - system_id: - description: The SimpliSafe system ID to affect - example: 123987 - armed_light_state: - description: "True for on, False for off" - example: "True" set_pin: description: Set/update a PIN fields: @@ -57,15 +22,33 @@ set_pin: pin: description: The value of the PIN example: 1256 -set_volume_property: - description: Set a level for one of the base station's various volumes +set_system_properties: + description: Set one or more system properties fields: - system_id: - description: The SimpliSafe system ID to affect - example: 123987 - volume_property: - description: The volume property to set (alarm, chime, voice_prompt) - example: voice_prompt - volume: - description: "A volume (off, low, medium, high)" - example: low + alarm_duration: + description: The length of a triggered alarm + example: 300 + alarm_volume: + description: The volume level of a triggered alarm + example: 2 + chime_volume: + description: The volume level of the door chime + example: 2 + entry_delay_away: + description: How long to delay when entering while "away" + example: 45 + entry_delay_home: + description: How long to delay when entering while "home" + example: 45 + exit_delay_away: + description: How long to delay when exiting while "away" + example: 45 + exit_delay_home: + description: How long to delay when exiting while "home" + example: 45 + light: + description: Whether the armed light should be visible + example: true + voice_prompt_volume: + description: The volume level of the voice prompt + example: 2 diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json index 5253655844b..d69362901ec 100644 --- a/homeassistant/components/sinch/manifest.json +++ b/homeassistant/components/sinch/manifest.json @@ -1,7 +1,7 @@ { "domain": "sinch", "name": "Sinch SMS", - "documentation": "https://www.home-assistant.io/components/sinch", + "documentation": "https://www.home-assistant.io/integrations/sinch", "dependencies": [], "codeowners": ["@bendikrb"], "requirements": ["clx-sdk-xms==1.0.0"] diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index c101100fbe8..c545adda281 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/sisyphus", "requirements": ["sisyphus-control==2.2.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@jkeljo"] } diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 1c4b98c2911..a56fe7ab151 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -2,7 +2,7 @@ "domain": "sma", "name": "SMA Solar", "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.3.4"], + "requirements": ["pysma==0.3.5"], "dependencies": [], "codeowners": ["@kellerza"] } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 8caebb4f871..40ec4179cd1 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -210,6 +211,7 @@ class SMAsensor(Entity): """SMA sensors are updated & don't poll.""" return False + @callback def async_update_values(self): """Update this sensor.""" update = False diff --git a/homeassistant/components/smartthings/.translations/de.json b/homeassistant/components/smartthings/.translations/de.json index dd672dee9f6..c6baac67898 100644 --- a/homeassistant/components/smartthings/.translations/de.json +++ b/homeassistant/components/smartthings/.translations/de.json @@ -19,7 +19,7 @@ "title": "Gib den pers\u00f6nlichen Zugangstoken an" }, "wait_install": { - "description": "Installieren Sie Home-Assistent SmartApp an mindestens einer Stelle, und klicken Sie auf Absenden.", + "description": "Installiere die Home-Assistent SmartApp an mindestens einer Stelle, und klicke auf Absenden.", "title": "SmartApp installieren" } }, diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 1e90709fc82..78d2c73ca73 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -32,11 +32,6 @@ ATTRIB_TO_CLASS = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add binary sensors for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 4f005a326cd..19a9e20cd6b 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -80,11 +80,6 @@ UNIT_MAP = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add climate entities for a config entry.""" ac_capabilities = [ diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 2d6eb2234f5..a41d9d6b9f7 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -33,11 +33,6 @@ VALUE_TO_STATE = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add covers for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 80d0e72fd96..aad62aed486 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -19,11 +19,6 @@ VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} SPEED_TO_VALUE = {v: k for k, v in VALUE_TO_SPEED.items()} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add fans for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 4bc3f487790..7978d85505d 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -21,11 +21,6 @@ from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add lights for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 7529f95fc34..2895bde0bf7 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -19,11 +19,6 @@ ST_LOCK_ATTR_MAP = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add locks for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/scene.py b/homeassistant/components/smartthings/scene.py index 4ecd66b1d78..a92f2f99ea3 100644 --- a/homeassistant/components/smartthings/scene.py +++ b/homeassistant/components/smartthings/scene.py @@ -4,11 +4,6 @@ from homeassistant.components.scene import Scene from .const import DATA_BROKERS, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add switches for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 3a6f9167054..38e32e90b85 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -229,11 +229,6 @@ UNITS = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} THREE_AXIS_NAMES = ["X Coordinate", "Y Coordinate", "Z Coordinate"] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add binary sensors for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/smartthings/switch.py b/homeassistant/components/smartthings/switch.py index 4d258269748..ace47a56d2c 100644 --- a/homeassistant/components/smartthings/switch.py +++ b/homeassistant/components/smartthings/switch.py @@ -9,11 +9,6 @@ from . import SmartThingsEntity from .const import DATA_BROKERS, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Platform uses config entry setup.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Add switches for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py new file mode 100644 index 00000000000..4897ef2844b --- /dev/null +++ b/homeassistant/components/sms/__init__.py @@ -0,0 +1,33 @@ +"""The sms component.""" +import logging + +import gammu # pylint: disable=import-error, no-member +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.isdevice})}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Configure Gammu state machine.""" + conf = config[DOMAIN] + device = conf.get(CONF_DEVICE) + gateway = gammu.StateMachine() # pylint: disable=no-member + try: + gateway.SetConfig(0, dict(Device=device, Connection="at")) + gateway.Init() + except gammu.GSMError as exc: # pylint: disable=no-member + _LOGGER.error("Failed to initialize, error %s", exc) + return False + else: + hass.data[DOMAIN] = gateway + return True diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py new file mode 100644 index 00000000000..aff2b704e05 --- /dev/null +++ b/homeassistant/components/sms/const.py @@ -0,0 +1,3 @@ +"""Constants for sms Component.""" + +DOMAIN = "sms" diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json new file mode 100644 index 00000000000..c58139993bb --- /dev/null +++ b/homeassistant/components/sms/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "sms", + "name": "SMS notifications via GSM-modem", + "documentation": "https://www.home-assistant.io/integrations/sms", + "requirements": ["python-gammu==2.12"], + "dependencies": [], + "codeowners": ["@ocalvo"] +} diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py new file mode 100644 index 00000000000..0a47e0aad25 --- /dev/null +++ b/homeassistant/components/sms/notify.py @@ -0,0 +1,47 @@ +"""Support for SMS notification services.""" +import logging + +import gammu # pylint: disable=import-error, no-member +import voluptuous as vol + +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService +from homeassistant.const import CONF_NAME, CONF_RECIPIENT +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_RECIPIENT): cv.string, vol.Optional(CONF_NAME): cv.string} +) + + +def get_service(hass, config, discovery_info=None): + """Get the SMS notification service.""" + gateway = hass.data[DOMAIN] + number = config[CONF_RECIPIENT] + return SMSNotificationService(gateway, number) + + +class SMSNotificationService(BaseNotificationService): + """Implement the notification service for SMS.""" + + def __init__(self, gateway, number): + """Initialize the service.""" + self.gateway = gateway + self.number = number + + def send_message(self, message="", **kwargs): + """Send SMS message.""" + # Prepare message data + # We tell that we want to use first SMSC number stored in phone + gammu_message = { + "Text": message, + "SMSC": {"Location": 1}, + "Number": self.number, + } + try: + self.gateway.SendSMS(gammu_message) + except gammu.GSMError as exc: # pylint: disable=no-member + _LOGGER.error("Sending to %s failed: %s", self.number, exc) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 60cabaf38f0..f2464489627 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -21,11 +21,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old configuration.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Add an solarEdge entry.""" # Add the needed sensors to hass diff --git a/homeassistant/components/solarlog/.translations/de.json b/homeassistant/components/solarlog/.translations/de.json index 5a1b384ed27..3e71154b383 100644 --- a/homeassistant/components/solarlog/.translations/de.json +++ b/homeassistant/components/solarlog/.translations/de.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfen Sie die Host-Adresse" + "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfe die Host-Adresse" }, "step": { "user": { @@ -13,7 +13,7 @@ "host": "Der Hostname oder die IP-Adresse Ihres Solar-Log-Ger\u00e4ts", "name": "Das Pr\u00e4fix, das f\u00fcr Ihre Solar-Log-Sensoren verwendet werden soll" }, - "title": "Definieren Sie Ihre Solar-Log-Verbindung" + "title": "Definiere deine Solar-Log-Verbindung" } }, "title": "Solar-Log" diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json index 9331628e027..b626da456a9 100644 --- a/homeassistant/components/solarlog/manifest.json +++ b/homeassistant/components/solarlog/manifest.json @@ -2,7 +2,7 @@ "domain": "solarlog", "name": "Solar-Log", "config_flow": true, - "documentation": "https://www.home-assistant.io/integration/solarlog", + "documentation": "https://www.home-assistant.io/integrations/solarlog", "dependencies": [], "codeowners": ["@Ernst79"], "requirements": ["sunwatcher==0.2.1"] diff --git a/homeassistant/components/soma/.translations/cs.json b/homeassistant/components/soma/.translations/cs.json index b3922b67795..42a8bddf841 100644 --- a/homeassistant/components/soma/.translations/cs.json +++ b/homeassistant/components/soma/.translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 397531562b1..a724a3d4485 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -2,7 +2,7 @@ "domain": "soma", "name": "Soma Connect", "config_flow": true, - "documentation": "", + "documentation": "https://www.home-assistant.io/integrations/soma", "dependencies": [], "codeowners": ["@ratsept"], "requirements": ["pysoma==0.0.10"] diff --git a/homeassistant/components/somfy/.translations/cs.json b/homeassistant/components/somfy/.translations/cs.json index 7ba035f562e..bf8a3bf916e 100644 --- a/homeassistant/components/somfy/.translations/cs.json +++ b/homeassistant/components/somfy/.translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el" + }, "step": { "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index d2c6210f01c..c3a977e32e1 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,13 +1,10 @@ """Support to embed Sonos.""" -import asyncio - import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS +from homeassistant.const import CONF_HOSTS from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN @@ -33,91 +30,12 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" -SERVICE_SET_TIMER = "set_sleep_timer" -SERVICE_CLEAR_TIMER = "clear_sleep_timer" -SERVICE_UPDATE_ALARM = "update_alarm" -SERVICE_SET_OPTION = "set_option" -SERVICE_PLAY_QUEUE = "play_queue" - -ATTR_SLEEP_TIME = "sleep_time" -ATTR_ALARM_ID = "alarm_id" -ATTR_VOLUME = "volume" -ATTR_ENABLED = "enabled" -ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" -ATTR_MASTER = "master" -ATTR_WITH_GROUP = "with_group" -ATTR_NIGHT_SOUND = "night_sound" -ATTR_SPEECH_ENHANCE = "speech_enhance" -ATTR_QUEUE_POSITION = "queue_position" - -SONOS_JOIN_SCHEMA = vol.Schema( - { - vol.Required(ATTR_MASTER): cv.entity_id, - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - } -) - -SONOS_UNJOIN_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids}) - -SONOS_STATES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean, - } -) - -SONOS_SET_TIMER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_SLEEP_TIME): vol.All( - vol.Coerce(int), vol.Range(min=0, max=86399) - ), - } -) - -SONOS_CLEAR_TIMER_SCHEMA = vol.Schema( - {vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids} -) - -SONOS_UPDATE_ALARM_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Required(ATTR_ALARM_ID): cv.positive_int, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_VOLUME): cv.small_float, - vol.Optional(ATTR_ENABLED): cv.boolean, - vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, - } -) - -SONOS_SET_OPTION_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, - vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, - } -) - -SONOS_PLAY_QUEUE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, - vol.Optional(ATTR_QUEUE_POSITION, default=0): cv.positive_int, - } -) - -DATA_SERVICE_EVENT = "sonos_service_idle" - async def async_setup(hass, config): """Set up the Sonos component.""" conf = config.get(DOMAIN) hass.data[DOMAIN] = conf or {} - hass.data[DATA_SERVICE_EVENT] = asyncio.Event() if conf is not None: hass.async_create_task( @@ -126,48 +44,6 @@ async def async_setup(hass, config): ) ) - async def service_handle(service): - """Dispatch a service call.""" - hass.data[DATA_SERVICE_EVENT].clear() - async_dispatcher_send(hass, DOMAIN, service.service, service.data) - await hass.data[DATA_SERVICE_EVENT].wait() - - hass.services.async_register( - DOMAIN, SERVICE_JOIN, service_handle, schema=SONOS_JOIN_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_UNJOIN, service_handle, schema=SONOS_UNJOIN_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=SONOS_STATES_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_RESTORE, service_handle, schema=SONOS_STATES_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_TIMER, service_handle, schema=SONOS_SET_TIMER_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CLEAR_TIMER, service_handle, schema=SONOS_CLEAR_TIMER_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_UPDATE_ALARM, service_handle, schema=SONOS_UPDATE_ALARM_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_OPTION, service_handle, schema=SONOS_SET_OPTION_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_PLAY_QUEUE, service_handle, schema=SONOS_PLAY_QUEUE_SCHEMA - ) - return True diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 9ce72d87dfe..bcdb74ad438 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -11,6 +11,7 @@ import pysonos from pysonos import alarms from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot +import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -30,42 +31,16 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ( - ENTITY_MATCH_ALL, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import ServiceCall, callback +from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.util.dt import utcnow from . import ( - ATTR_ALARM_ID, - ATTR_ENABLED, - ATTR_INCLUDE_LINKED_ZONES, - ATTR_MASTER, - ATTR_NIGHT_SOUND, - ATTR_QUEUE_POSITION, - ATTR_SLEEP_TIME, - ATTR_SPEECH_ENHANCE, - ATTR_TIME, - ATTR_VOLUME, - ATTR_WITH_GROUP, CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR, - DATA_SERVICE_EVENT, DOMAIN as SONOS_DOMAIN, - SERVICE_CLEAR_TIMER, - SERVICE_JOIN, - SERVICE_PLAY_QUEUE, - SERVICE_RESTORE, - SERVICE_SET_OPTION, - SERVICE_SET_TIMER, - SERVICE_SNAPSHOT, - SERVICE_UNJOIN, - SERVICE_UPDATE_ALARM, ) _LOGGER = logging.getLogger(__name__) @@ -97,6 +72,27 @@ ATTR_SONOS_GROUP = "sonos_group" UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] +SERVICE_JOIN = "join" +SERVICE_UNJOIN = "unjoin" +SERVICE_SNAPSHOT = "snapshot" +SERVICE_RESTORE = "restore" +SERVICE_SET_TIMER = "set_sleep_timer" +SERVICE_CLEAR_TIMER = "clear_sleep_timer" +SERVICE_UPDATE_ALARM = "update_alarm" +SERVICE_SET_OPTION = "set_option" +SERVICE_PLAY_QUEUE = "play_queue" + +ATTR_SLEEP_TIME = "sleep_time" +ATTR_ALARM_ID = "alarm_id" +ATTR_VOLUME = "volume" +ATTR_ENABLED = "enabled" +ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" +ATTR_MASTER = "master" +ATTR_WITH_GROUP = "with_group" +ATTR_NIGHT_SOUND = "night_sound" +ATTR_SPEECH_ENHANCE = "speech_enhance" +ATTR_QUEUE_POSITION = "queue_position" + class SonosData: """Storage class for platform global data.""" @@ -176,46 +172,101 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.debug("Adding discovery job") hass.async_add_executor_job(_discovery) - async def async_service_handle(service, data): + platform = entity_platform.current_platform.get() + + async def async_service_handle(service_call: ServiceCall): """Handle dispatched services.""" - entity_ids = data.get("entity_id") - entities = hass.data[DATA_SONOS].entities - if entity_ids and entity_ids != ENTITY_MATCH_ALL: - entities = [e for e in entities if e.entity_id in entity_ids] + entities = await platform.async_extract_from_service(service_call) - if service == SERVICE_JOIN: - master = [ - e - for e in hass.data[DATA_SONOS].entities - if e.entity_id == data[ATTR_MASTER] - ] + if not entities: + return + + if service_call.service == SERVICE_JOIN: + master = platform.entities.get(service_call.data[ATTR_MASTER]) if master: - await SonosEntity.join_multi(hass, master[0], entities) - elif service == SERVICE_UNJOIN: + await SonosEntity.join_multi(hass, master, entities) + else: + _LOGGER.error( + "Invalid master specified for join service: %s", + service_call.data[ATTR_MASTER], + ) + elif service_call.service == SERVICE_UNJOIN: await SonosEntity.unjoin_multi(hass, entities) - elif service == SERVICE_SNAPSHOT: - await SonosEntity.snapshot_multi(hass, entities, data[ATTR_WITH_GROUP]) - elif service == SERVICE_RESTORE: - await SonosEntity.restore_multi(hass, entities, data[ATTR_WITH_GROUP]) - else: - for entity in entities: - if service == SERVICE_SET_TIMER: - call = entity.set_sleep_timer - elif service == SERVICE_CLEAR_TIMER: - call = entity.clear_sleep_timer - elif service == SERVICE_UPDATE_ALARM: - call = entity.set_alarm - elif service == SERVICE_SET_OPTION: - call = entity.set_option - elif service == SERVICE_PLAY_QUEUE: - call = entity.play_queue + elif service_call.service == SERVICE_SNAPSHOT: + await SonosEntity.snapshot_multi( + hass, entities, service_call.data[ATTR_WITH_GROUP] + ) + elif service_call.service == SERVICE_RESTORE: + await SonosEntity.restore_multi( + hass, entities, service_call.data[ATTR_WITH_GROUP] + ) - hass.async_add_executor_job(call, data) + service.async_register_admin_service( + hass, + SONOS_DOMAIN, + SERVICE_JOIN, + async_service_handle, + cv.make_entity_service_schema({vol.Required(ATTR_MASTER): cv.entity_id}), + ) - # We are ready for the next service call - hass.data[DATA_SERVICE_EVENT].set() + service.async_register_admin_service( + hass, + SONOS_DOMAIN, + SERVICE_UNJOIN, + async_service_handle, + cv.make_entity_service_schema({}), + ) - async_dispatcher_connect(hass, SONOS_DOMAIN, async_service_handle) + join_unjoin_schema = cv.make_entity_service_schema( + {vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean} + ) + + service.async_register_admin_service( + hass, SONOS_DOMAIN, SERVICE_SNAPSHOT, async_service_handle, join_unjoin_schema + ) + + service.async_register_admin_service( + hass, SONOS_DOMAIN, SERVICE_RESTORE, async_service_handle, join_unjoin_schema + ) + + platform.async_register_entity_service( + SERVICE_SET_TIMER, + { + vol.Required(ATTR_SLEEP_TIME): vol.All( + vol.Coerce(int), vol.Range(min=0, max=86399) + ) + }, + "set_sleep_timer", + ) + + platform.async_register_entity_service(SERVICE_CLEAR_TIMER, {}, "clear_sleep_timer") + + platform.async_register_entity_service( + SERVICE_UPDATE_ALARM, + { + vol.Required(ATTR_ALARM_ID): cv.positive_int, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_VOLUME): cv.small_float, + vol.Optional(ATTR_ENABLED): cv.boolean, + vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean, + }, + "set_alarm", + ) + + platform.async_register_entity_service( + SERVICE_SET_OPTION, + { + vol.Optional(ATTR_NIGHT_SOUND): cv.boolean, + vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean, + }, + "set_option", + ) + + platform.async_register_entity_service( + SERVICE_PLAY_QUEUE, + {vol.Optional(ATTR_QUEUE_POSITION): cv.positive_int}, + "play_queue", + ) class _ProcessSonosEventQueue: @@ -480,10 +531,10 @@ class SonosEntity(MediaPlayerDevice): player = self.soco - def subscribe(service, action): + def subscribe(sonos_service, action): """Add a subscription to a pysonos service.""" queue = _ProcessSonosEventQueue(action) - sub = service.subscribe(auto_renew=True, event_queue=queue) + sub = sonos_service.subscribe(auto_renew=True, event_queue=queue) self._subscriptions.append(sub) subscribe(player.avTransport, self.update_media) @@ -1147,52 +1198,53 @@ class SonosEntity(MediaPlayerDevice): @soco_error() @soco_coordinator - def set_sleep_timer(self, data): + def set_sleep_timer(self, sleep_time): """Set the timer on the player.""" - self.soco.set_sleep_timer(data[ATTR_SLEEP_TIME]) + self.soco.set_sleep_timer(sleep_time) @soco_error() @soco_coordinator - def clear_sleep_timer(self, data): + def clear_sleep_timer(self): """Clear the timer on the player.""" self.soco.set_sleep_timer(None) @soco_error() @soco_coordinator - def set_alarm(self, data): + def set_alarm( + self, alarm_id, time=None, volume=None, enabled=None, include_linked_zones=None + ): """Set the alarm clock on the player.""" - alarm = None for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access - if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): + if one_alarm._alarm_id == str(alarm_id): alarm = one_alarm if alarm is None: - _LOGGER.warning("did not find alarm with id %s", data[ATTR_ALARM_ID]) + _LOGGER.warning("did not find alarm with id %s", alarm_id) return - if ATTR_TIME in data: - alarm.start_time = data[ATTR_TIME] - if ATTR_VOLUME in data: - alarm.volume = int(data[ATTR_VOLUME] * 100) - if ATTR_ENABLED in data: - alarm.enabled = data[ATTR_ENABLED] - if ATTR_INCLUDE_LINKED_ZONES in data: - alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES] + if time is not None: + alarm.start_time = time + if volume is not None: + alarm.volume = int(volume * 100) + if enabled is not None: + alarm.enabled = enabled + if include_linked_zones is not None: + alarm.include_linked_zones = include_linked_zones alarm.save() @soco_error() - def set_option(self, data): + def set_option(self, night_sound=None, speech_enhance=None): """Modify playback options.""" - if ATTR_NIGHT_SOUND in data and self._night_sound is not None: - self.soco.night_mode = data[ATTR_NIGHT_SOUND] + if night_sound is not None and self._night_sound is not None: + self.soco.night_mode = night_sound - if ATTR_SPEECH_ENHANCE in data and self._speech_enhance is not None: - self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE] + if speech_enhance is not None and self._speech_enhance is not None: + self.soco.dialog_mode = speech_enhance @soco_error() - def play_queue(self, data): + def play_queue(self, queue_position=0): """Start playing the queue.""" - self.soco.play_from_queue(data[ATTR_QUEUE_POSITION]) + self.soco.play_from_queue(queue_position) @property def device_state_attributes(self): diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 2104f931c0a..b5ff14ce01d 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -62,15 +62,8 @@ class SpcBinarySensor(BinarySensorDevice): @property def is_on(self): """Whether the device is switched on.""" - return self._zone.input == ZoneInput.OPEN - @property - def hidden(self) -> bool: - """Whether the device is hidden by default.""" - # These type of sensors are probably mainly used for automations - return True - @property def should_poll(self): """No polling needed.""" diff --git a/homeassistant/components/spotify/.translations/cs.json b/homeassistant/components/spotify/.translations/cs.json new file mode 100644 index 00000000000..bcb73eb66b0 --- /dev/null +++ b/homeassistant/components/spotify/.translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "M\u016f\u017eete nakonfigurovat pouze jeden \u00fa\u010det Spotify.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Integrace Spotify nen\u00ed nakonfigurov\u00e1na. Postupujte podle n\u00e1vodu." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno pomoc\u00ed Spotify." + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/da.json b/homeassistant/components/spotify/.translations/da.json new file mode 100644 index 00000000000..f4f4950317a --- /dev/null +++ b/homeassistant/components/spotify/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en enkelt Spotify-konto.", + "authorize_url_timeout": "Timeout ved generering af godkendelses-url.", + "missing_configuration": "Spotify-integrationen er ikke konfigureret. F\u00f8lg venligst dokumentationen." + }, + "create_entry": { + "default": "Godkendt med Spotify." + }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/en.json b/homeassistant/components/spotify/.translations/en.json new file mode 100644 index 00000000000..b26b2b6daf5 --- /dev/null +++ b/homeassistant/components/spotify/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Spotify account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Spotify integration is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Spotify." + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/es.json b/homeassistant/components/spotify/.translations/es.json new file mode 100644 index 00000000000..1e8a90246eb --- /dev/null +++ b/homeassistant/components/spotify/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puedes configurar una cuenta de Spotify.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autentificado con \u00e9xito con Spotify." + }, + "step": { + "pick_implementation": { + "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/fr.json b/homeassistant/components/spotify/.translations/fr.json new file mode 100644 index 00000000000..b6ec983df76 --- /dev/null +++ b/homeassistant/components/spotify/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Spotify.", + "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie avec Spotify." + }, + "step": { + "pick_implementation": { + "title": "Choisissez la m\u00e9thode d'authentification" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/hu.json b/homeassistant/components/spotify/.translations/hu.json new file mode 100644 index 00000000000..414c82751b5 --- /dev/null +++ b/homeassistant/components/spotify/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Csak egy Spotify-fi\u00f3kot konfigur\u00e1lhat.", + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." + }, + "create_entry": { + "default": "A Spotify sikeresen hiteles\u00edtett." + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/it.json b/homeassistant/components/spotify/.translations/it.json new file mode 100644 index 00000000000..ffe78aa0c02 --- /dev/null +++ b/homeassistant/components/spotify/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account di Spotify.", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione", + "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Spotify." + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/ko.json b/homeassistant/components/spotify/.translations/ko.json new file mode 100644 index 00000000000..af151ecc2d0 --- /dev/null +++ b/homeassistant/components/spotify/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Spotify \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Spotify \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "Spotify \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/no.json b/homeassistant/components/spotify/.translations/no.json new file mode 100644 index 00000000000..69b046cad0c --- /dev/null +++ b/homeassistant/components/spotify/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Spotify-konto.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Spotify-integrasjonen er ikke konfigurert. F\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket autentisering med Spotify." + }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/pl.json b/homeassistant/components/spotify/.translations/pl.json new file mode 100644 index 00000000000..1f2e1213882 --- /dev/null +++ b/homeassistant/components/spotify/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Spotify.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Integracja ze Spotify nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Spotify" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelnienia" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/ru.json b/homeassistant/components/spotify/.translations/ru.json new file mode 100644 index 00000000000..b19f226d8bb --- /dev/null +++ b/homeassistant/components/spotify/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, + "title": "Spotify" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/sv.json b/homeassistant/components/spotify/.translations/sv.json new file mode 100644 index 00000000000..47e5b85c93c --- /dev/null +++ b/homeassistant/components/spotify/.translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan endast konfigurera ett Spotify-konto.", + "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", + "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + }, + "create_entry": { + "default": "Lyckad autentisering med Spotify." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod." + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index fdfce7e498b..9e5feb1c582 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -1 +1,97 @@ -"""The spotify component.""" +"""The spotify integration.""" + +from spotipy import Spotify, SpotifyException +import voluptuous as vol + +from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.spotify import config_flow +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_CREDENTIALS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + DATA_SPOTIFY_CLIENT, + DATA_SPOTIFY_ME, + DATA_SPOTIFY_SESSION, + DOMAIN, +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Spotify integration.""" + if DOMAIN not in config: + return True + + if CONF_CLIENT_ID in config[DOMAIN]: + config_flow.SpotifyFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + "https://accounts.spotify.com/authorize", + "https://accounts.spotify.com/api/token", + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Spotify from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + session = OAuth2Session(hass, entry, implementation) + await session.async_ensure_token_valid() + spotify = Spotify(auth=session.token["access_token"]) + + try: + current_user = await hass.async_add_executor_job(spotify.me) + except SpotifyException: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = { + DATA_SPOTIFY_CLIENT: spotify, + DATA_SPOTIFY_ME: current_user, + DATA_SPOTIFY_SESSION: session, + } + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Spotify config entry.""" + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, MEDIA_PLAYER_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py new file mode 100644 index 00000000000..d619d3b2b10 --- /dev/null +++ b/homeassistant/components/spotify/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for Spotify.""" +import logging + +from spotipy import Spotify + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class SpotifyFlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Spotify OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + scopes = [ + # Needed to be able to control playback + "user-modify-playback-state", + # Needed in order to read available devices + "user-read-playback-state", + # Needed to determine if the user has Spotify Premium + "user-read-private", + ] + return {"scope": ",".join(scopes)} + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for Spotify.""" + spotify = Spotify(auth=data["token"]["access_token"]) + + try: + current_user = await self.hass.async_add_executor_job(spotify.current_user) + except Exception: # pylint: disable=broad-except + return self.async_abort(reason="connection_error") + + name = data["id"] = current_user["id"] + + if current_user.get("display_name"): + name = current_user["display_name"] + data["name"] = name + + await self.async_set_unique_id(current_user["id"]) + + return self.async_create_entry(title=name, data=data) diff --git a/homeassistant/components/spotify/const.py b/homeassistant/components/spotify/const.py new file mode 100644 index 00000000000..37bd1a2bf81 --- /dev/null +++ b/homeassistant/components/spotify/const.py @@ -0,0 +1,10 @@ +"""Define constants for the Spotify integration.""" + +DOMAIN = "spotify" + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" + +DATA_SPOTIFY_CLIENT = "spotify_client" +DATA_SPOTIFY_ME = "spotify_me" +DATA_SPOTIFY_SESSION = "spotify_session" diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index ab41becea65..be58d2bab40 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,10 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy-homeassistant==2.4.4.dev1"], - "dependencies": ["configurator", "http"], - "codeowners": [] + "requirements": ["spotipy==2.7.1"], + "zeroconf": ["_spotify-connect._tcp.local."], + "dependencies": ["http"], + "codeowners": ["@frenck"], + "config_flow": true, + "quality_scale": "silver" } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ba0c725eb7f..8bd5782f7ee 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -1,16 +1,15 @@ """Support for interacting with Spotify Connect.""" +from asyncio import run_coroutine_threadsafe +import datetime as dt from datetime import timedelta import logging -import random +from typing import Any, Callable, Dict, List, Optional -import spotipy -import spotipy.oauth2 -import voluptuous as vol +from aiohttp import ClientError +from spotipy import Spotify, SpotifyException -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -18,374 +17,325 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, ) -from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ID, + CONF_NAME, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util.dt import utc_from_timestamp + +from .const import DATA_SPOTIFY_CLIENT, DATA_SPOTIFY_ME, DATA_SPOTIFY_SESSION, DOMAIN _LOGGER = logging.getLogger(__name__) -AUTH_CALLBACK_NAME = "api:spotify" -AUTH_CALLBACK_PATH = "/api/spotify" - -CONF_ALIASES = "aliases" -CONF_CACHE_PATH = "cache_path" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" - -CONFIGURATOR_DESCRIPTION = ( - "To link your Spotify account, click the link, login, and authorize:" -) -CONFIGURATOR_LINK_NAME = "Link Spotify account" -CONFIGURATOR_SUBMIT_CAPTION = "I authorized successfully" - -DEFAULT_CACHE_PATH = ".spotify-token-cache" -DEFAULT_NAME = "Spotify" -DOMAIN = "spotify" - -SERVICE_PLAY_PLAYLIST = "play_playlist" -ATTR_RANDOM_SONG = "random_song" - -PLAY_PLAYLIST_SCHEMA = vol.Schema( - { - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_RANDOM_SONG, default=False): cv.boolean, - } -) - ICON = "mdi:spotify" SCAN_INTERVAL = timedelta(seconds=30) -SCOPE = "user-read-playback-state user-modify-playback-state user-read-private" - SUPPORT_SPOTIFY = ( - SUPPORT_VOLUME_SET + SUPPORT_NEXT_TRACK | SUPPORT_PAUSE | SUPPORT_PLAY - | SUPPORT_NEXT_TRACK - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SEEK + | SUPPORT_SELECT_SOURCE | SUPPORT_SHUFFLE_SET -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_CACHE_PATH): cv.string, - vol.Optional(CONF_ALIASES, default={}): {cv.string: cv.string}, - } + | SUPPORT_VOLUME_SET ) -def request_configuration(hass, config, add_entities, oauth): - """Request Spotify authorization.""" - configurator = hass.components.configurator - hass.data[DOMAIN] = configurator.request_config( - DEFAULT_NAME, - lambda _: None, - link_name=CONFIGURATOR_LINK_NAME, - link_url=oauth.get_authorize_url(), - description=CONFIGURATOR_DESCRIPTION, - submit_caption=CONFIGURATOR_SUBMIT_CAPTION, +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Spotify based on a config entry.""" + spotify = SpotifyMediaPlayer( + hass.data[DOMAIN][entry.entry_id][DATA_SPOTIFY_SESSION], + hass.data[DOMAIN][entry.entry_id][DATA_SPOTIFY_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_SPOTIFY_ME], + entry.data[CONF_ID], + entry.data[CONF_NAME], ) + async_add_entities([spotify], True) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Spotify platform.""" +def spotify_exception_handler(func): + """Decorate Spotify calls to handle Spotify exception. - callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) - oauth = spotipy.oauth2.SpotifyOAuth( - config.get(CONF_CLIENT_ID), - config.get(CONF_CLIENT_SECRET), - callback_url, - scope=SCOPE, - cache_path=cache, - ) - token_info = oauth.get_cached_token() - if not token_info: - _LOGGER.info("no token; requesting authorization") - hass.http.register_view(SpotifyAuthCallbackView(config, add_entities, oauth)) - request_configuration(hass, config, add_entities, oauth) - return - if hass.data.get(DOMAIN): - configurator = hass.components.configurator - configurator.request_done(hass.data.get(DOMAIN)) - del hass.data[DOMAIN] - player = SpotifyMediaPlayer( - oauth, config.get(CONF_NAME, DEFAULT_NAME), config[CONF_ALIASES] - ) - add_entities([player], True) + A decorator that wraps the passed in function, catches Spotify errors, + aiohttp exceptions and handles the availability of the media player. + """ - def play_playlist_service(service): - media_content_id = service.data[ATTR_MEDIA_CONTENT_ID] - random_song = service.data.get(ATTR_RANDOM_SONG) - player.play_playlist(media_content_id, random_song) + def wrapper(self, *args, **kwargs): + try: + result = func(self, *args, **kwargs) + self.player_available = True + return result + except (SpotifyException, ClientError): + self.player_available = False - hass.services.register( - DOMAIN, - SERVICE_PLAY_PLAYLIST, - play_playlist_service, - schema=PLAY_PLAYLIST_SCHEMA, - ) - - -class SpotifyAuthCallbackView(HomeAssistantView): - """Spotify Authorization Callback View.""" - - requires_auth = False - url = AUTH_CALLBACK_PATH - name = AUTH_CALLBACK_NAME - - def __init__(self, config, add_entities, oauth): - """Initialize.""" - self.config = config - self.add_entities = add_entities - self.oauth = oauth - - @callback - def get(self, request): - """Receive authorization token.""" - hass = request.app["hass"] - self.oauth.get_access_token(request.query["code"]) - hass.async_add_job(setup_platform, hass, self.config, self.add_entities) + return wrapper class SpotifyMediaPlayer(MediaPlayerDevice): """Representation of a Spotify controller.""" - def __init__(self, oauth, name, aliases): + def __init__(self, session, spotify: Spotify, me: dict, user_id: str, name: str): """Initialize.""" - self._name = name - self._oauth = oauth - self._album = None - self._title = None - self._artist = None - self._uri = None - self._image_url = None - self._state = None - self._current_device = None - self._devices = {} - self._volume = None - self._shuffle = False - self._player = None - self._user = None - self._aliases = aliases - self._token_info = self._oauth.get_cached_token() + self._id = user_id + self._me = me + self._name = f"Spotify {name}" + self._session = session + self._spotify = spotify - def refresh_spotify_instance(self): - """Fetch a new spotify instance.""" + self._currently_playing: Optional[dict] = {} + self._devices: Optional[List[dict]] = [] + self._playlist: Optional[dict] = None + self._spotify: Spotify = None - token_refreshed = False - need_token = self._token_info is None or self._oauth.is_token_expired( - self._token_info + self.player_available = False + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def icon(self) -> str: + """Return the icon.""" + return ICON + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.player_available + + @property + def unique_id(self) -> str: + """Return the unique ID.""" + return self._id + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this entity.""" + if self._me is not None: + model = self._me["product"] + + return { + "identifiers": {(DOMAIN, self._id)}, + "manufacturer": "Spotify AB", + "model": f"Spotify {model}".rstrip(), + "name": self._name, + } + + @property + def state(self) -> Optional[str]: + """Return the playback state.""" + if not self._currently_playing: + return STATE_IDLE + if self._currently_playing["is_playing"]: + return STATE_PLAYING + return STATE_PAUSED + + @property + def volume_level(self) -> Optional[float]: + """Return the device volume.""" + return self._currently_playing.get("device", {}).get("volume_percent", 0) / 100 + + @property + def media_content_id(self) -> Optional[str]: + """Return the media URL.""" + return self._currently_playing.get("item", {}).get("name") + + @property + def media_content_type(self) -> Optional[str]: + """Return the media type.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self) -> Optional[int]: + """Duration of current playing media in seconds.""" + if self._currently_playing.get("item") is None: + return None + return self._currently_playing["item"]["duration_ms"] / 1000 + + @property + def media_position(self) -> Optional[str]: + """Position of current playing media in seconds.""" + if not self._currently_playing: + return None + return self._currently_playing["progress_ms"] / 1000 + + @property + def media_position_updated_at(self) -> Optional[dt.datetime]: + """When was the position of the current playing media valid.""" + if not self._currently_playing: + return None + return utc_from_timestamp(self._currently_playing["timestamp"] / 1000) + + @property + def media_image_url(self) -> Optional[str]: + """Return the media image URL.""" + if ( + self._currently_playing.get("item") is None + or not self._currently_playing["item"]["album"]["images"] + ): + return None + return self._currently_playing["item"]["album"]["images"][0]["url"] + + @property + def media_image_remotely_accessible(self) -> bool: + """If the image url is remotely accessible.""" + return False + + @property + def media_title(self) -> Optional[str]: + """Return the media title.""" + return self._currently_playing.get("item", {}).get("name") + + @property + def media_artist(self) -> Optional[str]: + """Return the media artist.""" + if self._currently_playing.get("item") is None: + return None + return ", ".join( + [artist["name"] for artist in self._currently_playing["item"]["artists"]] ) - if need_token: - new_token = self._oauth.refresh_access_token( - self._token_info["refresh_token"] - ) - # skip when refresh failed - if new_token is None: - return - self._token_info = new_token - token_refreshed = True - if self._player is None or token_refreshed: - self._player = spotipy.Spotify(auth=self._token_info.get("access_token")) - self._user = self._player.me() + @property + def media_album_name(self) -> Optional[str]: + """Return the media album.""" + if self._currently_playing.get("item") is None: + return None + return self._currently_playing["item"]["album"]["name"] - def update(self): - """Update state and attributes.""" - self.refresh_spotify_instance() + @property + def media_track(self) -> Optional[int]: + """Track number of current playing media, music track only.""" + return self._currently_playing.get("item", {}).get("track_number") - # Don't true update when token is expired - if self._oauth.is_token_expired(self._token_info): - _LOGGER.warning("Spotify failed to update, token expired.") - return + @property + def media_playlist(self): + """Title of Playlist currently playing.""" + if self._playlist is None: + return None + return self._playlist["name"] - # Available devices - player_devices = self._player.devices() - if player_devices is not None: - devices = player_devices.get("devices") - if devices is not None: - old_devices = self._devices - self._devices = { - self._aliases.get(device.get("id"), device.get("name")): device.get( - "id" - ) - for device in devices - } - device_diff = { - name: id - for name, id in self._devices.items() - if old_devices.get(name, None) is None - } - if device_diff: - _LOGGER.info("New Devices: %s", str(device_diff)) - # Current playback state - current = self._player.current_playback() - if current is None: - self._state = STATE_IDLE - return - # Track metadata - item = current.get("item") - if item: - self._album = item.get("album").get("name") - self._title = item.get("name") - self._artist = ", ".join( - [artist.get("name") for artist in item.get("artists")] - ) - self._uri = item.get("uri") - images = item.get("album").get("images") - self._image_url = images[0].get("url") if images else None - # Playing state - self._state = STATE_PAUSED - if current.get("is_playing"): - self._state = STATE_PLAYING - self._shuffle = current.get("shuffle_state") - device = current.get("device") - if device is None: - self._state = STATE_IDLE - else: - if device.get("volume_percent"): - self._volume = device.get("volume_percent") / 100 - if device.get("name"): - self._current_device = device.get("name") + @property + def source(self) -> Optional[str]: + """Return the current playback device.""" + return self._currently_playing.get("device", {}).get("name") - def set_volume_level(self, volume): + @property + def source_list(self) -> Optional[List[str]]: + """Return a list of source devices.""" + if not self._devices: + return None + return [device["name"] for device in self._devices] + + @property + def shuffle(self) -> bool: + """Shuffling state.""" + return bool(self._currently_playing.get("shuffle_state")) + + @property + def supported_features(self) -> int: + """Return the media player features that are supported.""" + if self._me["product"] != "premium": + return 0 + return SUPPORT_SPOTIFY + + @spotify_exception_handler + def set_volume_level(self, volume: int) -> None: """Set the volume level.""" - self._player.volume(int(volume * 100)) + self._spotify.volume(int(volume * 100)) - def set_shuffle(self, shuffle): - """Enable/Disable shuffle mode.""" - self._player.shuffle(shuffle) - - def media_next_track(self): - """Skip to next track.""" - self._player.next_track() - - def media_previous_track(self): - """Skip to previous track.""" - self._player.previous_track() - - def media_play(self): + @spotify_exception_handler + def media_play(self) -> None: """Start or resume playback.""" - self._player.start_playback() + self._spotify.start_playback() - def media_pause(self): + @spotify_exception_handler + def media_pause(self) -> None: """Pause playback.""" - self._player.pause_playback() + self._spotify.pause_playback() - def select_source(self, source): - """Select playback device.""" - if self._devices: - self._player.transfer_playback( - self._devices[source], self._state == STATE_PLAYING - ) + @spotify_exception_handler + def media_previous_track(self) -> None: + """Skip to previous track.""" + self._spotify.previous_track() - def play_media(self, media_type, media_id, **kwargs): + @spotify_exception_handler + def media_next_track(self) -> None: + """Skip to next track.""" + self._spotify.next_track() + + @spotify_exception_handler + def media_seek(self, position): + """Send seek command.""" + self._spotify.seek_track(int(position * 1000)) + + @spotify_exception_handler + def play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" kwargs = {} + if media_type == MEDIA_TYPE_MUSIC: kwargs["uris"] = [media_id] elif media_type == MEDIA_TYPE_PLAYLIST: kwargs["context_uri"] = media_id else: - _LOGGER.error("media type %s is not supported", media_type) + _LOGGER.error("Media type %s is not supported", media_type) return - if not media_id.startswith("spotify:"): - _LOGGER.error("media id must be spotify uri") + + self._spotify.start_playback(**kwargs) + + @spotify_exception_handler + def select_source(self, source: str) -> None: + """Select playback device.""" + for device in self._devices: + if device["name"] == source: + self._spotify.transfer_playback( + device["id"], self.state == STATE_PLAYING + ) + return + + @spotify_exception_handler + def set_shuffle(self, shuffle: bool) -> None: + """Enable/Disable shuffle mode.""" + self._spotify.shuffle(shuffle) + + @spotify_exception_handler + def update(self) -> None: + """Update state and attributes.""" + if not self.enabled: return - self._player.start_playback(**kwargs) - def play_playlist(self, media_id, random_song): - """Play random music in a playlist.""" - if not media_id.startswith("spotify:"): - _LOGGER.error("media id must be spotify playlist uri") - return - kwargs = {"context_uri": media_id} - if random_song: - results = self._player.user_playlist_tracks("me", media_id) - position = random.randint(0, results["total"] - 1) - kwargs["offset"] = {"position": position} - self._player.start_playback(**kwargs) + if not self._session.valid_token or self._spotify is None: + run_coroutine_threadsafe( + self._session.async_ensure_token_valid(), self.hass.loop + ).result() + self._spotify = Spotify(auth=self._session.token["access_token"]) - @property - def name(self): - """Return the name.""" - return self._name + current = self._spotify.current_playback() + self._currently_playing = current or {} - @property - def icon(self): - """Return the icon.""" - return ICON + self._playlist = None + context = self._currently_playing.get("context") + if context is not None and context["type"] == MEDIA_TYPE_PLAYLIST: + self._playlist = self._spotify.playlist(current["context"]["uri"]) - @property - def state(self): - """Return the playback state.""" - return self._state - - @property - def volume_level(self): - """Return the device volume.""" - return self._volume - - @property - def shuffle(self): - """Shuffling state.""" - return self._shuffle - - @property - def source_list(self): - """Return a list of source devices.""" - if self._devices: - return list(self._devices.keys()) - - @property - def source(self): - """Return the current playback device.""" - return self._current_device - - @property - def media_content_id(self): - """Return the media URL.""" - return self._uri - - @property - def media_image_url(self): - """Return the media image URL.""" - return self._image_url - - @property - def media_artist(self): - """Return the media artist.""" - return self._artist - - @property - def media_album_name(self): - """Return the media album.""" - return self._album - - @property - def media_title(self): - """Return the media title.""" - return self._title - - @property - def supported_features(self): - """Return the media player features that are supported.""" - if self._user is not None and self._user["product"] == "premium": - return SUPPORT_SPOTIFY - return None - - @property - def media_content_type(self): - """Return the media type.""" - return MEDIA_TYPE_MUSIC + devices = self._spotify.devices() or {} + self._devices = devices.get("devices", []) diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json new file mode 100644 index 00000000000..316fbd946db --- /dev/null +++ b/homeassistant/components/spotify/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Spotify account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Spotify integration is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Spotify." + }, + "title": "Spotify" + } +} diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index c3edbef6944..de2fce5b1a1 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.12"], + "requirements": ["sqlalchemy==1.3.13"], "dependencies": [], "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 94c497e4db6..0610d4d9cf2 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -281,12 +281,9 @@ class SqueezeBoxDevice(MediaPlayerDevice): return STATE_IDLE return None - def async_query(self, *parameters): - """Send a command to the LMS. - - This method must be run in the event loop and returns a coroutine. - """ - return self._lms.async_query(*parameters, player=self._id) + async def async_query(self, *parameters): + """Send a command to the LMS.""" + return await self._lms.async_query(*parameters, player=self._id) async def async_update(self): """Retrieve the current state of the player.""" @@ -420,121 +417,85 @@ class SqueezeBoxDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_SQUEEZEBOX - def async_turn_off(self): - """Turn off media player. + async def async_turn_off(self): + """Turn off media player.""" + await self.async_query("power", "0") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("power", "0") + async def async_volume_up(self): + """Volume up media player.""" + await self.async_query("mixer", "volume", "+5") - def async_volume_up(self): - """Volume up media player. + async def async_volume_down(self): + """Volume down media player.""" + await self.async_query("mixer", "volume", "-5") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("mixer", "volume", "+5") - - def async_volume_down(self): - """Volume down media player. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("mixer", "volume", "-5") - - def async_set_volume_level(self, volume): - """Set volume level, range 0..1. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" volume_percent = str(int(volume * 100)) - return self.async_query("mixer", "volume", volume_percent) + await self.async_query("mixer", "volume", volume_percent) - def async_mute_volume(self, mute): - """Mute (true) or unmute (false) media player. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" mute_numeric = "1" if mute else "0" - return self.async_query("mixer", "muting", mute_numeric) + await self.async_query("mixer", "muting", mute_numeric) - def async_media_play_pause(self): - """Send pause command to media player. + async def async_media_play_pause(self): + """Send pause command to media player.""" + await self.async_query("pause") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("pause") + async def async_media_play(self): + """Send play command to media player.""" + await self.async_query("play") - def async_media_play(self): - """Send play command to media player. + async def async_media_pause(self): + """Send pause command to media player.""" + await self.async_query("pause", "1") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("play") + async def async_media_next_track(self): + """Send next track command.""" + await self.async_query("playlist", "index", "+1") - def async_media_pause(self): - """Send pause command to media player. + async def async_media_previous_track(self): + """Send next track command.""" + await self.async_query("playlist", "index", "-1") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("pause", "1") + async def async_media_seek(self, position): + """Send seek command.""" + await self.async_query("time", position) - def async_media_next_track(self): - """Send next track command. + async def async_turn_on(self): + """Turn the media player on.""" + await self.async_query("power", "1") - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("playlist", "index", "+1") - - def async_media_previous_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("playlist", "index", "-1") - - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("time", position) - - def async_turn_on(self): - """Turn the media player on. - - This method must be run in the event loop and returns a coroutine. - """ - return self.async_query("power", "1") - - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. - This method must be run in the event loop and returns a coroutine. """ if kwargs.get(ATTR_MEDIA_ENQUEUE): - return self._add_uri_to_playlist(media_id) + await self._add_uri_to_playlist(media_id) + return - return self._play_uri(media_id) + await self._play_uri(media_id) - def _play_uri(self, media_id): + async def _play_uri(self, media_id): """Replace the current play list with the uri.""" - return self.async_query("playlist", "play", media_id) + await self.async_query("playlist", "play", media_id) - def _add_uri_to_playlist(self, media_id): + async def _add_uri_to_playlist(self, media_id): """Add an item to the existing playlist.""" - return self.async_query("playlist", "add", media_id) + await self.async_query("playlist", "add", media_id) - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - return self.async_query("playlist", "shuffle", int(shuffle)) + await self.async_query("playlist", "shuffle", int(shuffle)) - def async_clear_playlist(self): + async def async_clear_playlist(self): """Send the media player the command for clear playlist.""" - return self.async_query("playlist", "clear") + await self.async_query("playlist", "clear") - def async_call_method(self, command, parameters=None): + async def async_call_method(self, command, parameters=None): """ Call Squeezebox JSON/RPC method. @@ -545,4 +506,4 @@ class SqueezeBoxDevice(MediaPlayerDevice): if parameters: for parameter in parameters: all_params.append(parameter) - return self.async_query(*all_params) + await self.async_query(*all_params) diff --git a/homeassistant/components/starline/.translations/de.json b/homeassistant/components/starline/.translations/de.json index 138969cc8b1..28332124f9c 100644 --- a/homeassistant/components/starline/.translations/de.json +++ b/homeassistant/components/starline/.translations/de.json @@ -25,7 +25,7 @@ "data": { "mfa_code": "SMS Code" }, - "description": "Geben Sie den an das Telefon gesendeten Code ein {Telefon_Nummer}", + "description": "Gib den an das Telefon gesendeten Code ein {Telefon_Nummer}", "title": "2-Faktor-Authentifizierung" }, "auth_user": { diff --git a/homeassistant/components/starline/.translations/hu.json b/homeassistant/components/starline/.translations/hu.json new file mode 100644 index 00000000000..c45d9ac871e --- /dev/null +++ b/homeassistant/components/starline/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "error_auth_app": "Helytelen alkalmaz\u00e1sazonos\u00edt\u00f3 vagy jelsz\u00f3", + "error_auth_mfa": "Helytelen k\u00f3d", + "error_auth_user": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/zh-Hant.json b/homeassistant/components/starline/.translations/zh-Hant.json index 0bd69d54ec6..6f8eeffc8b1 100644 --- a/homeassistant/components/starline/.translations/zh-Hant.json +++ b/homeassistant/components/starline/.translations/zh-Hant.json @@ -26,7 +26,7 @@ "mfa_code": "\u7c21\u8a0a\u5bc6\u78bc" }, "description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc", - "title": "\u5169\u968e\u6bb5\u8a8d\u8b49" + "title": "\u96d9\u91cd\u9a57\u8b49" }, "auth_user": { "data": { diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index aaffa20a698..a7bdd241b55 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -2,7 +2,7 @@ "domain": "starline", "name": "StarLine", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/starline", + "documentation": "https://www.home-assistant.io/integrations/starline", "requirements": ["starline==0.1.3"], "dependencies": [], "codeowners": ["@anonym-tsk"] diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 6e042b1536f..865fda93a3e 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -19,7 +19,10 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_state_change, +) from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -96,6 +99,7 @@ class StatisticsSensor(Entity): self.total = self.min = self.max = None self.min_age = self.max_age = None self.change = self.average_change = self.change_rate = None + self._update_listener = None async def async_added_to_hass(self): """Register callbacks.""" @@ -214,6 +218,15 @@ class StatisticsSensor(Entity): self.ages.popleft() self.states.popleft() + def _next_to_purge_timestamp(self): + """Find the timestamp when the next purge would occur.""" + if self.ages and self._max_age: + # Take the oldest entry from the ages list and add the configured max_age. + # If executed after purging old states, the result is the next timestamp + # in the future when the oldest state will expire. + return self.ages[0] + self._max_age + return None + async def async_update(self): """Get the latest data and updates the states.""" _LOGGER.debug("%s: updating statistics.", self.entity_id) @@ -266,6 +279,26 @@ class StatisticsSensor(Entity): self.change = self.average_change = STATE_UNKNOWN self.change_rate = STATE_UNKNOWN + # If max_age is set, ensure to update again after the defined interval. + next_to_purge_timestamp = self._next_to_purge_timestamp() + if next_to_purge_timestamp: + _LOGGER.debug( + "%s: scheduling update at %s", self.entity_id, next_to_purge_timestamp + ) + if self._update_listener: + self._update_listener() + self._update_listener = None + + @callback + def _scheduled_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug("%s: executing scheduled update", self.entity_id) + self.async_schedule_update_ha_state(True) + + self._update_listener = async_track_point_in_utc_time( + self.hass, _scheduled_update, next_to_purge_timestamp + ) + async def _async_initialize_from_database(self): """Initialize the list of states from the database. diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 87aefdb616d..c928deef01a 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.condition import ConditionCheckerType from homeassistant.helpers.typing import ConfigType @@ -16,6 +16,7 @@ CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( ) +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index c8eaddcb5bd..8c9c9e1a6fa 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -124,17 +124,11 @@ class SwitcherControl(SwitchDevice): self.async_schedule_update_ha_state() async def async_turn_on(self, **kwargs: Dict) -> None: - """Turn the entity on. - - This method must be run in the event loop and returns a coroutine. - """ + """Turn the entity on.""" await self._control_device(True) async def async_turn_off(self, **kwargs: Dict) -> None: - """Turn the entity off. - - This method must be run in the event loop and returns a coroutine. - """ + """Turn the entity off.""" await self._control_device(False) async def _control_device(self, send_on: bool) -> None: diff --git a/homeassistant/components/synologydsm/manifest.json b/homeassistant/components/synologydsm/manifest.json index d9405b3ee68..586fe75c697 100644 --- a/homeassistant/components/synologydsm/manifest.json +++ b/homeassistant/components/synologydsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synologydsm", "name": "SynologyDSM", "documentation": "https://www.home-assistant.io/integrations/synologydsm", - "requirements": ["python-synology==0.3.0"], + "requirements": ["python-synology==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index d415d009252..3f823331433 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, + CONF_API_VERSION, CONF_DISKS, CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -82,6 +83,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=True): cv.boolean, + vol.Optional(CONF_API_VERSION): cv.positive_int, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS): vol.All( @@ -110,8 +112,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): use_ssl = config.get(CONF_SSL) unit = hass.config.units.temperature_unit monitored_conditions = config.get(CONF_MONITORED_CONDITIONS) + api_version = config.get(CONF_API_VERSION) - api = SynoApi(host, port, username, password, unit, use_ssl) + api = SynoApi(host, port, username, password, unit, use_ssl, api_version) sensors = [ SynoNasUtilSensor(api, name, variable, _UTILISATION_MON_COND[variable]) @@ -150,13 +153,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SynoApi: """Class to interface with Synology DSM API.""" - def __init__(self, host, port, username, password, temp_unit, use_ssl): + def __init__(self, host, port, username, password, temp_unit, use_ssl, api_version): """Initialize the API wrapper class.""" self.temp_unit = temp_unit try: - self._api = SynologyDSM(host, port, username, password, use_https=use_ssl) + self._api = SynologyDSM( + host, + port, + username, + password, + use_https=use_ssl, + debugmode=False, + dsm_version=api_version, + ) except: # noqa: E722 pylint: disable=bare-except _LOGGER.error("Error setting up Synology DSM") @@ -231,6 +242,9 @@ class SynoNasUtilSensor(SynoNasSensor): if self.var_id in network_sensors or self.var_id in memory_sensors: attr = getattr(self._api.utilisation, self.var_id)(False) + if attr is None: + return None + if self.var_id in network_sensors: return round(attr / 1024.0, 1) if self.var_id in memory_sensors: diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 6bb4fc200af..0d74d6018a5 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -34,8 +34,11 @@ CONFIG_SCHEMA = vol.Schema( TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"] TAHOMA_TYPES = { + "io:AwningValanceIOComponent": "cover", "io:ExteriorVenetianBlindIOComponent": "cover", + "io:DiscreteGarageOpenerIOComponent": "cover", "io:HorizontalAwningIOComponent": "cover", + "io:GarageOpenerIOComponent": "cover", "io:LightIOSystemSensor": "sensor", "io:OnOffIOComponent": "switch", "io:OnOffLightIOComponent": "switch", @@ -49,8 +52,6 @@ TAHOMA_TYPES = { "io:VerticalExteriorAwningIOComponent": "cover", "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", - "io:GarageOpenerIOComponent": "cover", - "io:DiscreteGarageOpenerIOComponent": "cover", "rtds:RTDSContactSensor": "sensor", "rtds:RTDSMotionSensor": "sensor", "rtds:RTDSSmokeSensor": "smoke", diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index e11c2f4cdf5..fb2bedc746c 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -28,8 +28,11 @@ ATTR_LOCK_ORIG = "lock_originator" HORIZONTAL_AWNING = "io:HorizontalAwningIOComponent" TAHOMA_DEVICE_CLASSES = { - "io:ExteriorVenetianBlindIOComponent": DEVICE_CLASS_BLIND, HORIZONTAL_AWNING: DEVICE_CLASS_AWNING, + "io:AwningValanceIOComponent": DEVICE_CLASS_AWNING, + "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, + "io:ExteriorVenetianBlindIOComponent": DEVICE_CLASS_BLIND, + "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "io:RollerShutterGenericIOComponent": DEVICE_CLASS_SHUTTER, "io:RollerShutterUnoIOComponent": DEVICE_CLASS_SHUTTER, "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, @@ -37,8 +40,6 @@ TAHOMA_DEVICE_CLASSES = { "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, - "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, - "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, @@ -228,22 +229,22 @@ class TahomaCover(TahomaDevice, CoverDevice): == "io:RollerShutterWithLowSpeedManagementIOComponent" ): self.apply_action("setPosition", "secured") - elif self.tahoma_device.type in ( - "rts:BlindRTSComponent", + elif self.tahoma_device.type in { "io:ExteriorVenetianBlindIOComponent", - "rts:VenetianBlindRTSComponent", + "rts:BlindRTSComponent", "rts:DualCurtainRTSComponent", "rts:ExteriorVenetianBlindRTSComponent", - "rts:BlindRTSComponent", - ): + "rts:VenetianBlindRTSComponent", + }: self.apply_action("my") - elif self.tahoma_device.type in ( + elif self.tahoma_device.type in { HORIZONTAL_AWNING, + "io:AwningValanceIOComponent", "io:RollerShutterGenericIOComponent", "io:VerticalExteriorAwningIOComponent", "io:VerticalInteriorBlindVeluxIOComponent", "io:WindowOpenerVeluxIOComponent", - ): + }: self.apply_action("stop") else: self.apply_action("stopIdentify") diff --git a/homeassistant/components/tellduslive/.translations/cs.json b/homeassistant/components/tellduslive/.translations/cs.json new file mode 100644 index 00000000000..bab99c32124 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json index 18c3e88666e..c07ff528363 100644 --- a/homeassistant/components/tellduslive/.translations/de.json +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -7,12 +7,12 @@ "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { - "auth_error": "Authentifizierungsfehler, bitte versuchen Sie es erneut" + "auth_error": "Authentifizierungsfehler, bitte versuche es erneut" }, "step": { "auth": { - "description": "So verkn\u00fcpfen Sie Ihr TelldusLive-Konto: \n 1. Klicken Sie auf den Link unten \n 2. Melden Sie sich bei Telldus Live an \n 3. Autorisieren Sie ** {app_name} ** (klicken Sie auf ** Yes **). \n 4. Kommen Sie hierher zur\u00fcck und klicken Sie auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", - "title": "Authentifizieren Sie sich gegen TelldusLive" + "description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Klicke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (klicke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und klicke auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", + "title": "Authentifiziere dich gegen TelldusLive" }, "user": { "data": { diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py new file mode 100644 index 00000000000..019c9cd8787 --- /dev/null +++ b/homeassistant/components/template/alarm_control_panel.py @@ -0,0 +1,283 @@ +"""Support for Template alarm control panels.""" +import logging + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel import ( + ENTITY_ID_FORMAT, + FORMAT_NUMBER, + PLATFORM_SCHEMA, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + ATTR_CODE, + CONF_NAME, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + MATCH_ALL, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNAVAILABLE, +) +from homeassistant.core import callback +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) +_VALID_STATES = [ + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_ALARM_ARMED_NIGHT, + STATE_UNAVAILABLE, +] + +CONF_ARM_AWAY_ACTION = "arm_away" +CONF_ARM_HOME_ACTION = "arm_home" +CONF_ARM_NIGHT_ACTION = "arm_night" +CONF_DISARM_ACTION = "disarm" +CONF_ALARM_CONTROL_PANELS = "panels" +CONF_CODE_ARM_REQUIRED = "code_arm_required" + +ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( + { + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, + vol.Optional(CONF_NAME): cv.string, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys( + ALARM_CONTROL_PANEL_SCHEMA + ), + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Template Alarm Control Panels.""" + alarm_control_panels = [] + + for device, device_config in config[CONF_ALARM_CONTROL_PANELS].items(): + name = device_config.get(CONF_NAME, device) + state_template = device_config.get(CONF_VALUE_TEMPLATE) + disarm_action = device_config.get(CONF_DISARM_ACTION) + arm_away_action = device_config.get(CONF_ARM_AWAY_ACTION) + arm_home_action = device_config.get(CONF_ARM_HOME_ACTION) + arm_night_action = device_config.get(CONF_ARM_NIGHT_ACTION) + code_arm_required = device_config[CONF_CODE_ARM_REQUIRED] + + template_entity_ids = set() + + if state_template is not None: + temp_ids = state_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + else: + _LOGGER.warning("No value template - will use optimistic state") + + if not template_entity_ids: + template_entity_ids = MATCH_ALL + + alarm_control_panels.append( + AlarmControlPanelTemplate( + hass, + device, + name, + state_template, + disarm_action, + arm_away_action, + arm_home_action, + arm_night_action, + code_arm_required, + template_entity_ids, + ) + ) + + async_add_entities(alarm_control_panels) + + +class AlarmControlPanelTemplate(AlarmControlPanel): + """Representation of a templated Alarm Control Panel.""" + + def __init__( + self, + hass, + device_id, + name, + state_template, + disarm_action, + arm_away_action, + arm_home_action, + arm_night_action, + code_arm_required, + template_entity_ids, + ): + """Initialize the panel.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass + ) + self._name = name + self._template = state_template + self._disarm_script = None + self._code_arm_required = code_arm_required + if disarm_action is not None: + self._disarm_script = Script(hass, disarm_action) + self._arm_away_script = None + if arm_away_action is not None: + self._arm_away_script = Script(hass, arm_away_action) + self._arm_home_script = None + if arm_home_action is not None: + self._arm_home_script = Script(hass, arm_home_action) + self._arm_night_script = None + if arm_night_action is not None: + self._arm_night_script = Script(hass, arm_night_action) + + self._state = None + self._entities = template_entity_ids + + if self._template is not None: + self._template.hass = self.hass + + @property + def name(self): + """Return the display name of this alarm control panel.""" + return self._name + + @property + def should_poll(self): + """Return the polling state.""" + return False + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + supported_features = 0 + if self._arm_night_script is not None: + supported_features = supported_features | SUPPORT_ALARM_ARM_NIGHT + + if self._arm_home_script is not None: + supported_features = supported_features | SUPPORT_ALARM_ARM_HOME + + if self._arm_away_script is not None: + supported_features = supported_features | SUPPORT_ALARM_ARM_AWAY + + return supported_features + + @property + def code_format(self): + """Return one or more digits/characters.""" + return FORMAT_NUMBER + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._code_arm_required + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def template_alarm_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_alarm_control_panel_startup(event): + """Update template on startup.""" + if self._template is not None: + async_track_state_change( + self.hass, self._entities, template_alarm_state_listener + ) + + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_alarm_control_panel_startup + ) + + async def _async_alarm_arm(self, state, script=None, code=None): + """Arm the panel to specified state with supplied script.""" + optimistic_set = False + + if self._template is None: + self._state = state + optimistic_set = True + + if script is not None: + await script.async_run({ATTR_CODE: code}, context=self._context) + else: + _LOGGER.error("No script action defined for %s", state) + + if optimistic_set: + self.async_schedule_update_ha_state() + + async def async_alarm_arm_away(self, code=None): + """Arm the panel to Away.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_AWAY, script=self._arm_away_script, code=code + ) + + async def async_alarm_arm_home(self, code=None): + """Arm the panel to Home.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_HOME, script=self._arm_home_script, code=code + ) + + async def async_alarm_arm_night(self, code=None): + """Arm the panel to Night.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code + ) + + async def async_alarm_disarm(self, code=None): + """Disarm the panel.""" + await self._async_alarm_arm( + STATE_ALARM_DISARMED, script=self._disarm_script, code=code + ) + + async def async_update(self): + """Update the state from the template.""" + if self._template is None: + return + + try: + state = self._template.async_render().lower() + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + if state in _VALID_STATES: + self._state = state + _LOGGER.debug("Valid state - %s", state) + else: + _LOGGER.error( + "Received invalid alarm panel state: %s. Expected: %s", + state, + ", ".join(_VALID_STATES), + ) + self._state = None diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index f6678067d70..870e4035c2f 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -134,7 +134,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= } initialise_templates(hass, templates) - entity_ids = extract_entities(device, "cover", None, templates) + entity_ids = extract_entities( + device, "cover", device_config.get(CONF_ENTITY_ID), templates + ) covers.append( CoverTemplate( @@ -227,19 +229,6 @@ class CoverTemplate(CoverDevice): self._entities = entity_ids self._available = True - if self._template is not None: - self._template.hass = self.hass - if self._position_template is not None: - self._position_template.hass = self.hass - if self._tilt_template is not None: - self._tilt_template.hass = self.hass - if self._icon_template is not None: - self._icon_template.hass = self.hass - if self._entity_picture_template is not None: - self._entity_picture_template.hass = self.hass - if self._availability_template is not None: - self._availability_template.hass = self.hass - async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 89f54444376..14381b82e62 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -189,18 +189,12 @@ class TemplateFan(FanEntity): self._oscillating = None self._direction = None - self._template.hass = self.hass if self._speed_template: - self._speed_template.hass = self.hass self._supported_features |= SUPPORT_SET_SPEED if self._oscillating_template: - self._oscillating_template.hass = self.hass self._supported_features |= SUPPORT_OSCILLATE if self._direction_template: - self._direction_template.hass = self.hass self._supported_features |= SUPPORT_DIRECTION - if self._availability_template: - self._availability_template.hass = self.hass self._entities = entity_ids # List of valid speeds diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 70c097d0b2b..c5512461f34 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -5,8 +5,10 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, Light, ) from homeassistant.const import ( @@ -38,6 +40,8 @@ CONF_ON_ACTION = "turn_on" CONF_OFF_ACTION = "turn_off" CONF_LEVEL_ACTION = "set_level" CONF_LEVEL_TEMPLATE = "level_template" +CONF_TEMPERATURE_TEMPLATE = "temperature_template" +CONF_TEMPERATURE_ACTION = "set_temperature" LIGHT_SCHEMA = vol.Schema( { @@ -51,6 +55,8 @@ LIGHT_SCHEMA = vol.Schema( vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Optional(CONF_ENTITY_ID): cv.entity_ids, + vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA, } ) @@ -75,6 +81,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) + temperature_action = device_config.get(CONF_TEMPERATURE_ACTION) + temperature_template = device_config.get(CONF_TEMPERATURE_TEMPLATE) templates = { CONF_VALUE_TEMPLATE: state_template, @@ -82,10 +90,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_AVAILABILITY_TEMPLATE: availability_template, CONF_LEVEL_TEMPLATE: level_template, + CONF_TEMPERATURE_TEMPLATE: temperature_template, } initialise_templates(hass, templates) - entity_ids = extract_entities(device, "light", None, templates) + entity_ids = extract_entities( + device, "light", device_config.get(CONF_ENTITY_ID), templates + ) lights.append( LightTemplate( @@ -101,6 +112,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= level_action, level_template, entity_ids, + temperature_action, + temperature_template, ) ) @@ -129,6 +142,8 @@ class LightTemplate(Light): level_action, level_template, entity_ids, + temperature_action, + temperature_template, ): """Initialize the light.""" self.hass = hass @@ -146,30 +161,29 @@ class LightTemplate(Light): if level_action is not None: self._level_script = Script(hass, level_action) self._level_template = level_template + self._temperature_script = None + if temperature_action is not None: + self._temperature_script = Script(hass, temperature_action) + self._temperature_template = temperature_template self._state = False self._icon = None self._entity_picture = None self._brightness = None + self._temperature = None self._entities = entity_ids self._available = True - if self._template is not None: - self._template.hass = self.hass - if self._level_template is not None: - self._level_template.hass = self.hass - if self._icon_template is not None: - self._icon_template.hass = self.hass - if self._entity_picture_template is not None: - self._entity_picture_template.hass = self.hass - if self._availability_template is not None: - self._availability_template.hass = self.hass - @property def brightness(self): """Return the brightness of the light.""" return self._brightness + @property + def color_temp(self): + """Return the CT color value in mireds.""" + return self._temperature + @property def name(self): """Return the display name of this light.""" @@ -178,10 +192,12 @@ class LightTemplate(Light): @property def supported_features(self): """Flag supported features.""" + supported_features = 0 if self._level_script is not None: - return SUPPORT_BRIGHTNESS - - return 0 + supported_features |= SUPPORT_BRIGHTNESS + if self._temperature_script is not None: + supported_features |= SUPPORT_COLOR_TEMP + return supported_features @property def is_on(self): @@ -222,6 +238,7 @@ class LightTemplate(Light): if ( self._template is not None or self._level_template is not None + or self._temperature_template is not None or self._availability_template is not None ): async_track_state_change( @@ -249,10 +266,22 @@ class LightTemplate(Light): self._brightness = kwargs[ATTR_BRIGHTNESS] optimistic_set = True + if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs: + _LOGGER.info( + "Optimistically setting color temperature to %s", + kwargs[ATTR_COLOR_TEMP], + ) + self._temperature = kwargs[ATTR_COLOR_TEMP] + optimistic_set = True + if ATTR_BRIGHTNESS in kwargs and self._level_script: await self._level_script.async_run( {"brightness": kwargs[ATTR_BRIGHTNESS]}, context=self._context ) + elif ATTR_COLOR_TEMP in kwargs and self._temperature_script: + await self._temperature_script.async_run( + {"color_temp": kwargs[ATTR_COLOR_TEMP]}, context=self._context + ) else: await self._on_script.async_run() @@ -272,6 +301,8 @@ class LightTemplate(Light): self.update_brightness() + self.update_temperature() + for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), @@ -311,35 +342,57 @@ class LightTemplate(Light): @callback def update_brightness(self): """Update the brightness from the template.""" - if self._level_template is not None: - try: - brightness = self._level_template.async_render() - if 0 <= int(brightness) <= 255: - self._brightness = int(brightness) - else: - _LOGGER.error( - "Received invalid brightness : %s. Expected: 0-255", brightness - ) - self._brightness = None - except TemplateError as ex: - _LOGGER.error(ex) - self._state = None + if self._level_template is None: + return + try: + brightness = self._level_template.async_render() + if 0 <= int(brightness) <= 255: + self._brightness = int(brightness) + else: + _LOGGER.error( + "Received invalid brightness : %s. Expected: 0-255", brightness + ) + self._brightness = None + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None @callback def update_state(self): """Update the state from the template.""" - if self._template is not None: - try: - state = self._template.async_render().lower() - if state in _VALID_STATES: - self._state = state in ("true", STATE_ON) - else: - _LOGGER.error( - "Received invalid light is_on state: %s. Expected: %s", - state, - ", ".join(_VALID_STATES), - ) - self._state = None - except TemplateError as ex: - _LOGGER.error(ex) + if self._template is None: + return + try: + state = self._template.async_render().lower() + if state in _VALID_STATES: + self._state = state in ("true", STATE_ON) + else: + _LOGGER.error( + "Received invalid light is_on state: %s. Expected: %s", + state, + ", ".join(_VALID_STATES), + ) self._state = None + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + @callback + def update_temperature(self): + """Update the temperature from the template.""" + if self._temperature_template is None: + return + try: + temperature = int(self._temperature_template.async_render()) + if self.min_mireds <= temperature <= self.max_mireds: + self._temperature = temperature + else: + _LOGGER.error( + "Received invalid color temperature : %s. Expected: 0-%s", + temperature, + self.max_mireds, + ) + self._temperature = None + except TemplateError: + _LOGGER.error("Cannot evaluate temperature template", exc_info=True) + self._temperature = None diff --git a/homeassistant/components/tesla/.translations/de.json b/homeassistant/components/tesla/.translations/de.json index c2f6ba38eb9..4f435aa7839 100644 --- a/homeassistant/components/tesla/.translations/de.json +++ b/homeassistant/components/tesla/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "connection_error": "Fehler beim Verbinden; \u00dcberpr\u00fcfen Sie Ihr Netzwerk und versuchen Sie es erneut", + "connection_error": "Fehler beim Verbinden; \u00dcberpr\u00fcfe dein Netzwerk und versuche es erneut", "identifier_exists": "E-Mail bereits registriert", "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", "unknown_error": "Unbekannter Fehler, bitte Log-Info melden" @@ -12,7 +12,7 @@ "password": "Passwort", "username": "E-Mail-Adresse" }, - "description": "Bitte geben Sie Ihre Daten ein.", + "description": "Bitte gib deine Daten ein.", "title": "Tesla - Konfiguration" } }, diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 1ae65f66821..3c2a22793db 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( + ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, CONF_ACCESS_TOKEN, CONF_PASSWORD, @@ -118,6 +119,7 @@ async def async_setup_entry(hass, config_entry): controller = TeslaAPI( websession, refresh_token=config[CONF_TOKEN], + access_token=config[CONF_ACCESS_TOKEN], update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), ) (refresh_token, access_token) = await controller.connect() @@ -214,6 +216,7 @@ class TeslaDevice(Entity): attr = self._attributes if self.tesla_device.has_battery(): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() + attr[ATTR_BATTERY_CHARGING] = self.tesla_device.battery_charging() return attr @property diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 8f610d960b3..8b60cd00163 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,11 +8,6 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Tesla binary sensor.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" async_add_entities( @@ -60,3 +55,4 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): _LOGGER.debug("Updating sensor: %s", self._name) await super().async_update() self._state = self.tesla_device.get_value() + self._attributes = self.tesla_device.attrs diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index d7f21d7895f..d438f94f4c3 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -16,11 +16,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_HVAC = [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Tesla climate platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" async_add_entities( diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 33eed8cf7c1..7dffff5a5e0 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,11 +9,6 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Tesla lock platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" entities = [ diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index e3392074679..f536cdf96b4 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.2.3"], + "requirements": ["teslajsonpy==0.3.0"], "dependencies": [], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index a282f65f9e1..9b06828693f 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -15,11 +15,6 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Tesla sensor platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] @@ -42,6 +37,7 @@ class TeslaSensor(TeslaDevice, Entity): self.units = None self.last_changed_time = None self.type = sensor_type + self._device_class = tesla_device.device_class super().__init__(tesla_device, controller, config_entry) if self.type: @@ -64,6 +60,11 @@ class TeslaSensor(TeslaDevice, Entity): """Return the unit_of_measurement of the device.""" return self.units + @property + def device_class(self): + """Return the device_class of the device.""" + return self._device_class + async def async_update(self): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index fc9b5e1ba88..331f6bd8126 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,11 +9,6 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Tesla switch platform.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Tesla binary_sensors by config_entry.""" controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index ccc04d7f72a..abf3a6ab0f7 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -1,15 +1,26 @@ """Support for Timers.""" from datetime import timedelta import logging +import typing import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, +) +from homeassistant.core import callback +from homeassistant.helpers import collection import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -17,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "timer" ENTITY_ID_FORMAT = DOMAIN + ".{}" -DEFAULT_DURATION = timedelta(0) +DEFAULT_DURATION = 0 ATTR_DURATION = "duration" ATTR_REMAINING = "remaining" CONF_DURATION = "duration" @@ -37,6 +48,21 @@ SERVICE_PAUSE = "pause" SERVICE_CANCEL = "cancel" SERVICE_FINISH = "finish" +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period, +} +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_DURATION): cv.time_period, +} + def _none_to_empty_dict(value): if value is None: @@ -65,20 +91,45 @@ CONFIG_SCHEMA = vol.Schema( RELOAD_SERVICE_SCHEMA = vol.Schema({}) -async def async_setup(hass, config): - """Set up a timer.""" +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = await _async_process_config(hass, config) + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, Timer.from_yaml + ) - async def reload_service_handler(service_call): - """Remove all input booleans and load new ones from config.""" - conf = await component.async_prepare_reload() + storage_collection = TimerStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection(component, storage_collection, Timer) + + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, yaml_collection) + collection.attach_entity_registry_cleaner(hass, DOMAIN, DOMAIN, storage_collection) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) if conf is None: - return - new_entities = await _async_process_config(hass, conf) - if new_entities: - await component.async_add_entities(new_entities) + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -96,43 +147,55 @@ async def async_setup(hass, config): component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") - if entities: - await component.async_add_entities(entities) return True -async def _async_process_config(hass, config): - """Process config and create list of entities.""" - entities = [] +class TimerStorageCollection(collection.StorageCollection): + """Timer storage based collection.""" - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) - name = cfg.get(CONF_NAME) - icon = cfg.get(CONF_ICON) - duration = cfg[CONF_DURATION] + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + data = self.CREATE_SCHEMA(data) + # make duration JSON serializeable + data[CONF_DURATION] = str(data[CONF_DURATION]) + return data - entities.append(Timer(hass, object_id, name, icon, duration)) + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] - return entities + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + data = {**data, **self.UPDATE_SCHEMA(update_data)} + # make duration JSON serializeable + data[CONF_DURATION] = str(data[CONF_DURATION]) + return data class Timer(RestoreEntity): """Representation of a timer.""" - def __init__(self, hass, object_id, name, icon, duration): + def __init__(self, config: typing.Dict): """Initialize a timer.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name + self._config = config + self.editable = True self._state = STATUS_IDLE - self._duration = duration - self._remaining = self._duration - self._icon = icon - self._hass = hass + self._remaining = config[CONF_DURATION] self._end = None self._listener = None + @classmethod + def from_yaml(cls, config: typing.Dict) -> "Timer": + """Return entity instance initialized from yaml storage.""" + timer = cls(config) + timer.entity_id = ENTITY_ID_FORMAT.format(config[CONF_ID]) + timer.editable = False + return timer + @property def should_poll(self): """If entity should be polled.""" @@ -141,12 +204,12 @@ class Timer(RestoreEntity): @property def name(self): """Return name of the timer.""" - return self._name + return self._config.get(CONF_NAME) @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) @property def state(self): @@ -157,10 +220,16 @@ class Timer(RestoreEntity): def state_attributes(self): """Return the state attributes.""" return { - ATTR_DURATION: str(self._duration), + ATTR_DURATION: str(self._config[CONF_DURATION]), + ATTR_EDITABLE: self.editable, ATTR_REMAINING: str(self._remaining), } + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id for the entity.""" + return self._config[CONF_ID] + async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" # If not None, we got an initial value. @@ -184,23 +253,23 @@ class Timer(RestoreEntity): event = EVENT_TIMER_RESTARTED self._state = STATUS_ACTIVE - start = dt_util.utcnow() + start = dt_util.utcnow().replace(microsecond=0) if self._remaining and newduration is None: self._end = start + self._remaining else: if newduration: - self._duration = newduration + self._config[CONF_DURATION] = newduration self._remaining = newduration else: - self._remaining = self._duration - self._end = start + self._duration + self._remaining = self._config[CONF_DURATION] + self._end = start + self._config[CONF_DURATION] - self._hass.bus.async_fire(event, {"entity_id": self.entity_id}) + self.hass.bus.async_fire(event, {"entity_id": self.entity_id}) self._listener = async_track_point_in_utc_time( - self._hass, self.async_finished, self._end + self.hass, self.async_finished, self._end ) - await self.async_update_ha_state() + self.async_write_ha_state() async def async_pause(self): """Pause a timer.""" @@ -209,11 +278,11 @@ class Timer(RestoreEntity): self._listener() self._listener = None - self._remaining = self._end - dt_util.utcnow() + self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) self._state = STATUS_PAUSED self._end = None - self._hass.bus.async_fire(EVENT_TIMER_PAUSED, {"entity_id": self.entity_id}) - await self.async_update_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {"entity_id": self.entity_id}) + self.async_write_ha_state() async def async_cancel(self): """Cancel a timer.""" @@ -223,8 +292,8 @@ class Timer(RestoreEntity): self._state = STATUS_IDLE self._end = None self._remaining = timedelta() - self._hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) - await self.async_update_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) + self.async_write_ha_state() async def async_finish(self): """Reset and updates the states, fire finished event.""" @@ -234,8 +303,8 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE self._remaining = timedelta() - self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - await self.async_update_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) + self.async_write_ha_state() async def async_finished(self, time): """Reset and updates the states, fire finished event.""" @@ -245,5 +314,10 @@ class Timer(RestoreEntity): self._listener = None self._state = STATUS_IDLE self._remaining = timedelta() - self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - await self.async_update_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) + self.async_write_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 6c5d8827a86..cab57c59ac8 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -174,6 +174,20 @@ class TodSensor(BinarySensorDevice): self._time_before = before_event_date + # We are calculating the _time_after value assuming that it will happen today + # But that is not always true, e.g. after 23:00, before 12:00 and now is 10:00 + # If _time_before and _time_after are ahead of current_datetime: + # _time_before is set to 12:00 next day + # _time_after is set to 23:00 today + # current_datetime is set to 10:00 today + if ( + self._time_after > self.current_datetime + and self._time_before > self.current_datetime + timedelta(days=1) + ): + # remove one day from _time_before and _time_after + self._time_after -= timedelta(days=1) + self._time_before -= timedelta(days=1) + # Add offset to utc boundaries according to the configuration self._time_after += self._after_offset self._time_before += self._before_offset diff --git a/homeassistant/components/tplink/.translations/de.json b/homeassistant/components/tplink/.translations/de.json index 268d8ed0717..ba19fd04390 100644 --- a/homeassistant/components/tplink/.translations/de.json +++ b/homeassistant/components/tplink/.translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie TP-Link Smart Devices einrichten?", + "description": "M\u00f6chtest du TP-Link Smart Devices einrichten?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index ec3307fc87e..0e7be471f43 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -1,6 +1,8 @@ """Support for TPLink lights.""" +from datetime import timedelta import logging import time +from typing import Any, Dict, NamedTuple, Tuple, cast from pyHS100 import SmartBulb, SmartDeviceException @@ -24,6 +26,7 @@ from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN from .common import async_add_entities_retry PARALLEL_UPDATES = 0 +SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) @@ -72,192 +75,327 @@ def brightness_from_percentage(percent): return (percent * 255.0) / 100.0 +LightState = NamedTuple( + "LightState", + ( + ("state", bool), + ("brightness", int), + ("color_temp", float), + ("hs", Tuple[int, int]), + ("emeter_params", dict), + ), +) + + +LightFeatures = NamedTuple( + "LightFeatures", + ( + ("sysinfo", Dict[str, Any]), + ("mac", str), + ("alias", str), + ("model", str), + ("supported_features", int), + ("min_mireds", float), + ("max_mireds", float), + ), +) + + class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" def __init__(self, smartbulb: SmartBulb) -> None: """Initialize the bulb.""" self.smartbulb = smartbulb - self._sysinfo = None - self._state = None - self._available = False - self._color_temp = None - self._brightness = None - self._hs = None - self._supported_features = None - self._min_mireds = None - self._max_mireds = None - self._emeter_params = {} - - self._mac = None - self._alias = None - self._model = None + self._light_features = cast(LightFeatures, None) + self._light_state = cast(LightState, None) + self._is_available = True + self._is_setting_light_state = False @property def unique_id(self): """Return a unique ID.""" - return self._mac + return self._light_features.mac @property def name(self): """Return the name of the Smart Bulb.""" - return self._alias + return self._light_features.alias @property def device_info(self): """Return information about the device.""" return { - "name": self._alias, - "model": self._model, + "name": self._light_features.alias, + "model": self._light_features.model, "manufacturer": "TP-Link", - "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac)}, - "sw_version": self._sysinfo["sw_ver"], + "connections": {(dr.CONNECTION_NETWORK_MAC, self._light_features.mac)}, + "sw_version": self._light_features.sysinfo["sw_ver"], } @property def available(self) -> bool: """Return if bulb is available.""" - return self._available + return self._is_available @property def device_state_attributes(self): """Return the state attributes of the device.""" - return self._emeter_params + return self._light_state.emeter_params - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" - self._state = True - self.smartbulb.state = SmartBulb.BULB_STATE_ON + brightness = ( + int(kwargs[ATTR_BRIGHTNESS]) + if ATTR_BRIGHTNESS in kwargs + else self._light_state.brightness + if self._light_state.brightness is not None + else 255 + ) + color_tmp = ( + int(kwargs[ATTR_COLOR_TEMP]) + if ATTR_COLOR_TEMP in kwargs + else self._light_state.color_temp + ) - if ATTR_COLOR_TEMP in kwargs: - self._color_temp = kwargs.get(ATTR_COLOR_TEMP) - self.smartbulb.color_temp = mired_to_kelvin(self._color_temp) + await self.async_set_light_state_retry( + self._light_state, + LightState( + state=True, + brightness=brightness, + color_temp=color_tmp, + hs=tuple(kwargs.get(ATTR_HS_COLOR, self._light_state.hs or ())), + emeter_params=self._light_state.emeter_params, + ), + ) - brightness_value = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - brightness_pct = brightness_to_percentage(brightness_value) - if ATTR_HS_COLOR in kwargs: - self._hs = kwargs.get(ATTR_HS_COLOR) - hue, sat = self._hs - hsv = (int(hue), int(sat), brightness_pct) - self.smartbulb.hsv = hsv - elif ATTR_BRIGHTNESS in kwargs: - self._brightness = brightness_value - self.smartbulb.brightness = brightness_pct - - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - self._state = False - self.smartbulb.state = SmartBulb.BULB_STATE_OFF + await self.async_set_light_state_retry( + self._light_state, + LightState( + state=False, + brightness=self._light_state.brightness, + color_temp=self._light_state.color_temp, + hs=self._light_state.hs, + emeter_params=self._light_state.emeter_params, + ), + ) @property def min_mireds(self): """Return minimum supported color temperature.""" - return self._min_mireds + return self._light_features.min_mireds @property def max_mireds(self): """Return maximum supported color temperature.""" - return self._max_mireds + return self._light_features.max_mireds @property def color_temp(self): """Return the color temperature of this light in mireds for HA.""" - return self._color_temp + return self._light_state.color_temp @property def brightness(self): """Return the brightness of this light between 0..255.""" - return self._brightness + return self._light_state.brightness @property def hs_color(self): """Return the color.""" - return self._hs + return self._light_state.hs @property def is_on(self): """Return True if device is on.""" - return self._state + return self._light_state.state def update(self): """Update the TP-Link Bulb's state.""" - if self._supported_features is None: - # First run, update by blocking. - self.do_update() + # State is currently being set, ignore. + if self._is_setting_light_state: + return + + # Initial run, perform call blocking. + if not self._light_features: + self.do_update_retry(False) + # Subsequent runs should not block. else: - # Not first run, update in the background. - self.hass.add_job(self.do_update) + self.hass.add_job(self.do_update_retry, True) - def do_update(self): - """Update states.""" + def do_update_retry(self, update_state: bool) -> None: + """Update state data with retry.""" "" try: - if self._supported_features is None: - self.get_features() - - self._state = self.smartbulb.state == SmartBulb.BULB_STATE_ON - - if self._supported_features & SUPPORT_BRIGHTNESS: - self._brightness = brightness_from_percentage(self.smartbulb.brightness) - - if self._supported_features & SUPPORT_COLOR_TEMP: - if ( - self.smartbulb.color_temp is not None - and self.smartbulb.color_temp != 0 - ): - self._color_temp = kelvin_to_mired(self.smartbulb.color_temp) - - if self._supported_features & SUPPORT_COLOR: - hue, sat, _ = self.smartbulb.hsv - self._hs = (hue, sat) - - if self.smartbulb.has_emeter: - self._emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( - self.smartbulb.current_consumption() - ) - daily_statistics = self.smartbulb.get_emeter_daily() - monthly_statistics = self.smartbulb.get_emeter_monthly() - try: - self._emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format( - daily_statistics[int(time.strftime("%d"))] - ) - self._emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format( - monthly_statistics[int(time.strftime("%m"))] - ) - except KeyError: - # device returned no daily/monthly history - pass - - self._available = True - + # Update light features only once. + self._light_features = ( + self._light_features or self.get_light_features_retry() + ) + self._light_state = self.get_light_state_retry(self._light_features) + self._is_available = True except (SmartDeviceException, OSError) as ex: - if self._available: + if self._is_available: _LOGGER.warning( - "Could not read state for %s: %s", self.smartbulb.host, ex + "Could not read data for %s: %s", self.smartbulb.host, ex ) - self._available = False + self._is_available = False + + # The local variables were updates asyncronousally, + # we need the entity registry to poll this object's properties for + # updated information. Calling schedule_update_ha_state will only + # cause a loop. + if update_state: + self.schedule_update_ha_state() @property def supported_features(self): """Flag supported features.""" - return self._supported_features + return self._light_features.supported_features - def get_features(self): + def get_light_features_retry(self) -> LightFeatures: + """Retry the retrieval of the supported features.""" + try: + return self.get_light_features() + except (SmartDeviceException, OSError): + pass + + _LOGGER.debug("Retrying getting light features") + return self.get_light_features() + + def get_light_features(self): """Determine all supported features in one go.""" - self._sysinfo = self.smartbulb.sys_info - self._supported_features = 0 - self._mac = self.smartbulb.mac - self._alias = self.smartbulb.alias - self._model = self.smartbulb.model + sysinfo = self.smartbulb.sys_info + supported_features = 0 + mac = self.smartbulb.mac + alias = self.smartbulb.alias + model = self.smartbulb.model + min_mireds = None + max_mireds = None if self.smartbulb.is_dimmable: - self._supported_features += SUPPORT_BRIGHTNESS + supported_features += SUPPORT_BRIGHTNESS if getattr(self.smartbulb, "is_variable_color_temp", False): - self._supported_features += SUPPORT_COLOR_TEMP - self._min_mireds = kelvin_to_mired( - self.smartbulb.valid_temperature_range[1] - ) - self._max_mireds = kelvin_to_mired( - self.smartbulb.valid_temperature_range[0] - ) + supported_features += SUPPORT_COLOR_TEMP + min_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[1]) + max_mireds = kelvin_to_mired(self.smartbulb.valid_temperature_range[0]) if getattr(self.smartbulb, "is_color", False): - self._supported_features += SUPPORT_COLOR + supported_features += SUPPORT_COLOR + + return LightFeatures( + sysinfo=sysinfo, + mac=mac, + alias=alias, + model=model, + supported_features=supported_features, + min_mireds=min_mireds, + max_mireds=max_mireds, + ) + + def get_light_state_retry(self, light_features: LightFeatures) -> LightState: + """Retry the retrieval of getting light states.""" + try: + return self.get_light_state(light_features) + except (SmartDeviceException, OSError): + pass + + _LOGGER.debug("Retrying getting light state") + return self.get_light_state(light_features) + + def get_light_state(self, light_features: LightFeatures) -> LightState: + """Get the light state.""" + emeter_params = {} + brightness = None + color_temp = None + hue_saturation = None + state = self.smartbulb.state == SmartBulb.BULB_STATE_ON + + if light_features.supported_features & SUPPORT_BRIGHTNESS: + brightness = brightness_from_percentage(self.smartbulb.brightness) + + if light_features.supported_features & SUPPORT_COLOR_TEMP: + if self.smartbulb.color_temp is not None and self.smartbulb.color_temp != 0: + color_temp = kelvin_to_mired(self.smartbulb.color_temp) + + if light_features.supported_features & SUPPORT_COLOR: + hue, sat, _ = self.smartbulb.hsv + hue_saturation = (hue, sat) + + if self.smartbulb.has_emeter: + emeter_params[ATTR_CURRENT_POWER_W] = "{:.1f}".format( + self.smartbulb.current_consumption() + ) + daily_statistics = self.smartbulb.get_emeter_daily() + monthly_statistics = self.smartbulb.get_emeter_monthly() + try: + emeter_params[ATTR_DAILY_ENERGY_KWH] = "{:.3f}".format( + daily_statistics[int(time.strftime("%d"))] + ) + emeter_params[ATTR_MONTHLY_ENERGY_KWH] = "{:.3f}".format( + monthly_statistics[int(time.strftime("%m"))] + ) + except KeyError: + # device returned no daily/monthly history + pass + + return LightState( + state=state, + brightness=brightness, + color_temp=color_temp, + hs=hue_saturation, + emeter_params=emeter_params, + ) + + async def async_set_light_state_retry( + self, old_light_state: LightState, new_light_state: LightState + ) -> None: + """Set the light state with retry.""" + # Optimistically setting the light state. + self._light_state = new_light_state + + # Tell the device to set the states. + self._is_setting_light_state = True + try: + await self.hass.async_add_executor_job( + self.set_light_state, old_light_state, new_light_state + ) + self._is_available = True + self._is_setting_light_state = False + return + except (SmartDeviceException, OSError): + pass + + try: + _LOGGER.debug("Retrying setting light state") + await self.hass.async_add_executor_job( + self.set_light_state, old_light_state, new_light_state + ) + self._is_available = True + except (SmartDeviceException, OSError) as ex: + self._is_available = False + _LOGGER.warning("Could not set data for %s: %s", self.smartbulb.host, ex) + + self._is_setting_light_state = False + + def set_light_state( + self, old_light_state: LightState, new_light_state: LightState + ) -> None: + """Set the light state.""" + # Calling the API with the new state information. + if new_light_state.state != old_light_state.state: + if new_light_state.state: + self.smartbulb.state = SmartBulb.BULB_STATE_ON + else: + self.smartbulb.state = SmartBulb.BULB_STATE_OFF + return + + if new_light_state.color_temp != old_light_state.color_temp: + self.smartbulb.color_temp = mired_to_kelvin(new_light_state.color_temp) + + brightness_pct = brightness_to_percentage(new_light_state.brightness) + if new_light_state.hs != old_light_state.hs and len(new_light_state.hs) > 1: + hue, sat = new_light_state.hs + hsv = (int(hue), int(sat), brightness_pct) + self.smartbulb.hsv = hsv + elif new_light_state.brightness != old_light_state.brightness: + self.smartbulb.brightness = brightness_pct diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json index dccd39b6997..92b1f3e6b29 100644 --- a/homeassistant/components/traccar/.translations/de.json +++ b/homeassistant/components/traccar/.translations/de.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "M\u00f6chten Sie Traccar wirklich einrichten?", + "description": "M\u00f6chtest du Traccar wirklich einrichten?", "title": "Traccar einrichten" } }, diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index 4c0a3146eb8..736a6d72659 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -36,7 +36,7 @@ "scan_interval": "Aktualisierungsfrequenz" }, "description": "Konfigurieren von Optionen f\u00fcr Transmission", - "title": "Konfigurieren Sie die Optionen f\u00fcr die \u00dcbertragung" + "title": "Konfiguriere die Optionen f\u00fcr die \u00dcbertragung" } } } diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 222737b90c9..9f876dde505 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -14,7 +14,7 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission" }, "user": { "data": { @@ -35,8 +35,8 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission", - "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission" } } } diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 6bedc793ed9..0db731d6f01 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -11,11 +11,6 @@ from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import config from configuration.yaml.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index adf94c64fd6..1756df7baee 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -11,11 +11,6 @@ from .const import DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import config from configuration.yaml.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 8ae06771618..318101605e8 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -501,14 +501,12 @@ class Provider: """Load tts audio file from provider.""" raise NotImplementedError() - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load tts audio file from provider. Return a tuple of file extension and data as bytes. - - This method must be run in the event loop and returns a coroutine. """ - return self.hass.async_add_job( + return await self.hass.async_add_job( ft.partial(self.get_tts_audio, message, language, options=options) ) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index eb0ef5eca2f..8537e61a3ae 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -122,7 +122,7 @@ class TuyaClimateDevice(TuyaDevice, ClimateDevice): @property def fan_modes(self): """Return the list of available fan modes.""" - return self.tuya.fan_modes() + return self.tuya.fan_list() def set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/twentemilieu/.translations/de.json b/homeassistant/components/twentemilieu/.translations/de.json index 502a54a8a3d..586e36a5d31 100644 --- a/homeassistant/components/twentemilieu/.translations/de.json +++ b/homeassistant/components/twentemilieu/.translations/de.json @@ -14,7 +14,7 @@ "house_number": "Hausnummer", "post_code": "Postleitzahl" }, - "description": "Richten Sie Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.", + "description": "Richte Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.", "title": "Twente Milieu" } }, diff --git a/homeassistant/components/twilio/.translations/de.json b/homeassistant/components/twilio/.translations/de.json index 91a195780fd..46e53e182a1 100644 --- a/homeassistant/components/twilio/.translations/de.json +++ b/homeassistant/components/twilio/.translations/de.json @@ -5,11 +5,11 @@ "one_instance_allowed": "Es ist nur eine einzige Instanz erforderlich." }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLesen Sie in der [Dokumentation]({docs_url}) wie Sie Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurieren." + "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { - "description": "M\u00f6chten Sie Twilio wirklich einrichten?", + "description": "M\u00f6chtest du Twilio wirklich einrichten?", "title": "Twilio-Webhook einrichten" } }, diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 920937142a2..910a3debc1e 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -2,7 +2,7 @@ "domain": "ubee", "name": "Ubee Router", "documentation": "https://www.home-assistant.io/integrations/ubee", - "requirements": ["pyubee==0.7"], + "requirements": ["pyubee==0.8"], "dependencies": [], - "codeowners": [] + "codeowners": ["@mzdrale"] } diff --git a/homeassistant/components/unifi/.translations/sv.json b/homeassistant/components/unifi/.translations/sv.json index 864c887d6fe..bc1d9f8cb72 100644 --- a/homeassistant/components/unifi/.translations/sv.json +++ b/homeassistant/components/unifi/.translations/sv.json @@ -22,5 +22,15 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "init": { + "data": { + "one": "Tom", + "other": "Tomma" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/zh-Hans.json b/homeassistant/components/unifi/.translations/zh-Hans.json index 80ed9eb2fa5..2bc6bda37e4 100644 --- a/homeassistant/components/unifi/.translations/zh-Hans.json +++ b/homeassistant/components/unifi/.translations/zh-Hans.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u63a7\u5236\u5668" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u8ddd\u79bb\u4e0a\u6b21\u53d1\u73b0\u591a\u5c11\u79d2\u540e\u8ba4\u4e3a\u79bb\u5f00", + "track_clients": "\u8ddf\u8e2a\u7f51\u7edc\u5ba2\u6237\u7aef", + "track_devices": "\u8ddf\u8e2a\u7f51\u7edc\u8bbe\u5907\uff08Ubiquiti \u8bbe\u5907\uff09", + "track_wired_clients": "\u5305\u62ec\u6709\u7ebf\u7f51\u7edc\u5ba2\u6237\u7aef" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 37d4cf138f2..803793d0683 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -137,10 +137,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): self._state_template.hass = hass async def async_added_to_hass(self): - """Subscribe to children and template state changes. - - This method must be run in the event loop and returns a coroutine. - """ + """Subscribe to children and template state changes.""" @callback def async_on_dependency_update(*_): @@ -416,132 +413,79 @@ class UniversalMediaPlayer(MediaPlayerDevice): """When was the position of the current playing media valid.""" return self._child_attr(ATTR_MEDIA_POSITION_UPDATED_AT) - def async_turn_on(self): - """Turn the media player on. + async def async_turn_on(self): + """Turn the media player on.""" + await self._async_call_service(SERVICE_TURN_ON, allow_override=True) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_TURN_ON, allow_override=True) + async def async_turn_off(self): + """Turn the media player off.""" + await self._async_call_service(SERVICE_TURN_OFF, allow_override=True) - def async_turn_off(self): - """Turn the media player off. - - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_TURN_OFF, allow_override=True) - - def async_mute_volume(self, mute): - """Mute the volume. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_mute_volume(self, mute): + """Mute the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: mute} - return self._async_call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) + await self._async_call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) - def async_set_volume_level(self, volume): - """Set volume level, range 0..1. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume} - return self._async_call_service(SERVICE_VOLUME_SET, data, allow_override=True) + await self._async_call_service(SERVICE_VOLUME_SET, data, allow_override=True) - def async_media_play(self): - """Send play command. + async def async_media_play(self): + """Send play command.""" + await self._async_call_service(SERVICE_MEDIA_PLAY) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_PLAY) + async def async_media_pause(self): + """Send pause command.""" + await self._async_call_service(SERVICE_MEDIA_PAUSE) - def async_media_pause(self): - """Send pause command. + async def async_media_stop(self): + """Send stop command.""" + await self._async_call_service(SERVICE_MEDIA_STOP) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_PAUSE) + async def async_media_previous_track(self): + """Send previous track command.""" + await self._async_call_service(SERVICE_MEDIA_PREVIOUS_TRACK) - def async_media_stop(self): - """Send stop command. + async def async_media_next_track(self): + """Send next track command.""" + await self._async_call_service(SERVICE_MEDIA_NEXT_TRACK) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_STOP) - - def async_media_previous_track(self): - """Send previous track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_PREVIOUS_TRACK) - - def async_media_next_track(self): - """Send next track command. - - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_NEXT_TRACK) - - def async_media_seek(self, position): - """Send seek command. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_media_seek(self, position): + """Send seek command.""" data = {ATTR_MEDIA_SEEK_POSITION: position} - return self._async_call_service(SERVICE_MEDIA_SEEK, data) + await self._async_call_service(SERVICE_MEDIA_SEEK, data) - def async_play_media(self, media_type, media_id, **kwargs): - """Play a piece of media. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} - return self._async_call_service(SERVICE_PLAY_MEDIA, data) + await self._async_call_service(SERVICE_PLAY_MEDIA, data) - def async_volume_up(self): - """Turn volume up for media player. + async def async_volume_up(self): + """Turn volume up for media player.""" + await self._async_call_service(SERVICE_VOLUME_UP, allow_override=True) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_VOLUME_UP, allow_override=True) + async def async_volume_down(self): + """Turn volume down for media player.""" + await self._async_call_service(SERVICE_VOLUME_DOWN, allow_override=True) - def async_volume_down(self): - """Turn volume down for media player. + async def async_media_play_pause(self): + """Play or pause the media player.""" + await self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_VOLUME_DOWN, allow_override=True) - - def async_media_play_pause(self): - """Play or pause the media player. - - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_MEDIA_PLAY_PAUSE) - - def async_select_source(self, source): - """Set the input source. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_select_source(self, source): + """Set the input source.""" data = {ATTR_INPUT_SOURCE: source} - return self._async_call_service( - SERVICE_SELECT_SOURCE, data, allow_override=True - ) + await self._async_call_service(SERVICE_SELECT_SOURCE, data, allow_override=True) - def async_clear_playlist(self): - """Clear players playlist. + async def async_clear_playlist(self): + """Clear players playlist.""" + await self._async_call_service(SERVICE_CLEAR_PLAYLIST) - This method must be run in the event loop and returns a coroutine. - """ - return self._async_call_service(SERVICE_CLEAR_PLAYLIST) - - def async_set_shuffle(self, shuffle): - """Enable/disable shuffling. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_set_shuffle(self, shuffle): + """Enable/disable shuffling.""" data = {ATTR_MEDIA_SHUFFLE: shuffle} - return self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True) + await self._async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=True) async def async_update(self): """Update state in HA.""" diff --git a/homeassistant/components/upnp/.translations/de.json b/homeassistant/components/upnp/.translations/de.json index 907bfffbeea..253dfd59a6c 100644 --- a/homeassistant/components/upnp/.translations/de.json +++ b/homeassistant/components/upnp/.translations/de.json @@ -14,7 +14,7 @@ }, "step": { "confirm": { - "description": "M\u00f6chten Sie UPnP/IGD einrichten?", + "description": "M\u00f6chtest du UPnP/IGD einrichten?", "title": "UPnP/IGD" }, "init": { diff --git a/homeassistant/components/vacuum/.translations/zh-Hans.json b/homeassistant/components/vacuum/.translations/zh-Hans.json new file mode 100644 index 00000000000..b676cc7be9d --- /dev/null +++ b/homeassistant/components/vacuum/.translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u4f7f {entity_name} \u5f00\u59cb\u6e05\u626b" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u626b", + "is_docked": "{entity_name} \u6b63\u505c\u9760\u5728\u5e95\u5ea7\u4e0a" + }, + "trigger_type": { + "cleaning": "{entity_name} \u5f00\u59cb\u6e05\u626b", + "docked": "{entity_name} \u8fd4\u56de\u5e95\u5ea7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 5a2eefd94f2..cb17505f6e1 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -11,7 +11,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_TYPE, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -62,6 +62,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 8e00bc3fee5..d8f9dae13de 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -30,13 +30,11 @@ async def async_setup(hass, config): # Import from the configuration file if needed if DOMAIN not in config: return True - port = config[DOMAIN].get(CONF_PORT) data = {} if port: data = {CONF_PORT: port, CONF_NAME: "Velbus import"} - hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=data @@ -55,7 +53,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): discovery_info = {"cntrl": controller} for category in COMPONENT_TYPES: discovery_info[category] = [] - for module in modules: for channel in range(1, module.number_of_channels() + 1): for category in COMPONENT_TYPES: @@ -63,7 +60,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): discovery_info[category].append( (module.get_module_address(), channel) ) - hass.data[DOMAIN][entry.entry_id] = discovery_info for category in COMPONENT_TYPES: diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 505303ded24..86f4e7a7cd8 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -9,11 +9,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus binary sensor based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index 812e4605d95..e322cfb77c7 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -16,11 +16,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Velbus binary sensors.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus binary sensor based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index aea02331ead..4478bb81c3c 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -4,8 +4,10 @@ import logging from velbus.util import VelbusException from homeassistant.components.cover import ( + ATTR_POSITION, SUPPORT_CLOSE, SUPPORT_OPEN, + SUPPORT_SET_POSITION, SUPPORT_STOP, CoverDevice, ) @@ -16,11 +18,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Velbus covers.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus cover based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] @@ -38,24 +35,26 @@ class VelbusCover(VelbusEntity, CoverDevice): @property def supported_features(self): """Flag supported features.""" + if self._module.support_position(): + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP @property def is_closed(self): """Return if the cover is closed.""" - return self._module.is_closed(self._channel) + if self._module.get_position(self._channel) == 100: + return True + return False @property def current_cover_position(self): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open + Velbus: 100 = closed, 0 = open """ - if self._module.is_closed(self._channel): - return 0 - if self._module.is_open(self._channel): - return 100 - return None + pos = self._module.get_position(self._channel) + return 100 - pos def open_cover(self, **kwargs): """Open the cover.""" @@ -77,3 +76,10 @@ class VelbusCover(VelbusEntity, CoverDevice): self._module.stop(self._channel) except VelbusException as err: _LOGGER.error("A Velbus error occurred: %s", err) + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + try: + self._module.set(self._channel, (100 - kwargs[ATTR_POSITION])) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index 7db79e74d5b..d428b766edc 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -21,11 +21,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus light based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 250b2c01e4e..258b367fa5b 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,8 +2,8 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.0.35"], + "requirements": ["python-velbus==2.0.36"], "config_flow": true, "dependencies": [], - "codeowners": ["@cereal2nd"] + "codeowners": ["@Cereal2nd", "@brefra"] } diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 8af5df9e165..d8644b4569a 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,17 +1,14 @@ """Support for Velbus sensors.""" import logging +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR + from . import VelbusEntity from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus sensor based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] @@ -20,23 +17,53 @@ async def async_setup_entry(hass, entry, async_add_entities): for address, channel in modules_data: module = cntrl.get_module(address) entities.append(VelbusSensor(module, channel)) + if module.get_class(channel) == "counter": + entities.append(VelbusSensor(module, channel, True)) async_add_entities(entities) class VelbusSensor(VelbusEntity): """Representation of a sensor.""" + def __init__(self, module, channel, counter=False): + """Initialize a sensor Velbus entity.""" + super().__init__(module, channel) + self._is_counter = counter + + @property + def unique_id(self): + """Return unique ID for counter sensors.""" + unique_id = super().unique_id + if self._is_counter: + unique_id = f"{unique_id}-counter" + return unique_id + @property def device_class(self): """Return the device class of the sensor.""" + if self._module.get_class(self._channel) == "counter" and not self._is_counter: + if self._module.get_counter_unit(self._channel) == ENERGY_KILO_WATT_HOUR: + return DEVICE_CLASS_POWER + return None return self._module.get_class(self._channel) @property def state(self): """Return the state of the sensor.""" + if self._is_counter: + return self._module.get_counter_state(self._channel) return self._module.get_state(self._channel) @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" + if self._is_counter: + return self._module.get_counter_unit(self._channel) return self._module.get_unit(self._channel) + + @property + def icon(self): + """Icon to use in the frontend.""" + if self._is_counter: + return "mdi:counter" + return None diff --git a/homeassistant/components/velbus/switch.py b/homeassistant/components/velbus/switch.py index ead83f7d3cf..64d4b7c17f8 100644 --- a/homeassistant/components/velbus/switch.py +++ b/homeassistant/components/velbus/switch.py @@ -11,11 +11,6 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up Velbus switch based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json index 231588c8bf3..75614336c3d 100644 --- a/homeassistant/components/versasense/manifest.json +++ b/homeassistant/components/versasense/manifest.json @@ -1,7 +1,7 @@ { "domain": "versasense", "name": "VersaSense", - "documentation": "https://www.home-assistant.io/components/versasense", + "documentation": "https://www.home-assistant.io/integrations/versasense", "dependencies": [], "codeowners": ["@flamm3blemuff1n"], "requirements": ["pyversasense==0.0.6"] diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index aaa96fb0b96..37f88d16654 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.1.0"], + "requirements": ["pyhaversion==3.2.0"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "internal" diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 282e234811a..7632a101769 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR _LOGGER = logging.getLogger(__name__) @@ -54,7 +55,7 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Create the ViCare component.""" conf = config[DOMAIN] - params = {"token_file": "/tmp/vicare_token.save"} + params = {"token_file": hass.config.path(STORAGE_DIR, "vicare_token.save")} if conf.get(CONF_CIRCUIT) is not None: params["circuit"] = conf[CONF_CIRCUIT] diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json new file mode 100644 index 00000000000..abbf1092bf3 --- /dev/null +++ b/homeassistant/components/vizio/.translations/ca.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "host_exists": "L'amfitri\u00f3 ja est\u00e0 configurat.", + "name_exists": "El nom ja est\u00e0 configurat." + }, + "step": { + "user": { + "data": { + "access_token": "Testimoni d'acc\u00e9s", + "device_class": "Tipus de dispositiu", + "host": ":", + "name": "Nom" + }, + "title": "Configuraci\u00f3 del client de Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "volume_step": "Mida del pas de volum" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json new file mode 100644 index 00000000000..7a6dda98270 --- /dev/null +++ b/homeassistant/components/vizio/.translations/da.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurationsproces for Vizio-komponenten er allerede i gang.", + "already_setup": "Denne post er allerede blevet konfigureret.", + "already_setup_with_diff_host_and_name": "Denne post ser ud til allerede at v\u00e6re konfigureret med en anden v\u00e6rt og navn baseret p\u00e5 dens serienummer. Fjern eventuelle gamle poster fra din configuration.yaml og i menuen Integrationer, f\u00f8r du fors\u00f8ger at tilf\u00f8je denne enhed igen.", + "host_exists": "Vizio-komponent med v\u00e6rt er allerede konfigureret.", + "name_exists": "Vizio-komponent med navn er allerede konfigureret.", + "updated_options": "Denne post er allerede konfigureret, men indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med de tidligere importerede indstillingsv\u00e6rdier, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed.", + "updated_volume_step": "Denne post er allerede konfigureret, men lydstyrketrinst\u00f8rrelsen i konfigurationen stemmer ikke overens med konfigurationsposten, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed." + }, + "error": { + "cant_connect": "Kunne ikke oprette forbindelse til enheden. [Gennemg\u00e5 dokumentationen] (https://www.home-assistant.io/integrations/vizio/), og bekr\u00e6ft, at: \n - Enheden er t\u00e6ndt \n - Enheden er tilsluttet netv\u00e6rket \n - De angivne v\u00e6rdier er korrekte \n f\u00f8r du fors\u00f8ger at indsende igen.", + "host_exists": "Vizio-enhed med den specificerede v\u00e6rt er allerede konfigureret.", + "name_exists": "Vizio-enhed med det specificerede navn er allerede konfigureret.", + "tv_needs_token": "N\u00e5r enhedstypen er 'tv', skal der bruges en gyldig adgangstoken." + }, + "step": { + "user": { + "data": { + "access_token": "Adgangstoken", + "device_class": "Enhedstype", + "host": ":", + "name": "Navn" + }, + "title": "Ops\u00e6tning af Vizio SmartCast-klient" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout for API-anmodning (sekunder)", + "volume_step": "Lydstyrkestrinst\u00f8rrelse" + }, + "title": "Opdater Vizo SmartCast-indstillinger" + } + }, + "title": "Opdater Vizo SmartCast-indstillinger" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/de.json b/homeassistant/components/vizio/.translations/de.json new file mode 100644 index 00000000000..ead4ed4828b --- /dev/null +++ b/homeassistant/components/vizio/.translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurationsablauf f\u00fcr die Vizio-Komponente wird bereits ausgef\u00fchrt.", + "already_setup": "Dieser Eintrag wurde bereits eingerichtet.", + "host_exists": "Vizio-Komponent mit bereits konfiguriertem Host.", + "name_exists": "Vizio-Komponent mit bereits konfiguriertem Namen.", + "updated_options": "Dieser Eintrag wurde bereits eingerichtet, aber die in der Konfiguration definierten Optionen stimmen nicht mit den zuvor importierten Optionswerten \u00fcberein, daher wurde der Konfigurationseintrag entsprechend aktualisiert.", + "updated_volume_step": "Dieser Eintrag wurde bereits eingerichtet, aber die Lautst\u00e4rken-Schrittgr\u00f6\u00dfe in der Konfiguration stimmt nicht mit dem Konfigurationseintrag \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." + }, + "error": { + "cant_connect": "Es konnte keine Verbindung zum Ger\u00e4t hergestellt werden. [\u00dcberpr\u00fcfen Sie die Dokumentation] (https://www.home-assistant.io/integrations/vizio/) und \u00fcberpr\u00fcfen Sie Folgendes erneut:\n- Das Ger\u00e4t ist eingeschaltet\n- Das Ger\u00e4t ist mit dem Netzwerk verbunden\n- Die von Ihnen eingegebenen Werte sind korrekt\nbevor sie versuchen, erneut zu \u00fcbermitteln.", + "host_exists": "Host bereits konfiguriert.", + "name_exists": "Name bereits konfiguriert.", + "tv_needs_token": "Wenn der Ger\u00e4tetyp \"TV\" ist, wird ein g\u00fcltiger Zugriffstoken ben\u00f6tigt." + }, + "step": { + "user": { + "data": { + "access_token": "Zugangstoken", + "device_class": "Ger\u00e4tetyp", + "host": ":", + "name": "Name" + }, + "title": "Richten Sie den Vizio SmartCast-Client ein" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API Request Timeout (Sekunden)", + "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" + }, + "title": "Aktualisieren Sie die Vizo SmartCast-Optionen" + } + }, + "title": "Aktualisieren Sie die Vizo SmartCast-Optionen" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json new file mode 100644 index 00000000000..60fd9049bb3 --- /dev/null +++ b/homeassistant/components/vizio/.translations/en.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Config flow for vizio component already in progress.", + "already_setup": "This entry has already been setup.", + "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", + "host_exists": "Vizio component with host already configured.", + "name_exists": "Vizio component with name already configured.", + "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", + "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." + }, + "error": { + "cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit.", + "host_exists": "Vizio device with specified host already configured.", + "name_exists": "Vizio device with specified name already configured.", + "tv_needs_token": "When Device Type is `tv` then a valid Access Token is needed." + }, + "step": { + "user": { + "data": { + "access_token": "Access Token", + "device_class": "Device Type", + "host": ":", + "name": "Name" + }, + "title": "Setup Vizio SmartCast Client" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API Request Timeout (seconds)", + "volume_step": "Volume Step Size" + }, + "title": "Update Vizo SmartCast Options" + } + }, + "title": "Update Vizo SmartCast Options" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json new file mode 100644 index 00000000000..009f93a50c6 --- /dev/null +++ b/homeassistant/components/vizio/.translations/es.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Configurar el flujo para el componente vizio que ya est\u00e1 en marcha.", + "already_setup": "Esta entrada ya ha sido configurada.", + "already_setup_with_diff_host_and_name": "Esta entrada parece haber sido ya configurada con un host y un nombre diferentes basados en su n\u00famero de serie. Elimine las entradas antiguas de su archivo configuration.yaml y del men\u00fa Integraciones antes de volver a intentar agregar este dispositivo.", + "host_exists": "Host ya configurado del componente de Vizio", + "name_exists": "Nombre ya configurado del componente de Vizio", + "updated_options": "Esta entrada ya ha sido configurada pero las opciones definidas en la configuraci\u00f3n no coinciden con los valores de las opciones importadas previamente, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", + "updated_volume_step": "Esta entrada ya ha sido configurada pero el tama\u00f1o del paso de volumen en la configuraci\u00f3n no coincide con la entrada de la configuraci\u00f3n, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." + }, + "error": { + "cant_connect": "No se pudo conectar al dispositivo. [Revise los documentos] (https://www.home-assistant.io/integrations/vizio/) y vuelva a verificar que:\n- El dispositivo est\u00e1 encendido\n- El dispositivo est\u00e1 conectado a la red\n- Los valores que ha rellenado son precisos\nantes de intentar volver a enviar.", + "host_exists": "El host ya est\u00e1 configurado.", + "name_exists": "Nombre ya configurado.", + "tv_needs_token": "Cuando el tipo de dispositivo es `tv`, se necesita un token de acceso v\u00e1lido." + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso", + "device_class": "Tipo de dispositivo", + "host": "< Host / IP > : ", + "name": "Nombre" + }, + "title": "Configurar el cliente de Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Tiempo de espera de solicitud de API (segundos)", + "volume_step": "Tama\u00f1o del paso de volumen" + }, + "title": "Actualizar las opciones de SmartCast de Vizo" + } + }, + "title": "Actualizar las opciones de SmartCast de Vizo" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json new file mode 100644 index 00000000000..78d2347bfac --- /dev/null +++ b/homeassistant/components/vizio/.translations/fr.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_in_progress": "Flux de configuration pour le composant Vizio d\u00e9j\u00e0 en cours.", + "already_setup": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e.", + "already_setup_with_diff_host_and_name": "Cette entr\u00e9e semble avoir d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e avec un h\u00f4te et un nom diff\u00e9rents en fonction de son num\u00e9ro de s\u00e9rie. Veuillez supprimer toutes les anciennes entr\u00e9es de votre configuration.yaml et du menu Int\u00e9grations avant de r\u00e9essayer d'ajouter ce p\u00e9riph\u00e9rique.", + "host_exists": "Composant Vizio avec h\u00f4te d\u00e9j\u00e0 configur\u00e9.", + "name_exists": "Composant Vizio dont le nom est d\u00e9j\u00e0 configur\u00e9.", + "updated_options": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais les options d\u00e9finies dans la configuration ne correspondent pas aux valeurs des options pr\u00e9c\u00e9demment import\u00e9es, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", + "updated_volume_step": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e, mais la taille du pas du volume dans la configuration ne correspond pas \u00e0 l'entr\u00e9e de configuration, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." + }, + "error": { + "cant_connect": "Impossible de se connecter \u00e0 l'appareil. [V\u00e9rifier les documents](https://www.home-assistant.io/integrations/vizio/) et rev\u00e9rifier que:\n- L'appareil est sous tension\n- L'appareil est connect\u00e9 au r\u00e9seau\n- Les valeurs que vous avez saisies sont exactes\navant d'essayer de le soumettre \u00e0 nouveau.", + "host_exists": "H\u00f4te d\u00e9j\u00e0 configur\u00e9.", + "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9.", + "tv_needs_token": "Lorsque le type de p\u00e9riph\u00e9rique est \" TV \", un jeton d'acc\u00e8s valide est requis." + }, + "step": { + "user": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "device_class": "Type d'appareil", + "host": ":", + "name": "Nom" + }, + "title": "Configurer le client Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)" + }, + "title": "Mettre \u00e0 jour les options de Vizo SmartCast" + } + }, + "title": "Mettre \u00e0 jour les options de Vizo SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json new file mode 100644 index 00000000000..f3cb7b83026 --- /dev/null +++ b/homeassistant/components/vizio/.translations/it.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Il flusso di configurazione per vizio \u00e8 gi\u00e0 in corso.", + "already_setup": "Questa voce \u00e8 gi\u00e0 stata configurata.", + "already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.", + "host_exists": "Componente Vizio con host gi\u00e0 configurato.", + "name_exists": "Componente Vizio con nome gi\u00e0 configurato.", + "updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.", + "updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza." + }, + "error": { + "cant_connect": "Impossibile connettersi al dispositivo. [Esamina i documenti] (https://www.home-assistant.io/integrations/vizio/) e verifica nuovamente che: \n - Il dispositivo sia acceso \n - Il dispositivo sia collegato alla rete \n - I valori inseriti siano corretti \n prima di ritentare.", + "host_exists": "Dispositivo Vizio con host specificato gi\u00e0 configurato.", + "name_exists": "Dispositivo Vizio con il nome specificato gi\u00e0 configurato.", + "tv_needs_token": "Quando Device Type \u00e8 `tv`, \u00e8 necessario un token di accesso valido." + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso", + "device_class": "Tipo di dispositivo", + "host": "< Host / IP >: ", + "name": "Nome" + }, + "title": "Installazione del client Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout richiesta API (secondi)", + "volume_step": "Dimensione del passo del volume" + }, + "title": "Aggiornamento delle opzioni di Vizo SmartCast" + } + }, + "title": "Aggiornamento delle opzioni di Vizo SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json new file mode 100644 index 00000000000..3e54d343f7a --- /dev/null +++ b/homeassistant/components/vizio/.translations/ko.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "vizio \uad6c\uc131 \uc694\uc18c\uc5d0 \ub300\ud55c \uad6c\uc131 \ud50c\ub85c\uc6b0\uac00 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", + "already_setup": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "already_setup_with_diff_host_and_name": "\uc774 \ud56d\ubaa9\uc740 \uc2dc\ub9ac\uc5bc \ubc88\ud638\ub85c \ub2e4\ub978 \ud638\uc2a4\ud2b8 \ubc0f \uc774\ub984\uc73c\ub85c \uc774\ubbf8 \uc124\uc815\ub418\uc5b4\uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4. \uc774 \uae30\uae30\ub97c \ucd94\uac00\ud558\uae30 \uc804\uc5d0 configuration.yaml \ubc0f \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uba54\ub274\uc5d0\uc11c \uc774\uc804 \ud56d\ubaa9\uc744 \uc81c\uac70\ud574\uc8fc\uc138\uc694.", + "host_exists": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "name_exists": "\ud574\ub2f9 \uc774\ub984\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "updated_options": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uc635\uc158 \uac12\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "updated_volume_step": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc758 \ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30\uac00 \uad6c\uc131 \ud56d\ubaa9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cant_connect": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. [\uc548\ub0b4\ub97c \ucc38\uace0] (https://www.home-assistant.io/integrations/vizio/)\ud558\uace0 \uc591\uc2dd\uc744 \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \ub2e4\uc74c\uc744 \ub2e4\uc2dc \ud655\uc778\ud574\uc8fc\uc138\uc694.\n- \uae30\uae30 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uc2b5\ub2c8\uae4c?\n- \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc2b5\ub2c8\uae4c?\n- \uc785\ub825\ud55c \ub0b4\uc6a9\uc774 \uc62c\ubc14\ub985\ub2c8\uae4c?", + "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "tv_needs_token": "\uae30\uae30 \uc720\ud615\uc774 'tv' \uc778 \uacbd\uc6b0 \uc720\ud6a8\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "device_class": "\uae30\uae30 \uc885\ub958", + "host": "<\ud638\uc2a4\ud2b8/ip>:", + "name": "\uc774\ub984" + }, + "title": "Vizio SmartCast \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API \uc694\uccad \uc2dc\uac04 \ucd08\uacfc (\ucd08)", + "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" + }, + "title": "Vizo SmartCast \uc635\uc158 \uc5c5\ub370\uc774\ud2b8" + } + }, + "title": "Vizo SmartCast \uc635\uc158 \uc5c5\ub370\uc774\ud2b8" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json new file mode 100644 index 00000000000..965dd7af841 --- /dev/null +++ b/homeassistant/components/vizio/.translations/lb.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguratioun's Oflaf fir Vizio Komponent ass schonn am gaangen.", + "already_setup": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert.", + "already_setup_with_diff_host_and_name": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mat engem aneren Host an Numm bas\u00e9ierend unhand vu\u00a0senger Seriennummer. L\u00e4scht w.e.g. al Entr\u00e9e vun \u00e4rer configuration.yaml a\u00a0vum Integratioun's Men\u00fc ier dir prob\u00e9iert d\u00ebsen Apparate r\u00ebm b\u00e4i ze setzen.", + "host_exists": "Vizio Komponent mam Host ass schon konfigur\u00e9iert.", + "name_exists": "Vizio Komponent mam Numm ass scho konfigur\u00e9iert.", + "updated_options": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert.", + "updated_volume_step": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst an der Konfiguratioun st\u00ebmmt net mat der Konfiguratioun iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." + }, + "error": { + "cant_connect": "Konnt sech net mam Apparat verbannen. [Iwwerpr\u00e9ift Dokumentatioun] (https://www.home-assistant.io/integrations/vizio/) a stellt s\u00e9cher dass:\n- Den Apparat ass un\n- Den Apparat ass mam Netzwierk verbonnen\n- D'Optiounen d\u00e9i dir aginn hutt si korrekt\nier dir d'Verbindung nees prob\u00e9iert", + "host_exists": "Vizio Apparat mat d\u00ebsem Host ass scho konfigur\u00e9iert.", + "name_exists": "Vizio Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert.", + "tv_needs_token": "Wann den Typ vum Apparat `tv`ass da g\u00ebtt ee g\u00ebltegen Acc\u00e8s Jeton ben\u00e9idegt." + }, + "step": { + "user": { + "data": { + "access_token": "Acc\u00e8ss Jeton", + "device_class": "Typ vun Apparat", + "host": ":", + "name": "Numm" + }, + "title": "Vizo Smartcast ariichten" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Z\u00e4itiwwerscheidung bei der Ufro vun der API (sekonnen)", + "volume_step": "Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst" + }, + "title": "Vizo Smartcast Optiounen aktualis\u00e9ieren" + } + }, + "title": "Vizo Smartcast Optiounen aktualis\u00e9ieren" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json new file mode 100644 index 00000000000..cdf16bfe28d --- /dev/null +++ b/homeassistant/components/vizio/.translations/no.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurasjons flyt for Vizio komponent er allerede i gang.", + "already_setup": "Denne oppf\u00f8ringen er allerede konfigurert.", + "already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.", + "host_exists": "Vizio komponent med vert allerede konfigurert.", + "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", + "updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", + "updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter." + }, + "error": { + "cant_connect": "Kunne ikke koble til enheten. [Se gjennom dokumentene] (https://www.home-assistant.io/integrations/vizio/) og bekreft at: \n - Enheten er sl\u00e5tt p\u00e5 \n - Enheten er koblet til nettverket \n - Verdiene du fylte ut er n\u00f8yaktige \n f\u00f8r du pr\u00f8ver \u00e5 sende inn p\u00e5 nytt.", + "host_exists": "Vizio-enhet med spesifisert vert allerede konfigurert.", + "name_exists": "Vizio-enhet med spesifisert navn allerede konfigurert.", + "tv_needs_token": "N\u00e5r enhetstype er `tv`, er det n\u00f8dvendig med en gyldig tilgangstoken." + }, + "step": { + "user": { + "data": { + "access_token": "Tilgangstoken", + "device_class": "Enhetstype", + "host": ":", + "name": "Navn" + }, + "title": "Oppsett Vizio SmartCast Client" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Tidsavbrudd for API-foresp\u00f8rsel (sekunder)", + "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" + }, + "title": "Oppdater Vizo SmartCast alternativer" + } + }, + "title": "Oppdater Vizo SmartCast alternativer" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json new file mode 100644 index 00000000000..f70e6d728df --- /dev/null +++ b/homeassistant/components/vizio/.translations/pl.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguracja komponentu Vizio jest ju\u017c w trakcie.", + "already_setup": "Ten komponent jest ju\u017c skonfigurowany.", + "already_setup_with_diff_host_and_name": "Wygl\u0105da na to, \u017ce ten wpis zosta\u0142 ju\u017c skonfigurowany z innym hostem i nazw\u0105 na podstawie jego numeru seryjnego. Usu\u0144 wszystkie stare wpisy z pliku configuration.yaml i z menu Integracje przed ponown\u0105 pr\u00f3b\u0105 dodania tego urz\u0105dzenia.", + "host_exists": "Komponent Vizio dla tego hosta jest ju\u017c skonfigurowany.", + "name_exists": "Komponent Vizio dla tej nazwy jest ju\u017c skonfigurowany.", + "updated_options": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", + "updated_volume_step": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale rozmiar skoku g\u0142o\u015bno\u015bci w konfiguracji nie pasuje do wpisu konfiguracji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." + }, + "error": { + "cant_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem. [Przejrzyj dokumentacj\u0119] (https://www.home-assistant.io/integrations/vizio/) i ponownie sprawd\u017a, czy: \n - urz\u0105dzenie jest w\u0142\u0105czone,\n - urz\u0105dzenie jest pod\u0142\u0105czone do sieci,\n - wprowadzone warto\u015bci s\u0105 prawid\u0142owe,\n przed pr\u00f3b\u0105 ponownego przes\u0142ania.", + "host_exists": "Urz\u0105dzenie Vizio z okre\u015blonym hostem jest ju\u017c skonfigurowane.", + "name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane.", + "tv_needs_token": "Gdy typem urz\u0105dzenia jest `tv` potrzebny jest prawid\u0142owy token dost\u0119pu." + }, + "step": { + "user": { + "data": { + "access_token": "Token dost\u0119pu", + "device_class": "Typ urz\u0105dzenia", + "host": ":", + "name": "Nazwa" + }, + "title": "Konfiguracja klienta Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", + "volume_step": "Skok g\u0142o\u015bno\u015bci" + }, + "title": "Aktualizacja opcji Vizo SmartCast" + } + }, + "title": "Aktualizuj opcje Vizo SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ru.json b/homeassistant/components/vizio/.translations/ru.json new file mode 100644 index 00000000000..2206336a5b4 --- /dev/null +++ b/homeassistant/components/vizio/.translations/ru.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "already_setup": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", + "already_setup_with_diff_host_and_name": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u044d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0445\u043e\u0441\u0442\u043e\u043c \u0438 \u0438\u043c\u0435\u043d\u0435\u043c \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0435\u0433\u043e \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0432\u0441\u0435 \u0441\u0442\u0430\u0440\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e configuration.yaml \u0438 \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 \"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\" \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "updated_options": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", + "updated_volume_step": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u0448\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cant_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e:\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e;\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0441\u0435\u0442\u0438;\n- \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0432\u0432\u0435\u043b\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/integrations/vizio/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "tv_needs_token": "\u0414\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f `tv` \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "device_class": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "host": "<\u0425\u043e\u0441\u0442/IP>:<\u041f\u043e\u0440\u0442>", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "Vizio SmartCast" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 API (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "volume_step": "\u0428\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Vizio SmartCast" + } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Vizio SmartCast" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/sv.json b/homeassistant/components/vizio/.translations/sv.json new file mode 100644 index 00000000000..2c127f602ce --- /dev/null +++ b/homeassistant/components/vizio/.translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad." + }, + "error": { + "host_exists": "Vizio-enheten med angivet v\u00e4rdnamn \u00e4r redan konfigurerad.", + "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad." + }, + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "device_class": "Enhetstyp", + "name": "Namn" + }, + "title": "St\u00e4ll in Vizio SmartCast-klient" + } + }, + "title": "" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout f\u00f6r API-anrop (sekunder)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json new file mode 100644 index 00000000000..6707a321911 --- /dev/null +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_in_progress": "Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "already_setup": "\u6b64\u7269\u4ef6\u5df2\u8a2d\u5b9a\u904e\u3002", + "already_setup_with_diff_host_and_name": "\u6839\u64da\u6240\u63d0\u4f9b\u7684\u5e8f\u865f\uff0c\u6b64\u7269\u4ef6\u4f3c\u4e4e\u5df2\u7d93\u4f7f\u7528\u4e0d\u540c\u7684\u4e3b\u6a5f\u7aef\u8207\u540d\u7a31\u9032\u884c\u8a2d\u5b9a\u3002\u8acb\u5f9e\u6574\u5408\u9078\u55ae Config.yaml \u4e2d\u79fb\u9664\u820a\u7269\u4ef6\uff0c\u7136\u5f8c\u518d\u65b0\u589e\u6b64\u8a2d\u5099\u3002", + "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "updated_options": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u5b9a\u7fa9\u8207\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", + "updated_volume_step": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u97f3\u91cf\u5927\u5c0f\u8207\u7269\u4ef6\u8a2d\u5b9a\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" + }, + "error": { + "cant_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u8a2d\u5099\u3002[\u8acb\u53c3\u8003\u8aaa\u660e\u6587\u4ef6](https://www.home-assistant.io/integrations/vizio/) \u4e26\u78ba\u8a8d\u4ee5\u4e0b\u9805\u76ee\uff1a\n- \u8a2d\u5099\u5df2\u958b\u6a5f\n- \u8a2d\u5099\u5df2\u9023\u7dda\u81f3\u7db2\u8def\n- \u586b\u5beb\u8cc7\u6599\u6b63\u78ba\n\u7136\u5f8c\u518d\u91cd\u65b0\u50b3\u9001\u3002", + "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "tv_needs_token": "\u7576\u8a2d\u5099\u985e\u5225\u70ba\u300cTV\u300d\u6642\uff0c\u9700\u8981\u5b58\u53d6\u5bc6\u9470\u3002" + }, + "step": { + "user": { + "data": { + "access_token": "\u5b58\u53d6\u5bc6\u9470", + "device_class": "\u8a2d\u5099\u985e\u5225", + "host": "<\u4e3b\u6a5f\u7aef/IP>:", + "name": "\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Vizio SmartCast \u5ba2\u6236\u7aef" + } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "API \u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09", + "volume_step": "\u97f3\u91cf\u5927\u5c0f" + }, + "title": "\u66f4\u65b0 Vizo SmartCast \u9078\u9805" + } + }, + "title": "\u66f4\u65b0 Vizo SmartCast \u9078\u9805" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 3ffbf46f928..436ad829d94 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,41 +1,76 @@ """The vizio component.""" +import asyncio + import voluptuous as vol -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_DEVICE_CLASS, - CONF_HOST, - CONF_NAME, -) +from homeassistant.components.media_player import DEVICE_CLASS_TV +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import ( - CONF_VOLUME_STEP, - DEFAULT_DEVICE_CLASS, - DEFAULT_NAME, - DEFAULT_VOLUME_STEP, -) +from .const import DOMAIN, VIZIO_SCHEMA -def validate_auth(config): - """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" +def validate_auth(config: ConfigType) -> ConfigType: + """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS == DEVICE_CLASS_TV.""" token = config.get(CONF_ACCESS_TOKEN) - if config[CONF_DEVICE_CLASS] == "tv" and not token: + if config[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV and not token: raise vol.Invalid( - f"When '{CONF_DEVICE_CLASS}' is 'tv' then '{CONF_ACCESS_TOKEN}' is required.", + f"When '{CONF_DEVICE_CLASS}' is '{DEVICE_CLASS_TV}' then " + f"'{CONF_ACCESS_TOKEN}' is required.", path=[CONF_ACCESS_TOKEN], ) + return config -VIZIO_SCHEMA = { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.All( - cv.string, vol.Lower, vol.In(["tv", "soundbar"]) - ), - vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All( - vol.Coerce(int), vol.Range(min=1, max=10) - ), -} +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, [vol.All(vol.Schema(VIZIO_SCHEMA), validate_auth)] + ) + }, + extra=vol.ALLOW_EXTRA, +) + +PLATFORMS = ["media_player"] + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Component setup, run import config flow for each entry in config.""" + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Load the saved entities.""" + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ] + ) + ) + + return unload_ok diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py new file mode 100644 index 00000000000..04f70da4a8c --- /dev/null +++ b/homeassistant/components/vizio/config_flow.py @@ -0,0 +1,229 @@ +"""Config flow for Vizio.""" +import logging +from typing import Any, Dict + +from pyvizio import VizioAsync, async_guess_device_type +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, +) +from homeassistant.core import callback + +from . import validate_auth +from .const import ( + CONF_VOLUME_STEP, + DEFAULT_DEVICE_CLASS, + DEFAULT_NAME, + DEFAULT_VOLUME_STEP, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +def _get_config_flow_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: + """Return schema defaults based on user input/config dict. Retain info already provided for future form views by setting them as defaults in schema.""" + if input_dict is None: + input_dict = {} + + return vol.Schema( + { + vol.Required( + CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_HOST, default=input_dict.get(CONF_HOST)): str, + vol.Optional( + CONF_DEVICE_CLASS, + default=input_dict.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), + ): vol.All(str, vol.Lower, vol.In([DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER])), + vol.Optional( + CONF_ACCESS_TOKEN, default=input_dict.get(CONF_ACCESS_TOKEN, "") + ): str, + }, + extra=vol.REMOVE_EXTRA, + ) + + +def _host_is_same(host1: str, host2: str) -> bool: + """Check if host1 and host2 are the same.""" + return host1.split(":")[0] == host2.split(":")[0] + + +class VizioOptionsConfigFlow(config_entries.OptionsFlow): + """Handle Transmission client options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize vizio options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """Manage the vizio options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_VOLUME_STEP, + default=self.config_entry.options.get( + CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP + ), + ): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Vizio config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> VizioOptionsConfigFlow: + """Get the options flow for this handler.""" + return VizioOptionsConfigFlow(config_entry) + + def __init__(self) -> None: + """Initialize config flow.""" + self._user_schema = None + self._must_show_form = None + + async def async_step_user( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + # Store current values in case setup fails and user needs to edit + self._user_schema = _get_config_flow_schema(user_input) + + # Check if new config entry matches any existing config entries + for entry in self.hass.config_entries.async_entries(DOMAIN): + if _host_is_same(entry.data[CONF_HOST], user_input[CONF_HOST]): + errors[CONF_HOST] = "host_exists" + break + + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + + if not errors: + try: + # Ensure schema passes custom validation, otherwise catch exception and add error + validate_auth(user_input) + + # Ensure config is valid for a device + if not await VizioAsync.validate_ha_config( + user_input[CONF_HOST], + user_input.get(CONF_ACCESS_TOKEN), + user_input[CONF_DEVICE_CLASS], + ): + errors["base"] = "cant_connect" + except vol.Invalid: + errors["base"] = "tv_needs_token" + + if not errors: + # Skip validating config and creating entry if form must be shown + if self._must_show_form: + self._must_show_form = False + else: + # Abort flow if existing entry with same unique ID matches new config entry. + # Since name and host check have already passed, if an entry already exists, + # It is likely a reconfigured device. + unique_id = await VizioAsync.get_unique_id( + user_input[CONF_HOST], + user_input.get(CONF_ACCESS_TOKEN), + user_input[CONF_DEVICE_CLASS], + ) + + if await self.async_set_unique_id( + unique_id=unique_id, raise_on_progress=True + ): + return self.async_abort( + reason="already_setup_with_diff_host_and_name" + ) + + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + # Use user_input params as default values for schema if user_input is non-empty, otherwise use default schema + schema = self._user_schema or _get_config_flow_schema() + + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_import(self, import_config: Dict[str, Any]) -> Dict[str, Any]: + """Import a config entry from configuration.yaml.""" + # Check if new config entry matches any existing config entries + for entry in self.hass.config_entries.async_entries(DOMAIN): + if _host_is_same(entry.data[CONF_HOST], import_config[CONF_HOST]): + updated_options = {} + updated_name = {} + + if entry.data[CONF_NAME] != import_config[CONF_NAME]: + updated_name[CONF_NAME] = import_config[CONF_NAME] + + if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: + updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] + + if updated_options or updated_name: + new_data = entry.data.copy() + new_options = entry.options.copy() + + if updated_name: + new_data.update(updated_name) + + if updated_options: + new_data.update(updated_options) + new_options.update(updated_options) + + self.hass.config_entries.async_update_entry( + entry=entry, data=new_data, options=new_options, + ) + return self.async_abort(reason="updated_entry") + + return self.async_abort(reason="already_setup") + + return await self.async_step_user(user_input=import_config) + + async def async_step_zeroconf( + self, discovery_info: Dict[str, Any] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + + discovery_info[ + CONF_HOST + ] = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + + # Check if new config entry matches any existing config entries and abort if so + for entry in self.hass.config_entries.async_entries(DOMAIN): + if _host_is_same(entry.data[CONF_HOST], discovery_info[CONF_HOST]): + return self.async_abort(reason="already_setup") + + # Set default name to discovered device name by stripping zeroconf service + # (`type`) from `name` + num_chars_to_strip = len(discovery_info[CONF_TYPE]) + 1 + discovery_info[CONF_NAME] = discovery_info[CONF_NAME][:-num_chars_to_strip] + + discovery_info[CONF_DEVICE_CLASS] = await async_guess_device_type( + discovery_info[CONF_HOST] + ) + + # Form must be shown after discovery so user can confirm/update configuration before ConfigEntry creation. + self._must_show_form = True + + return await self.async_step_user(user_input=discovery_info) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 828c4e600e0..92fb37c153e 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,13 +1,77 @@ """Constants used by vizio component.""" +from datetime import timedelta + +from pyvizio.const import ( + DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, +) +import voluptuous as vol + +from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, +) +import homeassistant.helpers.config_validation as cv CONF_VOLUME_STEP = "volume_step" +DEFAULT_DEVICE_CLASS = DEVICE_CLASS_TV DEFAULT_NAME = "Vizio SmartCast" +DEFAULT_TIMEOUT = 8 DEFAULT_VOLUME_STEP = 1 -DEFAULT_DEVICE_CLASS = "tv" + DEVICE_ID = "pyvizio" -DEVICE_NAME = "Python Vizio" DOMAIN = "vizio" +ICON = {DEVICE_CLASS_TV: "mdi:television", DEVICE_CLASS_SPEAKER: "mdi:speaker"} -ICON = {"tv": "mdi:television", "soundbar": "mdi:speaker"} +COMMON_SUPPORTED_COMMANDS = ( + SUPPORT_SELECT_SOURCE + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP +) + +SUPPORTED_COMMANDS = { + DEVICE_CLASS_SPEAKER: COMMON_SUPPORTED_COMMANDS, + DEVICE_CLASS_TV: ( + COMMON_SUPPORTED_COMMANDS | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK + ), +} + +# Since Vizio component relies on device class, this dict will ensure that changes to +# the values of DEVICE_CLASS_SPEAKER or DEVICE_CLASS_TV don't require changes to pyvizio. +VIZIO_DEVICE_CLASSES = { + DEVICE_CLASS_SPEAKER: VIZIO_DEVICE_CLASS_SPEAKER, + DEVICE_CLASS_TV: VIZIO_DEVICE_CLASS_TV, +} + +VIZIO_SCHEMA = { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.All( + cv.string, vol.Lower, vol.In([DEVICE_CLASS_TV, DEVICE_CLASS_SPEAKER]) + ), + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All( + vol.Coerce(int), vol.Range(min=1, max=10) + ), +} + +MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 7ae0570fa86..ea1162540cf 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,9 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.0.12"], + "requirements": ["pyvizio==0.1.4"], "dependencies": [], - "codeowners": ["@raman325"] + "codeowners": ["@raman325"], + "config_flow": true, + "zeroconf": ["_viziocast._tcp.local."] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 418cf8e3835..b2f529bce10 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,22 +1,12 @@ """Vizio SmartCast Device support.""" -from datetime import timedelta import logging +from typing import Callable, List -from pyvizio import Vizio -import voluptuous as vol +from pyvizio import VizioAsync from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice -from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, -) +from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS, @@ -25,60 +15,98 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import VIZIO_SCHEMA, validate_auth -from .const import CONF_VOLUME_STEP, DEFAULT_NAME, DEVICE_ID, ICON +from .const import ( + CONF_VOLUME_STEP, + DEFAULT_TIMEOUT, + DEFAULT_VOLUME_STEP, + DEVICE_ID, + DOMAIN, + ICON, + MIN_TIME_BETWEEN_FORCED_SCANS, + MIN_TIME_BETWEEN_SCANS, + SUPPORTED_COMMANDS, + VIZIO_DEVICE_CLASSES, +) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -COMMON_SUPPORTED_COMMANDS = ( - SUPPORT_SELECT_SOURCE - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP -) - -SUPPORTED_COMMANDS = { - "soundbar": COMMON_SUPPORTED_COMMANDS, - "tv": (COMMON_SUPPORTED_COMMANDS | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK), -} +PARALLEL_UPDATES = 0 -PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth) +async def async_setup_entry( + hass: HomeAssistantType, + config_entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up a Vizio media player entry.""" + host = config_entry.data[CONF_HOST] + token = config_entry.data.get(CONF_ACCESS_TOKEN) + name = config_entry.data[CONF_NAME] + device_class = config_entry.data[CONF_DEVICE_CLASS] + # If config entry options not set up, set them up, otherwise assign values managed in options + if not config_entry.options: + volume_step = config_entry.data.get(CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP) + hass.config_entries.async_update_entry( + config_entry, options={CONF_VOLUME_STEP: volume_step} + ) + else: + volume_step = config_entry.options[CONF_VOLUME_STEP] -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Vizio media player platform.""" - host = config[CONF_HOST] - token = config.get(CONF_ACCESS_TOKEN) - name = config[CONF_NAME] - volume_step = config[CONF_VOLUME_STEP] - device_type = config[CONF_DEVICE_CLASS] - device = VizioDevice(host, token, name, volume_step, device_type) - if not device.validate_setup(): + device = VizioAsync( + DEVICE_ID, + host, + name, + auth_token=token, + device_type=VIZIO_DEVICE_CLASSES[device_class], + session=async_get_clientsession(hass, False), + timeout=DEFAULT_TIMEOUT, + ) + + if not await device.can_connect(): fail_auth_msg = "" if token: - fail_auth_msg = " and auth token is correct" - _LOGGER.error( - "Failed to set up Vizio platform, please check if host " - "is valid and available%s", + fail_auth_msg = f"and auth token '{token}' are correct." + else: + fail_auth_msg = "is correct." + _LOGGER.warning( + "Failed to connect to Vizio device, please check if host '%s' " + "is valid and available. Also check if device class '%s' %s", + host, + device_class, fail_auth_msg, ) - return + raise PlatformNotReady - add_entities([device], True) + entity = VizioDevice(config_entry, device, name, volume_step, device_class) + + async_add_entities([entity], update_before_add=True) class VizioDevice(MediaPlayerDevice): """Media Player implementation which performs REST requests to device.""" - def __init__(self, host, token, name, volume_step, device_type): + def __init__( + self, + config_entry: ConfigEntry, + device: VizioAsync, + name: str, + volume_step: int, + device_class: str, + ) -> None: """Initialize Vizio device.""" + self._config_entry = config_entry + self._async_unsub_listeners = [] self._name = name self._state = None @@ -86,141 +114,194 @@ class VizioDevice(MediaPlayerDevice): self._volume_step = volume_step self._current_input = None self._available_inputs = None - self._device_type = device_type - self._supported_commands = SUPPORTED_COMMANDS[device_type] - self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) + self._device_class = device_class + self._supported_commands = SUPPORTED_COMMANDS[device_class] + self._device = device self._max_volume = float(self._device.get_max_volume()) - self._unique_id = None - self._icon = ICON[device_type] + self._icon = ICON[device_class] + self._available = True @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): + async def async_update(self) -> None: """Retrieve latest state of the device.""" - is_on = self._device.get_power_state() + is_on = await self._device.get_power_state(log_api_exception=False) - if not self._unique_id: - self._unique_id = self._device.get_esn() + if is_on is None: + self._available = False + return - if is_on: - self._state = STATE_ON - - volume = self._device.get_current_volume() - if volume is not None: - self._volume_level = float(volume) / self._max_volume - - input_ = self._device.get_current_input() - if input_ is not None: - self._current_input = input_.meta_name - - inputs = self._device.get_inputs() - if inputs is not None: - self._available_inputs = [input_.name for input_ in inputs] - - else: - if is_on is None: - self._state = None - else: - self._state = STATE_OFF + self._available = True + if not is_on: + self._state = STATE_OFF self._volume_level = None self._current_input = None self._available_inputs = None + return + + self._state = STATE_ON + + volume = await self._device.get_current_volume(log_api_exception=False) + if volume is not None: + self._volume_level = float(volume) / self._max_volume + + input_ = await self._device.get_current_input(log_api_exception=False) + if input_ is not None: + self._current_input = input_.meta_name + + inputs = await self._device.get_inputs(log_api_exception=False) + if inputs is not None: + self._available_inputs = [input_.name for input_ in inputs] + + @staticmethod + async def _async_send_update_options_signal( + hass: HomeAssistantType, config_entry: ConfigEntry + ) -> None: + """Send update event when when Vizio config entry is updated.""" + # Move this method to component level if another entity ever gets added for a single config entry. + # See here: https://github.com/home-assistant/home-assistant/pull/30653#discussion_r366426121 + async_dispatcher_send(hass, config_entry.entry_id, config_entry) + + async def _async_update_options(self, config_entry: ConfigEntry) -> None: + """Update options if the update signal comes from this entity.""" + self._volume_step = config_entry.options[CONF_VOLUME_STEP] + + async def async_added_to_hass(self): + """Register callbacks when entity is added.""" + # Register callback for when config entry is updated. + self._async_unsub_listeners.append( + self._config_entry.add_update_listener( + self._async_send_update_options_signal + ) + ) + + # Register callback for update event + self._async_unsub_listeners.append( + async_dispatcher_connect( + self.hass, self._config_entry.entry_id, self._async_update_options + ) + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks when entity is removed.""" + for listener in self._async_unsub_listeners: + listener() + + self._async_unsub_listeners.clear() @property - def state(self): + def available(self) -> bool: + """Return the availabiliity of the device.""" + return self._available + + @property + def state(self) -> str: """Return the state of the device.""" return self._state @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def icon(self): + def icon(self) -> str: """Return the icon of the device.""" return self._icon @property - def volume_level(self): + def volume_level(self) -> float: """Return the volume level of the device.""" return self._volume_level @property - def source(self): + def source(self) -> str: """Return current input of the device.""" return self._current_input @property - def source_list(self): + def source_list(self) -> List: """Return list of available inputs of the device.""" return self._available_inputs @property - def supported_features(self): + def supported_features(self) -> int: """Flag device features that are supported.""" return self._supported_commands @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id of the device.""" - return self._unique_id + return self._config_entry.unique_id - def turn_on(self): + @property + def device_info(self): + """Return device registry information.""" + return { + "identifiers": {(DOMAIN, self._config_entry.unique_id)}, + "name": self.name, + "manufacturer": "VIZIO", + } + + @property + def device_class(self): + """Return device class for entity.""" + return self._device_class + + async def async_turn_on(self) -> None: """Turn the device on.""" - self._device.pow_on() + await self._device.pow_on() - def turn_off(self): + async def async_turn_off(self) -> None: """Turn the device off.""" - self._device.pow_off() + await self._device.pow_off() - def mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: - self._device.mute_on() + await self._device.mute_on() else: - self._device.mute_off() + await self._device.mute_off() - def media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous channel command.""" - self._device.ch_down() + await self._device.ch_down() - def media_next_track(self): + async def async_media_next_track(self) -> None: """Send next channel command.""" - self._device.ch_up() + await self._device.ch_up() - def select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" - self._device.input_switch(source) + await self._device.input_switch(source) - def volume_up(self): + async def async_volume_up(self) -> None: """Increasing volume of the device.""" - self._device.vol_up(num=self._volume_step) + await self._device.vol_up(num=self._volume_step) + if self._volume_level is not None: self._volume_level = min( 1.0, self._volume_level + self._volume_step / self._max_volume ) - def volume_down(self): + async def async_volume_down(self) -> None: """Decreasing volume of the device.""" - self._device.vol_down(num=self._volume_step) + await self._device.vol_down(num=self._volume_step) + if self._volume_level is not None: self._volume_level = max( 0.0, self._volume_level - self._volume_step / self._max_volume ) - def validate_setup(self): - """Validate if host is available and auth token is correct.""" - return self._device.can_connect() - - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level.""" if self._volume_level is not None: if volume > self._volume_level: num = int(self._max_volume * (volume - self._volume_level)) + await self._device.vol_up(num=num) self._volume_level = volume - self._device.vol_up(num=num) + elif volume < self._volume_level: num = int(self._max_volume * (self._volume_level - volume)) + await self._device.vol_down(num=num) self._volume_level = volume - self._device.vol_down(num=num) diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json new file mode 100644 index 00000000000..64b2fb5f936 --- /dev/null +++ b/homeassistant/components/vizio/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Vizio SmartCast", + "step": { + "user": { + "title": "Setup Vizio SmartCast Client", + "data": { + "name": "Name", + "host": ":", + "device_class": "Device Type", + "access_token": "Access Token" + } + } + }, + "error": { + "host_exists": "Vizio device with specified host already configured.", + "name_exists": "Vizio device with specified name already configured.", + "cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit.", + "tv_needs_token": "When Device Type is `tv` then a valid Access Token is needed." + }, + "abort": { + "already_setup": "This entry has already been setup.", + "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", + "updated_entry": "This entry has already been setup but the name and/or options defined in the config do not match the previously imported config so the config entry has been updated accordingly." + } + }, + "options": { + "title": "Update Vizo SmartCast Options", + "step": { + "init": { + "title": "Update Vizo SmartCast Options", + "data": { + "volume_step": "Volume Step Size" + } + } + } + } +} diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index f62a74345b1..369b9c33c0d 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -251,73 +251,75 @@ class Volumio(MediaPlayerDevice): """Flag of media commands that are supported.""" return SUPPORT_VOLUMIO - def async_media_next_track(self): + async def async_media_next_track(self): """Send media_next command to media player.""" - return self.send_volumio_msg("commands", params={"cmd": "next"}) + await self.send_volumio_msg("commands", params={"cmd": "next"}) - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send media_previous command to media player.""" - return self.send_volumio_msg("commands", params={"cmd": "prev"}) + await self.send_volumio_msg("commands", params={"cmd": "prev"}) - def async_media_play(self): + async def async_media_play(self): """Send media_play command to media player.""" - return self.send_volumio_msg("commands", params={"cmd": "play"}) + await self.send_volumio_msg("commands", params={"cmd": "play"}) - def async_media_pause(self): + async def async_media_pause(self): """Send media_pause command to media player.""" if self._state["trackType"] == "webradio": - return self.send_volumio_msg("commands", params={"cmd": "stop"}) - return self.send_volumio_msg("commands", params={"cmd": "pause"}) + await self.send_volumio_msg("commands", params={"cmd": "stop"}) + else: + await self.send_volumio_msg("commands", params={"cmd": "pause"}) - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "volume", "volume": int(volume * 100)} ) - def async_volume_up(self): + async def async_volume_up(self): """Service to send the Volumio the command for volume up.""" - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "volume", "volume": "plus"} ) - def async_volume_down(self): + async def async_volume_down(self): """Service to send the Volumio the command for volume down.""" - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "volume", "volume": "minus"} ) - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command to media player.""" mutecmd = "mute" if mute else "unmute" if mute: # mute is implemented as 0 volume, do save last volume level self._lastvol = self._state["volume"] - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "volume", "volume": mutecmd} ) + return - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "volume", "volume": self._lastvol} ) - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "random", "value": str(shuffle).lower()} ) - def async_select_source(self, source): + async def async_select_source(self, source): """Choose a different available playlist and play it.""" self._currentplaylist = source - return self.send_volumio_msg( + await self.send_volumio_msg( "commands", params={"cmd": "playplaylist", "name": source} ) - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" self._currentplaylist = None - return self.send_volumio_msg("commands", params={"cmd": "clearQueue"}) + await self.send_volumio_msg("commands", params={"cmd": "clearQueue"}) @Throttle(PLAYLIST_UPDATE_INTERVAL) async def _async_update_playlists(self, **kwargs): diff --git a/homeassistant/components/weblink/__init__.py b/homeassistant/components/weblink/__init__.py index be6814da30c..8a770f916bd 100644 --- a/homeassistant/components/weblink/__init__.py +++ b/homeassistant/components/weblink/__init__.py @@ -36,6 +36,12 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the weblink component.""" + _LOGGER.warning( + "The weblink integration has been deprecated and is pending for removal " + "in Home Assistant 0.107.0. Please use this instead: " + "https://www.home-assistant.io/lovelace/entities/#weblink" + ) + links = config.get(DOMAIN) for link in links.get(CONF_ENTITIES): diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index e03fea68fd7..9dec8fe0c71 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -17,11 +17,12 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from .const import ATTR_SOUND_OUTPUT + DOMAIN = "webostv" CONF_SOURCES = "sources" CONF_ON_ACTION = "turn_on_action" -CONF_STANDBY_CONNECTION = "standby_connection" DEFAULT_NAME = "LG webOS Smart TV" WEBOSTV_CONFIG_FILE = "webostv.conf" @@ -31,6 +32,8 @@ ATTR_BUTTON = "button" SERVICE_COMMAND = "command" ATTR_COMMAND = "command" +SERVICE_SELECT_SOUND_OUTPUT = "select_sound_output" + CUSTOMIZE_SCHEMA = vol.Schema( {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} ) @@ -46,9 +49,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional( - CONF_STANDBY_CONNECTION, default=False - ): cv.boolean, vol.Optional(CONF_ICON): cv.string, } ) @@ -64,9 +64,15 @@ BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) +SOUND_OUTPUT_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_SOUND_OUTPUT): cv.string}) + SERVICE_TO_METHOD = { SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, + SERVICE_SELECT_SOUND_OUTPUT: { + "method": "async_select_sound_output", + "schema": SOUND_OUTPUT_SCHEMA, + }, } _LOGGER = logging.getLogger(__name__) @@ -100,9 +106,8 @@ async def async_setup_tv(hass, config, conf): host = conf[CONF_HOST] config_file = hass.config.path(WEBOSTV_CONFIG_FILE) - standby_connection = conf[CONF_STANDBY_CONNECTION] - client = WebOsClient(host, config_file, standby_connection=standby_connection) + client = WebOsClient(host, config_file) hass.data[DOMAIN][host] = {"client": client} if client.is_registered(): diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py new file mode 100644 index 00000000000..a81696f6c0b --- /dev/null +++ b/homeassistant/components/webostv/const.py @@ -0,0 +1,4 @@ +"""Constants used for WebOS TV.""" +LIVE_TV_APP_ID = "com.webos.app.livetv" + +ATTR_SOUND_OUTPUT = "sound_output" diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ff254e35159..e55867432cc 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -2,7 +2,7 @@ "domain": "webostv", "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": ["aiopylgtv==0.2.7"], + "requirements": ["aiopylgtv==0.3.2"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index b7c8a416870..99df9fd17ce 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -36,13 +36,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN +from .const import ATTR_SOUND_OUTPUT, LIVE_TV_APP_ID _LOGGER = logging.getLogger(__name__) -LIVETV_APP_ID = "com.webos.app.livetv" - - SUPPORT_WEBOSTV = ( SUPPORT_TURN_OFF | SUPPORT_NEXT_TRACK @@ -118,17 +116,11 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): self._customize = customize self._on_script = on_script - # Assume that the TV is not muted - self._muted = False - self._volume = 0 + # Assume that the TV is not paused + self._paused = False + self._current_source = None - self._current_source_id = None - self._state = None self._source_list = {} - self._app_list = {} - self._input_list = {} - self._channel = None - self._last_icon = None async def async_added_to_hass(self): """Connect and subscribe to dispatcher signals and state updates.""" @@ -138,10 +130,6 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): self.async_handle_state_update ) - # force state update if needed - if self._state is None: - await self.async_handle_state_update() - async def async_will_remove_from_hass(self): """Call disconnect on removal.""" self._client.unregister_state_update_callback(self.async_handle_state_update) @@ -159,18 +147,6 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_handle_state_update(self): """Update state from WebOsClient.""" - self._current_source_id = self._client.current_appId - self._muted = self._client.muted - self._volume = self._client.volume - self._channel = self._client.current_channel - self._app_list = self._client.apps - self._input_list = self._client.inputs - - if self._current_source_id == "": - self._state = STATE_OFF - else: - self._state = STATE_ON - self.update_sources() self.async_schedule_update_ha_state(False) @@ -180,8 +156,11 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): self._source_list = {} conf_sources = self._customize[CONF_SOURCES] - for app in self._app_list.values(): - if app["id"] == self._current_source_id: + found_live_tv = False + for app in self._client.apps.values(): + if app["id"] == LIVE_TV_APP_ID: + found_live_tv = True + if app["id"] == self._client.current_appId: self._current_source = app["title"] self._source_list[app["title"]] = app elif ( @@ -192,8 +171,10 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): ): self._source_list[app["title"]] = app - for source in self._input_list.values(): - if source["appId"] == self._current_source_id: + for source in self._client.inputs.values(): + if source["appId"] == LIVE_TV_APP_ID: + found_live_tv = True + if source["appId"] == self._client.current_appId: self._current_source = source["label"] self._source_list[source["label"]] = source elif ( @@ -203,6 +184,20 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): ): self._source_list[source["label"]] = source + # special handling of live tv since this might not appear in the app or input lists in some cases + if not found_live_tv: + app = {"id": LIVE_TV_APP_ID, "title": "Live TV"} + if LIVE_TV_APP_ID == self._client.current_appId: + self._current_source = app["title"] + self._source_list["Live TV"] = app + elif ( + not conf_sources + or app["id"] in conf_sources + or any(word in app["title"] for word in conf_sources) + or any(word in app["id"] for word in conf_sources) + ): + self._source_list["Live TV"] = app + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update(self): """Connect.""" @@ -228,17 +223,23 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - return self._state + if self._client.is_on: + return STATE_ON + + return STATE_OFF @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - return self._muted + return self._client.muted @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._volume / 100.0 + if self._client.volume is not None: + return self._client.volume / 100.0 + + return None @property def source(self): @@ -253,30 +254,27 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @property def media_content_type(self): """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL + if self._client.current_appId == LIVE_TV_APP_ID: + return MEDIA_TYPE_CHANNEL + + return None @property def media_title(self): """Title of current playing media.""" - if (self._channel is not None) and ("channelName" in self._channel): - return self._channel["channelName"] + if (self._client.current_appId == LIVE_TV_APP_ID) and ( + self._client.current_channel is not None + ): + return self._client.current_channel.get("channelName") return None @property def media_image_url(self): """Image url of current playing media.""" - if self._current_source_id in self._app_list: - icon = self._app_list[self._current_source_id]["largeIcon"] + if self._client.current_appId in self._client.apps: + icon = self._client.apps[self._client.current_appId]["largeIcon"] if not icon.startswith("http"): - icon = self._app_list[self._current_source_id]["icon"] - - # 'icon' holds a URL with a transient key. Avoid unnecessary - # updates by returning the same URL until the image changes. - if self._last_icon and ( - icon.split("/")[-1] == self._last_icon.split("/")[-1] - ): - return self._last_icon - self._last_icon = icon + icon = self._client.apps[self._client.current_appId]["icon"] return icon return None @@ -287,6 +285,14 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attributes = {} + if self._client.sound_output is not None and self.state != STATE_OFF: + attributes[ATTR_SOUND_OUTPUT] = self._client.sound_output + return attributes + @cmd async def async_turn_off(self): """Turn off media player.""" @@ -294,15 +300,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_turn_on(self): """Turn on the media player.""" - connected = self._client.is_connected() if self._on_script: await self._on_script.async_run() - # if connection was already active - # ensure is still alive - if connected: - await self._client.get_current_app() - @cmd async def async_volume_up(self): """Volume up the media player.""" @@ -316,7 +316,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @cmd async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - tv_volume = volume * 100 + tv_volume = int(round(volume * 100)) await self._client.set_volume(tv_volume) @cmd @@ -324,10 +324,18 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): """Send mute command.""" await self._client.set_mute(mute) + @cmd + async def async_select_sound_output(self, sound_output): + """Select the sound output.""" + await self._client.change_sound_output(sound_output) + @cmd async def async_media_play_pause(self): - """Client pause command acts as a play-pause toggle.""" - await self._client.pause() + """Simulate play pause media player.""" + if self._paused: + await self.async_media_play() + else: + await self.async_media_pause() @cmd async def async_select_source(self, source): @@ -351,7 +359,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): partial_match_channel_id = None perfect_match_channel_id = None - for channel in await self._client.get_channels(): + for channel in self._client.channels: if media_id == channel["channelNumber"]: perfect_match_channel_id = channel["channelId"] continue @@ -379,11 +387,13 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @cmd async def async_media_play(self): """Send play command.""" + self._paused = False await self._client.play() @cmd async def async_media_pause(self): """Send media pause command to media player.""" + self._paused = True await self._client.pause() @cmd @@ -395,7 +405,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_media_next_track(self): """Send next track command.""" current_input = self._client.get_input() - if current_input == LIVETV_APP_ID: + if current_input == LIVE_TV_APP_ID: await self._client.channel_up() else: await self._client.fast_forward() @@ -404,7 +414,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_media_previous_track(self): """Send the previous track command.""" current_input = self._client.get_input() - if current_input == LIVETV_APP_ID: + if current_input == LIVE_TV_APP_ID: await self._client.channel_down() else: await self._client.rewind() diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index e75fafbfe23..ece76b5ed32 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -16,6 +16,9 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" + if discovery_info is None: + return None + host = discovery_info.get(CONF_HOST) icon_path = discovery_info.get(CONF_ICON) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 137a6026eda..1dfb3a6f1d3 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -24,3 +24,12 @@ command: https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py example: 'media.controls/rewind' +select_sound_output: + description: 'Send the TV the command to change sound output.' + fields: + entity_id: + description: Name(s) of the webostv entities to change sound output on. + example: 'media_player.living_room_tv' + sound_output: + description: Name of the sound output to switch to. + example: 'external_speaker' diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9b1c4cd465f..9cac85dee09 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,4 +1,5 @@ """Support for WeMo device discovery.""" +import asyncio import logging import pywemo @@ -6,9 +7,9 @@ import requests import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.discovery import SERVICE_WEMO -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN @@ -26,9 +27,6 @@ WEMO_MODEL_DISPATCH = { "Socket": "switch", } -SUBSCRIPTION_REGISTRY = None -KNOWN_DEVICES = [] - _LOGGER = logging.getLogger(__name__) @@ -70,9 +68,13 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up for WeMo devices.""" - hass.data[DOMAIN] = config + hass.data[DOMAIN] = { + "config": config.get(DOMAIN, {}), + "registry": None, + "pending": {}, + } if DOMAIN in config: hass.async_create_task( @@ -86,106 +88,103 @@ def setup(hass, config): async def async_setup_entry(hass, entry): """Set up a wemo config entry.""" - - config = hass.data[DOMAIN] - - # Keep track of WeMo devices - devices = [] + config = hass.data[DOMAIN].pop("config") # Keep track of WeMo device subscriptions for push updates - global SUBSCRIPTION_REGISTRY - SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() - await hass.async_add_executor_job(SUBSCRIPTION_REGISTRY.start) + registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() + await hass.async_add_executor_job(registry.start) def stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") - SUBSCRIPTION_REGISTRY.stop() + registry.stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) - def setup_url_for_device(device): - """Determine setup.xml url for given device.""" - return f"http://{device.host}:{device.port}/setup.xml" + devices = {} - def setup_url_for_address(host, port): - """Determine setup.xml url for given host and port pair.""" - if not port: - port = pywemo.ouimeaux_device.probe_wemo(host) - - if not port: - return None - - return f"http://{host}:{port}/setup.xml" - - def discovery_dispatch(service, discovery_info): - """Dispatcher for incoming WeMo discovery events.""" - # name, model, location, mac - model_name = discovery_info.get("model_name") - serial = discovery_info.get("serial") - - # Only register a device once - if serial in KNOWN_DEVICES: - _LOGGER.debug("Ignoring known device %s %s", service, discovery_info) - return - - _LOGGER.debug("Discovered unique WeMo device: %s", serial) - KNOWN_DEVICES.append(serial) - - component = WEMO_MODEL_DISPATCH.get(model_name, "switch") - - discovery.load_platform(hass, component, DOMAIN, discovery_info, config) - - discovery.async_listen(hass, SERVICE_WEMO, discovery_dispatch) - - def discover_wemo_devices(now): - """Run discovery for WeMo devices.""" - _LOGGER.debug("Beginning WeMo device discovery...") + static_conf = config.get(CONF_STATIC, []) + if static_conf: _LOGGER.debug("Adding statically configured WeMo devices...") - for host, port in config.get(DOMAIN, {}).get(CONF_STATIC, []): - url = setup_url_for_address(host, port) - - if not url: - _LOGGER.error( - "Unable to get description url for WeMo at: %s", - f"{host}:{port}" if port else host, - ) + for device in await asyncio.gather( + *[ + hass.async_add_executor_job(validate_static_config, host, port) + for host, port in static_conf + ] + ): + if device is None: continue - try: - device = pywemo.discovery.device_from_description(url, None) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) - continue + devices.setdefault(device.serialnumber, device) - if not [d[1] for d in devices if d[1].serialnumber == device.serialnumber]: - devices.append((url, device)) + if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): + _LOGGER.debug("Scanning network for WeMo devices...") + for device in await hass.async_add_executor_job(pywemo.discover_devices): + devices.setdefault( + device.serialnumber, device, + ) - if config.get(DOMAIN, {}).get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - _LOGGER.debug("Scanning network for WeMo devices...") - for device in pywemo.discover_devices(): - if not [ - d[1] for d in devices if d[1].serialnumber == device.serialnumber - ]: - devices.append((setup_url_for_device(device), device)) + loaded_components = set() - for url, device in devices: - _LOGGER.debug("Adding WeMo device at %s:%i", device.host, device.port) + for device in devices.values(): + _LOGGER.debug( + "Adding WeMo device at %s:%i (%s)", + device.host, + device.port, + device.serialnumber, + ) - discovery_info = { - "model_name": device.model_name, - "serial": device.serialnumber, - "mac_address": device.mac, - "ssdp_description": url, - } + component = WEMO_MODEL_DISPATCH.get(device.model_name, "switch") - discovery_dispatch(SERVICE_WEMO, discovery_info) + # Three cases: + # - First time we see component, we need to load it and initialize the backlog + # - Component is being loaded, add to backlog + # - Component is loaded, backlog is gone, dispatch discovery - _LOGGER.debug("WeMo device discovery has finished") + if component not in loaded_components: + hass.data[DOMAIN]["pending"][component] = [device] + loaded_components.add(component) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, discover_wemo_devices) + elif component in hass.data[DOMAIN]["pending"]: + hass.data[DOMAIN]["pending"][component].append(device) + + else: + async_dispatcher_send( + hass, f"{DOMAIN}.{component}", device, + ) return True + + +def validate_static_config(host, port): + """Handle a static config.""" + url = setup_url_for_address(host, port) + + if not url: + _LOGGER.error( + "Unable to get description url for WeMo at: %s", + f"{host}:{port}" if port else host, + ) + return None + + try: + device = pywemo.discovery.device_from_description(url, None) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout,) as err: + _LOGGER.error("Unable to access WeMo at %s (%s)", url, err) + return None + + return device + + +def setup_url_for_address(host, port): + """Determine setup.xml url for given host and port pair.""" + if not port: + port = pywemo.ouimeaux_device.probe_wemo(host) + + if not port: + return None + + return f"http://{host}:{port}/setup.xml" diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 6f7c9e7ee2b..db1ba60364e 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,41 +3,36 @@ import asyncio import logging import async_timeout -from pywemo import discovery -import requests from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN as WEMO_DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Register discovered WeMo binary sensors.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo binary sensors.""" - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + async_add_entities([WemoBinarySensor(device)]) - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.binary_sensor", _discovered_wemo) - if device: - add_entities([WemoBinarySensor(hass, device)]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("binary_sensor") + ] + ) class WemoBinarySensor(BinarySensorDevice): """Representation a WeMo binary sensor.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize the WeMo sensor.""" self.wemo = device self._state = None @@ -67,7 +62,7 @@ class WemoBinarySensor(BinarySensorDevice): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) @@ -126,3 +121,13 @@ class WemoBinarySensor(BinarySensorDevice): def available(self): """Return true if sensor is available.""" return self._available + + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index ac1e202f38d..cec481a2eb4 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -4,8 +4,6 @@ from datetime import timedelta import logging import async_timeout -from pywemo import discovery -import requests import voluptuous as vol from homeassistant.components.fan import ( @@ -17,14 +15,17 @@ from homeassistant.components.fan import ( FanEntity, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import SUBSCRIPTION_REGISTRY -from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY +from .const import ( + DOMAIN as WEMO_DOMAIN, + SERVICE_RESET_FILTER_LIFE, + SERVICE_SET_HUMIDITY, +) SCAN_INTERVAL = timedelta(seconds=10) -DATA_KEY = "fan.wemo" +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -91,36 +92,30 @@ SET_HUMIDITY_SCHEMA = vol.Schema( RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo humidifiers.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo binary sensors.""" + entities = [] - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + entity = WemoHumidifier(device) + entities.append(entity) + async_add_entities([entity]) - if discovery_info is None: - return + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] - - try: - device = WemoHumidifier(discovery.device_from_description(location, mac)) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady - - hass.data[DATA_KEY][device.entity_id] = device - add_entities([device]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("fan") + ] + ) def service_handle(service): """Handle the WeMo humidifier services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) - humidifiers = [ - device - for device in hass.data[DATA_KEY].values() - if device.entity_id in entity_ids - ] + humidifiers = [entity for entity in entities if entity.entity_id in entity_ids] if service.service == SERVICE_SET_HUMIDITY: target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) @@ -132,12 +127,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): humidifier.reset_filter_life() # Register service(s) - hass.services.register( - DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA + hass.services.async_register( + WEMO_DOMAIN, SERVICE_SET_HUMIDITY, service_handle, schema=SET_HUMIDITY_SCHEMA ) - hass.services.register( - DOMAIN, + hass.services.async_register( + WEMO_DOMAIN, SERVICE_RESET_FILTER_LIFE, service_handle, schema=RESET_FILTER_LIFE_SCHEMA, @@ -199,6 +194,16 @@ class WemoHumidifier(FanEntity): """Return true if switch is available.""" return self._available + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + @property def icon(self): """Return the icon of device based on its type.""" @@ -236,7 +241,7 @@ class WemoHumidifier(FanEntity): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 59b6d9e390e..5988019e66f 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -4,8 +4,6 @@ from datetime import timedelta import logging import async_timeout -from pywemo import discovery -import requests from homeassistant import util from homeassistant.components.light import ( @@ -19,10 +17,10 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN as WEMO_DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -34,29 +32,29 @@ SUPPORT_WEMO = ( ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo switches.""" - - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] - - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo lights.""" + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" if device.model_name == "Dimmer": - add_entities([WemoDimmer(device)]) + async_add_entities([WemoDimmer(device)]) else: - setup_bridge(device, add_entities) + await hass.async_add_executor_job( + setup_bridge, hass, device, async_add_entities + ) + + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.light", _discovered_wemo) + + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("light") + ] + ) -def setup_bridge(bridge, add_entities): +def setup_bridge(hass, bridge, async_add_entities): """Set up a WeMo link.""" lights = {} @@ -73,7 +71,7 @@ def setup_bridge(bridge, add_entities): new_lights.append(lights[light_id]) if new_lights: - add_entities(new_lights) + hass.add_job(async_add_entities, new_lights) update_lights() @@ -110,6 +108,16 @@ class WemoLight(Light): """Return the name of the light.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, + "model": type(self.wemo).__name__, + "manufacturer": "Belkin", + } + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -235,7 +243,7 @@ class WemoDimmer(Light): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_executor_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 531ac34ce92..ad8ea45ffd6 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -4,18 +4,16 @@ from datetime import datetime, timedelta import logging import async_timeout -from pywemo import discovery -import requests from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN -from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert -from . import SUBSCRIPTION_REGISTRY -from .const import DOMAIN +from .const import DOMAIN as WEMO_DOMAIN SCAN_INTERVAL = timedelta(seconds=10) +PARALLEL_UPDATES = 0 _LOGGER = logging.getLogger(__name__) @@ -32,24 +30,21 @@ WEMO_OFF = 0 WEMO_STANDBY = 8 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up discovered WeMo switches.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up WeMo switches.""" - if discovery_info is not None: - location = discovery_info["ssdp_description"] - mac = discovery_info["mac_address"] + async def _discovered_wemo(device): + """Handle a discovered Wemo device.""" + async_add_entities([WemoSwitch(device)]) - try: - device = discovery.device_from_description(location, mac) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - ) as err: - _LOGGER.error("Unable to access %s (%s)", location, err) - raise PlatformNotReady + async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.switch", _discovered_wemo) - if device: - add_entities([WemoSwitch(device)]) + await asyncio.gather( + *[ + _discovered_wemo(device) + for device in hass.data[WEMO_DOMAIN]["pending"].pop("switch") + ] + ) class WemoSwitch(SwitchDevice): @@ -97,7 +92,12 @@ class WemoSwitch(SwitchDevice): @property def device_info(self): """Return the device info.""" - return {"name": self._name, "identifiers": {(DOMAIN, self._serialnumber)}} + return { + "name": self.wemo.name, + "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } @property def device_state_attributes(self): @@ -200,7 +200,7 @@ class WemoSwitch(SwitchDevice): # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() - registry = SUBSCRIPTION_REGISTRY + registry = self.hass.data[WEMO_DOMAIN]["registry"] await self.hass.async_add_job(registry.register, self.wemo) registry.on(self.wemo, None, self._subscription_callback) diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 21c31ccdaaf..5794dbbc1a5 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -7,6 +7,9 @@ "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + }, "profile": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/withings/.translations/cs.json b/homeassistant/components/withings/.translations/cs.json index a8aea1fa08f..379ad7fde30 100644 --- a/homeassistant/components/withings/.translations/cs.json +++ b/homeassistant/components/withings/.translations/cs.json @@ -1,6 +1,13 @@ { "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Integrace Withings nen\u00ed nakonfigurov\u00e1na. Postupujte podle n\u00e1vodu." + }, "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index e4599fe8ec2..72d851ad873 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout ved generering af godkendelses-url.", + "missing_configuration": "Withings-integrationen er ikke konfigureret. F\u00f8lg venligst dokumentationen.", "no_flows": "Du skal konfigurere Withings, f\u00f8r du kan godkende med den. L\u00e6s venligst dokumentationen." }, "create_entry": { - "default": "Godkendt med Withings for den valgte profil." + "default": "Godkendt med Withings." }, "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + }, "profile": { "data": { "profile": "Profile" diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index ccd5f3f41fd..a75160fcef8 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -11,14 +11,14 @@ "data": { "profile": "Profil" }, - "description": "Welches Profil haben Sie auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", + "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", "title": "Benutzerprofil" }, "user": { "data": { "profile": "Profil" }, - "description": "W\u00e4hlen Sie ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stellen Sie sicher, dass Sie auf der Withings-Seite denselben Benutzer ausw\u00e4hlen, da sonst die Daten nicht korrekt gekennzeichnet werden.", + "description": "W\u00e4hle ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stelle sicher, dass du auf der Withings-Seite denselben Benutzer ausw\u00e4hlst, da sonst die Daten nicht korrekt gekennzeichnet werden.", "title": "Benutzerprofil." } }, diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 987e3347a99..c39ac530ae6 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Withings integration is not configured. Please follow the documentation.", "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." }, "create_entry": { - "default": "Successfully authenticated with Withings for the selected profile." + "default": "Successfully authenticated with Withings." }, "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + }, "profile": { "data": { "profile": "Profile" diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index c1e969c7f51..c239d7d8db9 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor, lea la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, "step": { + "pick_implementation": { + "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, "profile": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index ed3a43ae295..bd0ec740421 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -7,6 +7,9 @@ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, "step": { + "pick_implementation": { + "title": "Choisissez une m\u00e9thode d'authentification" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/hu.json b/homeassistant/components/withings/.translations/hu.json new file mode 100644 index 00000000000..000e19c2067 --- /dev/null +++ b/homeassistant/components/withings/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lassza ki a hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 4ac73dde195..4a6f5e67965 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione.", "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione." }, "create_entry": { - "default": "Autenticazione completata con Withings per il profilo selezionato." + "default": "Autenticazione riuscita con Withings." }, "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, "profile": { "data": { "profile": "Profilo" diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 4191e03d440..4ff2a80434a 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { - "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + }, "profile": { "data": { "profile": "\ud504\ub85c\ud544" diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index e6ef316548b..4f3fb27e7b2 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune." }, "create_entry": { "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." }, "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index bdde342e7bc..1c4a8c0fb71 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Withings-integreringen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen." }, "create_entry": { - "default": "Vellykket autentisering for Withings og den valgte profilen." + "default": "Vellykket godkjent med Withings." }, "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 90fe281c29f..afe35bd06cf 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Integracja z Withings nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelnienia" + }, "profile": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index 750e306c89a..407bcf48c1a 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Withings \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, "profile": { "data": { "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" diff --git a/homeassistant/components/withings/.translations/sv.json b/homeassistant/components/withings/.translations/sv.json new file mode 100644 index 00000000000..e2493e9afa7 --- /dev/null +++ b/homeassistant/components/withings/.translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 77f3efbd4b9..06870c4020a 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, "profile": { "data": { "profile": "\u500b\u4eba\u8a2d\u5b9a" diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 0bb7be16f8e..ea570569fa6 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -177,7 +177,11 @@ WITHINGS_ATTRIBUTES = [ const.MEAS_SPO2_PCT, MeasureType.SP02, "SP02", const.UOM_PERCENT, None ), WithingsMeasureAttribute( - const.MEAS_HYDRATION, MeasureType.HYDRATION, "Hydration", "", "mdi:water" + const.MEAS_HYDRATION, + MeasureType.HYDRATION, + "Hydration", + const.UOM_PERCENT, + "mdi:water", ), WithingsMeasureAttribute( const.MEAS_PWV, diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 23be2cd385f..9f40c4babd9 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -8,7 +8,15 @@ "data": { "profile": "Profile" } - } + }, + "pick_implementation": { "title": "Pick Authentication Method" } + }, + "abort": { + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Withings integration is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Withings." } } } diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json index 753d7868021..2a7ef92b0ec 100644 --- a/homeassistant/components/wled/.translations/de.json +++ b/homeassistant/components/wled/.translations/de.json @@ -13,11 +13,11 @@ "data": { "host": "Hostname oder IP-Adresse" }, - "description": "Richten Sie Ihre WLED f\u00fcr die Integration mit Home Assistant ein.", - "title": "Verkn\u00fcpfen Sie Ihr WLED" + "description": "Richte deine WLED f\u00fcr die Integration mit Home Assistant ein.", + "title": "Verkn\u00fcpfe dein WLED" }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie die WLED mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du die WLED mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", "title": "Gefundenes WLED-Ger\u00e4t" } }, diff --git a/homeassistant/components/wled/.translations/lb.json b/homeassistant/components/wled/.translations/lb.json index ea23956af42..0e9381bd164 100644 --- a/homeassistant/components/wled/.translations/lb.json +++ b/homeassistant/components/wled/.translations/lb.json @@ -17,7 +17,7 @@ "title": "\u00c4ren WLED verbannen" }, "zeroconf_confirm": { - "description": "W\u00ebllt dir den WLED mam Numm `{name}` am 'Home Assistant dob\u00e4isetzen?", + "description": "W\u00ebllt dir den WLED mam Numm `{name}` am Home Assistant dob\u00e4isetzen?", "title": "Entdeckten WLED Apparat" } }, diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json index a884a20b337..a1893bbce58 100644 --- a/homeassistant/components/wled/.translations/ru.json +++ b/homeassistant/components/wled/.translations/ru.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WLED `{name}`?", - "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 WLED" + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e WLED" } }, "title": "WLED" diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index e6adb460743..1684da28c3f 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -118,10 +118,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class WLEDEntity(Entity): """Defines a base WLED entity.""" - def __init__(self, entry_id: str, wled: WLED, name: str, icon: str) -> None: + def __init__( + self, + entry_id: str, + wled: WLED, + name: str, + icon: str, + enabled_default: bool = True, + ) -> None: """Initialize the WLED entity.""" self._attributes: Dict[str, Union[str, int, float]] = {} self._available = True + self._enabled_default = enabled_default self._entry_id = entry_id self._icon = icon self._name = name @@ -143,6 +151,11 @@ class WLEDEntity(Entity): """Return True if entity is available.""" return self._available + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + @property def should_poll(self) -> bool: """Return the polling requirement of the entity.""" @@ -171,6 +184,9 @@ class WLEDEntity(Entity): async def async_update(self) -> None: """Update WLED entity.""" + if not self.enabled: + return + if self.wled.device is None: self._available = False return diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index f464b27e140..c3fc2d4e6c2 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -50,13 +50,14 @@ class WLEDSensor(WLEDDeviceEntity): icon: str, unit_of_measurement: str, key: str, + enabled_default: bool = True, ) -> None: """Initialize WLED sensor.""" self._state = None self._unit_of_measurement = unit_of_measurement self._key = key - super().__init__(entry_id, wled, name, icon) + super().__init__(entry_id, wled, name, icon, enabled_default) @property def unique_id(self) -> str: @@ -109,6 +110,7 @@ class WLEDUptimeSensor(WLEDSensor): "mdi:clock-outline", None, "uptime", + enabled_default=False, ) @property @@ -134,6 +136,7 @@ class WLEDFreeHeapSensor(WLEDSensor): "mdi:memory", DATA_BYTES, "free_heap", + enabled_default=False, ) async def _wled_update(self) -> None: diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 7be2f12d949..131cc61ed61 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -34,7 +34,7 @@ def x10_command(command): def get_unit_status(code): """Get on/off status for given unit.""" - output = check_output(f"heyu onstate {code}", shell=True) + output = check_output(["heyu", "onstate", code]) return int(output.decode("utf-8")[0]) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index f5e7e476ac5..50de263fb15 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,16 +1,22 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +import logging + from miio import AirQualityMonitor, Device, DeviceException import voluptuous as vol -from homeassistant.components.air_quality import ( - _LOGGER, - PLATFORM_SCHEMA, - AirQualityEntity, -) +from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import NoEntitySpecifiedError, PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + MODEL_AIRQUALITYMONITOR_B1, + MODEL_AIRQUALITYMONITOR_S1, + MODEL_AIRQUALITYMONITOR_V1, +) + +_LOGGER = logging.getLogger(__name__) + DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" ATTR_CO2E = "carbon_dioxide_equivalent" @@ -54,9 +60,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - device = AirMonitorB1(name, AirQualityMonitor(host, token, model=model), unique_id) - async_add_entities([device], update_before_add=True) + device = AirQualityMonitor(host, token, model=model) + + if model == MODEL_AIRQUALITYMONITOR_S1: + entity = AirMonitorS1(name, device, unique_id) + elif model == MODEL_AIRQUALITYMONITOR_B1: + entity = AirMonitorB1(name, device, unique_id) + elif model == MODEL_AIRQUALITYMONITOR_V1: + entity = AirMonitorV1(name, device, unique_id) + else: + raise NoEntitySpecifiedError(f"Not support for entity {unique_id}") + + async_add_entities([entity], update_before_add=True) class AirMonitorB1(AirQualityEntity): @@ -69,22 +85,24 @@ class AirMonitorB1(AirQualityEntity): self._unique_id = unique_id self._icon = "mdi:cloud" self._unit_of_measurement = "μg/m3" + self._available = None + self._air_quality_index = None + self._carbon_dioxide = None self._carbon_dioxide_equivalent = None self._particulate_matter_2_5 = None self._total_volatile_organic_compounds = None async def async_update(self): """Fetch state from the miio device.""" - try: state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) - self._carbon_dioxide_equivalent = state.co2e self._particulate_matter_2_5 = round(state.pm25, 1) self._total_volatile_organic_compounds = round(state.tvoc, 3) - + self._available = True except DeviceException as ex: + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @property @@ -97,11 +115,26 @@ class AirMonitorB1(AirQualityEntity): """Return the icon to use for device if any.""" return self._icon + @property + def available(self): + """Return true when state is known.""" + return self._available + @property def unique_id(self): """Return the unique ID.""" return self._unique_id + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return self._air_quality_index + + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self._carbon_dioxide + @property def carbon_dioxide_equivalent(self): """Return the CO2e (carbon dioxide equivalent) level.""" @@ -133,3 +166,40 @@ class AirMonitorB1(AirQualityEntity): def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + + +class AirMonitorS1(AirMonitorB1): + """Air Quality class for Xiaomi cgllc.airmonitor.s1 device.""" + + async def async_update(self): + """Fetch state from the miio device.""" + try: + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + self._carbon_dioxide = state.co2 + self._particulate_matter_2_5 = state.pm25 + self._total_volatile_organic_compounds = state.tvoc + self._available = True + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class AirMonitorV1(AirMonitorB1): + """Air Quality class for Xiaomi cgllc.airmonitor.s1 device.""" + + async def async_update(self): + """Fetch state from the miio device.""" + try: + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + self._air_quality_index = state.aqi + self._available = True + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return None diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index f8be37b313c..54dd684f6b1 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -46,3 +46,8 @@ SERVICE_MOVE_REMOTE_CONTROL_STEP = "vacuum_remote_control_move_step" SERVICE_START_REMOTE_CONTROL = "vacuum_remote_control_start" SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" SERVICE_CLEAN_ZONE = "vacuum_clean_zone" + +# AirQuality Model +MODEL_AIRQUALITYMONITOR_V1 = "zhimi.airmonitor.v1" +MODEL_AIRQUALITYMONITOR_B1 = "cgllc.airmonitor.b1" +MODEL_AIRQUALITYMONITOR_S1 = "cgllc.airmonitor.s1" diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 1e7cada1a7b..9e4446f2964 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -16,7 +16,6 @@ from homeassistant.components.remote import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_HIDDEN, CONF_COMMAND, CONF_HOST, CONF_NAME, @@ -61,7 +60,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_SLOT, default=DEFAULT_SLOT): vol.All( int, vol.Range(min=1, max=1000000) ), - vol.Optional(ATTR_HIDDEN, default=True): cv.boolean, vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)), vol.Optional(CONF_COMMANDS, default={}): cv.schema_with_slug_keys( COMMAND_SCHEMA @@ -106,16 +104,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= slot = config.get(CONF_SLOT) timeout = config.get(CONF_TIMEOUT) - hidden = config.get(ATTR_HIDDEN) - xiaomi_miio_remote = XiaomiMiioRemote( - friendly_name, - device, - unique_id, - slot, - timeout, - hidden, - config.get(CONF_COMMANDS), + friendly_name, device, unique_id, slot, timeout, config.get(CONF_COMMANDS), ) hass.data[DATA_KEY][host] = xiaomi_miio_remote @@ -178,14 +168,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class XiaomiMiioRemote(RemoteDevice): """Representation of a Xiaomi Miio Remote device.""" - def __init__( - self, friendly_name, device, unique_id, slot, timeout, hidden, commands - ): + def __init__(self, friendly_name, device, unique_id, slot, timeout, commands): """Initialize the remote.""" self._name = friendly_name self._device = device self._unique_id = unique_id - self._is_hidden = hidden self._slot = slot self._timeout = timeout self._state = False @@ -206,11 +193,6 @@ class XiaomiMiioRemote(RemoteDevice): """Return the remote object.""" return self._device - @property - def hidden(self): - """Return if we should hide entity.""" - return self._is_hidden - @property def slot(self): """Return the slot to save learned command.""" @@ -235,13 +217,6 @@ class XiaomiMiioRemote(RemoteDevice): """We should not be polled for device up state.""" return False - @property - def device_state_attributes(self): - """Hide remote by default.""" - if self._is_hidden: - return {"hidden": "true"} - return - async def async_turn_on(self, **kwargs): """Turn the device on.""" _LOGGER.error( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 4ef34e8ff56..a32a28993ca 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -60,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.ALLOW_EXTRA, ) -FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90, "Gentle": 105} +FAN_SPEEDS = {"Silent": 38, "Standard": 60, "Medium": 77, "Turbo": 90, "Gentle": 105} ATTR_CLEAN_START = "clean_start" ATTR_CLEAN_STOP = "clean_stop" diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index b947f6b448c..eae1cd32c06 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -237,21 +237,30 @@ class YeelightDevice: """Return configured device model.""" return self._model - @property - def is_nightlight_enabled(self) -> bool: - """Return true / false if nightlight is currently enabled.""" - if self.bulb is None: - return False - - return self._active_mode == ACTIVE_MODE_NIGHTLIGHT - @property def is_nightlight_supported(self) -> bool: """Return true / false if nightlight is supported.""" if self.model: return self.bulb.get_model_specs().get("night_light", False) - return self._active_mode is not None + # It should support both ceiling and other lights + return self._nightlight_brightness is not None + + @property + def is_nightlight_enabled(self) -> bool: + """Return true / false if nightlight is currently enabled.""" + if self.bulb is None: + return False + + # Only ceiling lights have active_mode, from SDK docs: + # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only) + if self._active_mode is not None: + return self._active_mode == ACTIVE_MODE_NIGHTLIGHT + + if self._nightlight_brightness is not None: + return int(self._nightlight_brightness) > 0 + + return False @property def is_color_flow_enabled(self) -> bool: @@ -266,6 +275,10 @@ class YeelightDevice: def _color_flow(self): return self.bulb.last_properties.get("flowing") + @property + def _nightlight_brightness(self): + return self.bulb.last_properties.get("nl_br") + @property def type(self): """Return bulb type.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 61de12eafbf..2605823a99d 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -134,6 +134,7 @@ MODEL_TO_DEVICE_TYPE = { "color2": BulbType.Color, "strip1": BulbType.Color, "bslamp1": BulbType.Color, + "bslamp2": BulbType.Color, "RGBW": BulbType.Color, "lamp1": BulbType.WhiteTemp, "ceiling1": BulbType.WhiteTemp, @@ -281,7 +282,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device_type == BulbType.White: _lights_setup_helper(YeelightGenericLight) elif device_type == BulbType.Color: - _lights_setup_helper(YeelightColorLight) + if nl_switch_light and device.is_nightlight_supported: + _lights_setup_helper(YeelightColorLightWithNightlightSwitch) + _lights_setup_helper(YeelightNightLightModeWithWithoutBrightnessControl) + else: + _lights_setup_helper(YeelightColorLightWithoutNightlightSwitch) elif device_type == BulbType.WhiteTemp: if nl_switch_light and device.is_nightlight_supported: _lights_setup_helper(YeelightWithNightLight) @@ -290,7 +295,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch) elif device_type == BulbType.WhiteTempMood: if nl_switch_light and device.is_nightlight_supported: - _lights_setup_helper(YeelightNightLightMode) + _lights_setup_helper(YeelightNightLightModeWithAmbientSupport) _lights_setup_helper(YeelightWithAmbientAndNightlight) else: _lights_setup_helper(YeelightWithAmbientWithoutNightlight) @@ -808,8 +813,8 @@ class YeelightGenericLight(Light): _LOGGER.error("Unable to set scene: %s", ex) -class YeelightColorLight(YeelightGenericLight): - """Representation of a Color Yeelight light.""" +class YeelightColorLightSupport: + """Representation of a Color Yeelight light support.""" @property def supported_features(self) -> int: @@ -821,7 +826,7 @@ class YeelightColorLight(YeelightGenericLight): return YEELIGHT_COLOR_EFFECT_LIST -class YeelightWhiteTempLightsupport: +class YeelightWhiteTempLightSupport: """Representation of a Color Yeelight light.""" @property @@ -834,18 +839,28 @@ class YeelightWhiteTempLightsupport: return YEELIGHT_TEMP_ONLY_EFFECT_LIST -class YeelightWhiteTempWithoutNightlightSwitch( - YeelightWhiteTempLightsupport, YeelightGenericLight +class YeelightNightLightSupport: + """Representation of a Yeelight nightlight support.""" + + @property + def _turn_on_power_mode(self): + return PowerMode.NORMAL + + +class YeelightColorLightWithoutNightlightSwitch( + YeelightColorLightSupport, YeelightGenericLight ): - """White temp light, when nightlight switch is not set to light.""" + """Representation of a Color Yeelight light.""" @property def _brightness_property(self): return "current_brightness" -class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight): - """Representation of a Yeelight with nightlight support. +class YeelightColorLightWithNightlightSwitch( + YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight +): + """Representation of a Yeelight with rgb support and nightlight. It represents case when nightlight switch is set to light. """ @@ -855,9 +870,29 @@ class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight """Return true if device is on.""" return super().is_on and not self.device.is_nightlight_enabled + +class YeelightWhiteTempWithoutNightlightSwitch( + YeelightWhiteTempLightSupport, YeelightGenericLight +): + """White temp light, when nightlight switch is not set to light.""" + @property - def _turn_on_power_mode(self): - return PowerMode.NORMAL + def _brightness_property(self): + return "current_brightness" + + +class YeelightWithNightLight( + YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight +): + """Representation of a Yeelight with temp only support and nightlight. + + It represents case when nightlight switch is set to light. + """ + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return super().is_on and not self.device.is_nightlight_enabled class YeelightNightLightMode(YeelightGenericLight): @@ -891,6 +926,26 @@ class YeelightNightLightMode(YeelightGenericLight): return YEELIGHT_TEMP_ONLY_EFFECT_LIST +class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): + """Representation of a Yeelight, with ambient support, when in nightlight mode.""" + + @property + def _power_property(self): + return "main_power" + + +class YeelightNightLightModeWithWithoutBrightnessControl(YeelightNightLightMode): + """Representation of a Yeelight, when in nightlight mode. + + It represents case when nightlight mode brightness control is not supported. + """ + + @property + def supported_features(self): + """Flag no supported features.""" + return 0 + + class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch): """Representation of a Yeelight which has ambilight support. @@ -913,7 +968,7 @@ class YeelightWithAmbientAndNightlight(YeelightWithNightLight): return "main_power" -class YeelightAmbientLight(YeelightColorLight): +class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): """Representation of a Yeelight ambient light.""" PROPERTIES_MAPPING = {"color_mode": "bg_lmode"} @@ -931,6 +986,10 @@ class YeelightAmbientLight(YeelightColorLight): """Return the name of the device if any.""" return f"{self.device.name} ambilight" + @property + def _brightness_property(self): + return "bright" + def _get_property(self, prop, default=None): bg_prop = self.PROPERTIES_MAPPING.get(prop) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index c8417748fd9..6e49d287186 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_BRAND = "YI Home Camera" DEFAULT_PASSWORD = "" -DEFAULT_PATH = "/tmp/sd/record" +DEFAULT_PATH = "/tmp/sd/record" # nosec DEFAULT_PORT = 21 DEFAULT_USERNAME = "root" DEFAULT_ARGUMENTS = "-pred 1" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index d6be4cdf6a0..b4dbbda51f1 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -148,15 +148,20 @@ def handle_homekit(hass, info) -> bool: def info_from_service(service): """Return prepared info from mDNS entries.""" - properties = {} + properties = {"_raw": {}} for key, value in service.properties.items(): + # See https://ietf.org/rfc/rfc6763.html#section-6.4 and + # https://ietf.org/rfc/rfc6763.html#section-6.5 for expected encodings + # for property keys and values + key = key.decode("ascii") + properties["_raw"][key] = value + try: if isinstance(value, bytes): - value = value.decode("utf-8") - properties[key.decode("utf-8")] = value + properties[key] = value.decode("utf-8") except UnicodeDecodeError: - _LOGGER.warning("Unicode decode error on %s: %s", key, value) + pass address = service.addresses[0] diff --git a/homeassistant/components/zha/.translations/cs.json b/homeassistant/components/zha/.translations/cs.json new file mode 100644 index 00000000000..0951ca3377e --- /dev/null +++ b/homeassistant/components/zha/.translations/cs.json @@ -0,0 +1,14 @@ +{ + "device_automation": { + "trigger_subtype": { + "button_2": "Druh\u00e9 tla\u010d\u00edtko", + "button_3": "T\u0159et\u00ed tla\u010d\u00edtko", + "button_4": "\u010ctvrt\u00e9 tla\u010d\u00edtko", + "button_5": "P\u00e1t\u00e9 tla\u010d\u00edtko", + "button_6": "\u0160est\u00e9 tla\u010d\u00edtko", + "close": "Zav\u0159\u00edt", + "dim_down": "ztmavit", + "dim_up": "ro\u017ehnout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 7c732b6906e..ea5586ef96f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -12,7 +12,6 @@ import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( @@ -53,11 +52,7 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import ( - async_get_device_info, - async_is_bindable_target, - get_matched_clusters, -) +from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -212,13 +207,9 @@ async def websocket_permit_devices(hass, connection, msg): async def websocket_get_devices(hass, connection, msg): """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) - devices = [] - for device in zha_gateway.devices.values(): - devices.append( - async_get_device_info(hass, device, ha_device_registry=ha_device_registry) - ) + devices = [device.async_get_info() for device in zha_gateway.devices.values()] + connection.send_result(msg[ID], devices) @@ -228,16 +219,13 @@ async def websocket_get_devices(hass, connection, msg): async def websocket_get_groupable_devices(hass, connection, msg): """Get ZHA devices that can be grouped.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) - devices = [] - for device in zha_gateway.devices.values(): - if device.is_groupable: - devices.append( - async_get_device_info( - hass, device, ha_device_registry=ha_device_registry - ) - ) + devices = [ + device.async_get_info() + for device in zha_gateway.devices.values() + if device.is_groupable or device.is_coordinator + ] + connection.send_result(msg[ID], devices) @@ -246,7 +234,8 @@ async def websocket_get_groupable_devices(hass, connection, msg): @websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) async def websocket_get_groups(hass, connection, msg): """Get ZHA groups.""" - groups = await get_groups(hass) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + groups = [group.async_get_info() for group in zha_gateway.groups.values()] connection.send_result(msg[ID], groups) @@ -258,13 +247,10 @@ async def websocket_get_groups(hass, connection, msg): async def websocket_get_device(hass, connection, msg): """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) ieee = msg[ATTR_IEEE] device = None if ieee in zha_gateway.devices: - device = async_get_device_info( - hass, zha_gateway.devices[ieee], ha_device_registry=ha_device_registry - ) + device = zha_gateway.devices[ieee].async_get_info() if not device: connection.send_message( websocket_api.error_message( @@ -283,17 +269,11 @@ async def websocket_get_device(hass, connection, msg): async def websocket_get_group(hass, connection, msg): """Get ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) group_id = msg[GROUP_ID] group = None - if group_id in zha_gateway.application_controller.groups: - group = async_get_group_info( - hass, - zha_gateway, - zha_gateway.application_controller.groups[group_id], - ha_device_registry, - ) + if group_id in zha_gateway.groups: + group = zha_gateway.groups.get(group_id).async_get_info() if not group: connection.send_message( websocket_api.error_message( @@ -316,25 +296,10 @@ async def websocket_get_group(hass, connection, msg): async def websocket_add_group(hass, connection, msg): """Add a new ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) - group_id = len(zha_gateway.application_controller.groups) + 1 group_name = msg[GROUP_NAME] - zigpy_group = async_get_group_by_name(zha_gateway, group_name) - ret_group = None members = msg.get(ATTR_MEMBERS) - - # guard against group already existing - if zigpy_group is None: - zigpy_group = zha_gateway.application_controller.groups.add_group( - group_id, group_name - ) - if members is not None: - tasks = [] - for ieee in members: - tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) - await asyncio.gather(*tasks) - ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) - connection.send_result(msg[ID], ret_group) + group = await zha_gateway.async_create_zigpy_group(group_name, members) + connection.send_result(msg[ID], group.async_get_info()) @websocket_api.require_admin @@ -348,17 +313,16 @@ async def websocket_add_group(hass, connection, msg): async def websocket_remove_groups(hass, connection, msg): """Remove the specified ZHA groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - groups = zha_gateway.application_controller.groups group_ids = msg[GROUP_IDS] if len(group_ids) > 1: tasks = [] for group_id in group_ids: - tasks.append(remove_group(groups[group_id], zha_gateway)) + tasks.append(zha_gateway.async_remove_zigpy_group(group_id)) await asyncio.gather(*tasks) else: - await remove_group(groups[group_ids[0]], zha_gateway) - ret_groups = await get_groups(hass) + await zha_gateway.async_remove_zigpy_group(group_ids[0]) + ret_groups = [group.async_get_info() for group in zha_gateway.groups.values()] connection.send_result(msg[ID], ret_groups) @@ -374,25 +338,21 @@ async def websocket_remove_groups(hass, connection, msg): async def websocket_add_group_members(hass, connection, msg): """Add members to a ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) group_id = msg[GROUP_ID] members = msg[ATTR_MEMBERS] - zigpy_group = None + zha_group = None - if group_id in zha_gateway.application_controller.groups: - zigpy_group = zha_gateway.application_controller.groups[group_id] - tasks = [] - for ieee in members: - tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) - await asyncio.gather(*tasks) - if not zigpy_group: + if group_id in zha_gateway.groups: + zha_group = zha_gateway.groups.get(group_id) + await zha_group.async_add_members(members) + if not zha_group: connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return - ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + ret_group = zha_group.async_get_info() connection.send_result(msg[ID], ret_group) @@ -408,88 +368,24 @@ async def websocket_add_group_members(hass, connection, msg): async def websocket_remove_group_members(hass, connection, msg): """Remove members from a ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) group_id = msg[GROUP_ID] members = msg[ATTR_MEMBERS] - zigpy_group = None + zha_group = None - if group_id in zha_gateway.application_controller.groups: - zigpy_group = zha_gateway.application_controller.groups[group_id] - tasks = [] - for ieee in members: - tasks.append(zha_gateway.devices[ieee].async_remove_from_group(group_id)) - await asyncio.gather(*tasks) - if not zigpy_group: + if group_id in zha_gateway.groups: + zha_group = zha_gateway.groups.get(group_id) + await zha_group.async_remove_members(members) + if not zha_group: connection.send_message( websocket_api.error_message( msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" ) ) return - ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + ret_group = zha_group.async_get_info() connection.send_result(msg[ID], ret_group) -async def get_groups(hass,): - """Get ZHA Groups.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ha_device_registry = await async_get_registry(hass) - - groups = [] - for group in zha_gateway.application_controller.groups.values(): - groups.append( - async_get_group_info(hass, zha_gateway, group, ha_device_registry) - ) - return groups - - -async def remove_group(group, zha_gateway): - """Remove ZHA Group.""" - if group.members: - tasks = [] - for member_ieee in group.members.keys(): - if member_ieee[0] in zha_gateway.devices: - tasks.append( - zha_gateway.devices[member_ieee[0]].async_remove_from_group( - group.group_id - ) - ) - if tasks: - await asyncio.gather(*tasks) - else: - # we have members but none are tracked by ZHA for whatever reason - zha_gateway.application_controller.groups.pop(group.group_id) - else: - zha_gateway.application_controller.groups.pop(group.group_id) - - -@callback -def async_get_group_info(hass, zha_gateway, group, ha_device_registry): - """Get ZHA group.""" - ret_group = {} - ret_group["group_id"] = group.group_id - ret_group["name"] = group.name - ret_group["members"] = [ - async_get_device_info( - hass, - zha_gateway.get_device(member_ieee[0]), - ha_device_registry=ha_device_registry, - ) - for member_ieee in group.members.keys() - if member_ieee[0] in zha_gateway.devices - ] - return ret_group - - -@callback -def async_get_group_by_name(zha_gateway, group_name): - """Get ZHA group by name.""" - for group in zha_gateway.application_controller.groups.values(): - if group.name == group_name: - return group - return None - - @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -709,9 +605,9 @@ async def websocket_get_bindable_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_IEEE] source_device = zha_gateway.get_device(source_ieee) - ha_device_registry = await async_get_registry(hass) + devices = [ - async_get_device_info(hass, device, ha_device_registry=ha_device_registry) + device.async_get_info() for device in zha_gateway.devices.values() if async_is_bindable_target(source_device, device) ] @@ -884,6 +780,7 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati zdo.debug(fmt, *(log_msg[2] + (outcome,))) +@callback def async_load_api(hass): """Set up the web socket API.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] @@ -905,7 +802,12 @@ def async_load_api(hass): async def remove(service): """Remove a node from the network.""" - ieee = service.data.get(ATTR_IEEE_ADDRESS) + ieee = service.data[ATTR_IEEE_ADDRESS] + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None and zha_device.is_coordinator: + _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) + return _LOGGER.info("Removing node %s", ieee) await application_controller.remove(ieee) @@ -1157,6 +1059,7 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_unbind_devices) +@callback def async_unload_api(hass): """Unload the ZHA API.""" hass.services.async_remove(DOMAIN, SERVICE_PERMIT) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index d8bc1187be8..58b671a340f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -46,11 +46,6 @@ CLASS_MAPPING = { STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation binary sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation binary sensor from config entry.""" @@ -130,6 +125,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): """Return device class from component DEVICE_CLASSES.""" return self._device_class + @callback def async_set_state(self, state): """Set the state.""" self._state = bool(state) diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 16592c9a8df..03b1a8450db 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -62,4 +62,35 @@ class Shade(ZigbeeChannel): class WindowCovering(ZigbeeChannel): """Window channel.""" - pass + _value_attribute = 8 + REPORT_CONFIG = ( + {"attr": "current_position_lift_percentage", "config": REPORT_CONFIG_IMMEDIATE}, + ) + + async def async_update(self): + """Retrieve latest state.""" + result = await self.get_attribute_value( + "current_position_lift_percentage", from_cache=False + ) + self.debug("read current position: %s", result) + + async_dispatcher_send( + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from window_covering cluster.""" + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + self.debug( + "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value + ) + if attrid == self._value_attribute: + async_dispatcher_send( + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value(self._value_attribute, from_cache=from_cache) + await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 7afde3e5f78..c1701479a43 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -22,6 +22,7 @@ from ..const import ( SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, + SIGNAL_STATE_ATTR, ) from ..helpers import get_attr_id_by_name @@ -355,6 +356,14 @@ class PowerConfigurationChannel(ZigbeeChannel): async_dispatcher_send( self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) + return + attr_name = self.cluster.attributes.get(attrid, [attrid])[0] + async_dispatcher_send( + self._zha_device.hass, + f"{self.unique_id}_{SIGNAL_STATE_ATTR}", + attr_name, + value, + ) async def async_initialize(self, from_cache): """Initialize channel.""" diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 848f5805ad5..b8782101cd4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -3,6 +3,7 @@ import enum import logging from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT @@ -17,6 +18,7 @@ ATTR_CLUSTER_ID = "cluster_id" ATTR_CLUSTER_TYPE = "cluster_type" ATTR_COMMAND = "command" ATTR_COMMAND_TYPE = "command_type" +ATTR_DEVICE_TYPE = "device_type" ATTR_ENDPOINT_ID = "endpoint_id" ATTR_IEEE = "ieee" ATTR_LAST_SEEN = "last_seen" @@ -48,6 +50,7 @@ CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" +CHANNEL_COVER = "window_covering" CHANNEL_DOORLOCK = "door_lock" CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" @@ -72,7 +75,7 @@ CLUSTER_COMMANDS_SERVER = "server_commands" CLUSTER_TYPE_IN = "in" CLUSTER_TYPE_OUT = "out" -COMPONENTS = (BINARY_SENSOR, DEVICE_TRACKER, FAN, LIGHT, LOCK, SENSOR, SWITCH) +COMPONENTS = (BINARY_SENSOR, COVER, DEVICE_TRACKER, FAN, LIGHT, LOCK, SENSOR, SWITCH) CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" @@ -222,13 +225,18 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED = 0 WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" -ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_MSG = "zha_gateway_message" -ZHA_GW_MSG_DEVICE_REMOVED = "device_removed" -ZHA_GW_MSG_DEVICE_INFO = "device_info" ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" +ZHA_GW_MSG_DEVICE_INFO = "device_info" ZHA_GW_MSG_DEVICE_JOINED = "device_joined" -ZHA_GW_MSG_LOG_OUTPUT = "log_output" +ZHA_GW_MSG_DEVICE_REMOVED = "device_removed" +ZHA_GW_MSG_GROUP_ADDED = "group_added" +ZHA_GW_MSG_GROUP_INFO = "group_info" +ZHA_GW_MSG_GROUP_MEMBER_ADDED = "group_member_added" +ZHA_GW_MSG_GROUP_MEMBER_REMOVED = "group_member_removed" +ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" +ZHA_GW_MSG_LOG_OUTPUT = "log_output" +ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_RADIO = "radio" ZHA_GW_RADIO_DESCRIPTION = "radio_description" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 634a06f7f58..8810fd77fe7 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -32,6 +32,7 @@ from .const import ( ATTR_CLUSTER_ID, ATTR_COMMAND, ATTR_COMMAND_TYPE, + ATTR_DEVICE_TYPE, ATTR_ENDPOINT_ID, ATTR_IEEE, ATTR_LAST_SEEN, @@ -57,6 +58,7 @@ from .const import ( POWER_BATTERY_OR_UNKNOWN, POWER_MAINS_POWERED, SIGNAL_AVAILABLE, + UNKNOWN, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ) @@ -102,8 +104,18 @@ class ZHADevice(LogMixin): self._available_check = async_track_time_interval( self.hass, self._check_available, _UPDATE_ALIVE_INTERVAL ) + self._ha_device_id = None self.status = DeviceStatus.CREATED + @property + def device_id(self): + """Return the HA device registry device id.""" + return self._ha_device_id + + def set_device_id(self, device_id): + """Set the HA device registry device id.""" + self._ha_device_id = device_id + @property def name(self): """Return device name.""" @@ -160,6 +172,14 @@ class ZHADevice(LogMixin): """Return true if device is mains powered.""" return self._zigpy_device.node_desc.is_mains_powered + @property + def device_type(self): + """Return the logical device type for the device.""" + node_descriptor = self._zigpy_device.node_desc + return ( + node_descriptor.logical_type.name if node_descriptor.is_valid else UNKNOWN + ) + @property def power_source(self): """Return the power source for the device.""" @@ -281,6 +301,7 @@ class ZHADevice(LogMixin): ATTR_RSSI: self.rssi, ATTR_LAST_SEEN: update_time, ATTR_AVAILABLE: self.available, + ATTR_DEVICE_TYPE: self.device_type, } def add_cluster_channel(self, cluster_channel): @@ -395,6 +416,25 @@ class ZHADevice(LogMixin): """Set last seen on the zigpy device.""" self._zigpy_device.last_seen = last_seen + @callback + def async_get_info(self): + """Get ZHA device information.""" + device_info = {} + device_info.update(self.device_info) + device_info["entities"] = [ + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } + for entity_ref in self.gateway.device_registry[self.ieee] + ] + reg_device = self.gateway.ha_device_registry.async_get(self.device_id) + if reg_device is not None: + device_info["user_given_name"] = reg_device.name_by_user + device_info["device_reg_id"] = reg_device.id + device_info["area_id"] = reg_device.area_id + return device_info + @callback def async_get_clusters(self): """Get all clusters for this device.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 72931c665ee..e5a199c5bbd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -12,6 +12,8 @@ import logging import os import traceback +import zigpy.device as zigpy_dev + from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.core import callback from homeassistant.helpers.device_registry import ( @@ -19,6 +21,7 @@ from homeassistant.helpers.device_registry import ( async_get_registry as get_dev_reg, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from .const import ( ATTR_IEEE, @@ -56,6 +59,11 @@ from .const import ( ZHA_GW_MSG_DEVICE_INFO, ZHA_GW_MSG_DEVICE_JOINED, ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_GROUP_ADDED, + ZHA_GW_MSG_GROUP_INFO, + ZHA_GW_MSG_GROUP_MEMBER_ADDED, + ZHA_GW_MSG_GROUP_MEMBER_REMOVED, + ZHA_GW_MSG_GROUP_REMOVED, ZHA_GW_MSG_LOG_ENTRY, ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_RAW_INIT, @@ -64,7 +72,7 @@ from .const import ( ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint -from .helpers import async_get_device_info +from .group import ZHAGroup from .patches import apply_application_controller_patch from .registries import RADIO_TYPES from .store import async_get_registry @@ -85,9 +93,11 @@ class ZHAGateway: self._hass = hass self._config = config self._devices = {} + self._groups = {} self._device_registry = collections.defaultdict(list) self.zha_storage = None self.ha_device_registry = None + self.ha_entity_registry = None self.application_controller = None self.radio_description = None hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self @@ -103,6 +113,7 @@ class ZHAGateway: """Initialize controller and connect radio.""" self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = await get_dev_reg(self._hass) + self.ha_entity_registry = await get_ent_reg(self._hass) usb_path = self._config_entry.data.get(CONF_USB_PATH) baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) @@ -121,6 +132,7 @@ class ZHAGateway: self.application_controller = radio_details[CONTROLLER](radio, database) apply_application_controller_patch(self) self.application_controller.add_listener(self) + self.application_controller.groups.add_listener(self) await self.application_controller.startup(auto_form=True) self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self.application_controller.ieee @@ -135,13 +147,13 @@ class ZHAGateway: await coro for device in self.application_controller.devices.values(): - if device.nwk == 0x0000: - continue init_tasks.append( init_with_semaphore(self.async_device_restored(device), semaphore) ) await asyncio.gather(*init_tasks) + self._initialize_groups() + def device_joined(self, device): """Handle device joined. @@ -160,9 +172,6 @@ class ZHAGateway: def raw_device_initialized(self, device): """Handle a device initialization without quirks loaded.""" - if device.nwk == 0x0000: - return - manuf = device.manufacturer async_dispatcher_send( self._hass, @@ -181,9 +190,49 @@ class ZHAGateway: """Handle device joined and basic information discovered.""" self._hass.async_create_task(self.async_device_initialized(device)) - def device_left(self, device): + def device_left(self, device: zigpy_dev.Device): """Handle device leaving the network.""" - pass + self.async_update_device(device, False) + + def group_member_removed(self, zigpy_group, endpoint): + """Handle zigpy group member removed event.""" + # need to handle endpoint correctly on groups + zha_group = self._async_get_or_create_group(zigpy_group) + zha_group.info("group_member_removed - endpoint: %s", endpoint) + self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) + + def group_member_added(self, zigpy_group, endpoint): + """Handle zigpy group member added event.""" + # need to handle endpoint correctly on groups + zha_group = self._async_get_or_create_group(zigpy_group) + zha_group.info("group_member_added - endpoint: %s", endpoint) + self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) + + def group_added(self, zigpy_group): + """Handle zigpy group added event.""" + zha_group = self._async_get_or_create_group(zigpy_group) + zha_group.info("group_added") + # need to dispatch for entity creation here + self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_ADDED) + + def group_removed(self, zigpy_group): + """Handle zigpy group added event.""" + self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) + zha_group = self._groups.pop(zigpy_group.group_id, None) + zha_group.info("group_removed") + + def _send_group_gateway_message(self, zigpy_group, gateway_message_type): + """Send the gareway event for a zigpy group event.""" + zha_group = self._groups.get(zigpy_group.group_id, None) + if zha_group is not None: + async_dispatcher_send( + self._hass, + ZHA_GW_MSG, + { + ATTR_TYPE: gateway_message_type, + ZHA_GW_MSG_GROUP_INFO: zha_group.async_get_info(), + }, + ) async def _async_remove_device(self, device, entity_refs): if entity_refs is not None: @@ -191,9 +240,7 @@ class ZHAGateway: for entity_ref in entity_refs: remove_tasks.append(entity_ref.remove_future) await asyncio.wait(remove_tasks) - reg_device = self.ha_device_registry.async_get_device( - {(DOMAIN, str(device.ieee))}, set() - ) + reg_device = self.ha_device_registry.async_get(device.device_id) if reg_device is not None: self.ha_device_registry.async_remove_device(reg_device.id) @@ -202,7 +249,7 @@ class ZHAGateway: zha_device = self._devices.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None) if zha_device is not None: - device_info = async_get_device_info(self._hass, zha_device) + device_info = zha_device.async_get_info() zha_device.async_unsub_dispatcher() async_dispatcher_send( self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee)) @@ -224,7 +271,15 @@ class ZHAGateway: def get_group(self, group_id): """Return Group for given group id.""" - return self.application_controller.groups[group_id] + return self.groups.get(group_id) + + @callback + def async_get_group_by_name(self, group_name): + """Get ZHA group by name.""" + for group in self.groups.values(): + if group.name == group_name: + return group + return None def get_entity_reference(self, entity_id): """Return entity reference for given entity_id if found.""" @@ -247,6 +302,11 @@ class ZHAGateway: """Return devices.""" return self._devices + @property + def groups(self): + """Return groups.""" + return self._groups + @property def device_registry(self): """Return entities by ieee.""" @@ -293,6 +353,12 @@ class ZHAGateway: logging.getLogger(logger_name).removeHandler(self._log_relay_handler) self.debug_enabled = False + def _initialize_groups(self): + """Initialize ZHA groups.""" + for group_id in self.application_controller.groups: + group = self.application_controller.groups[group_id] + self._async_get_or_create_group(group) + @callback def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" @@ -300,7 +366,7 @@ class ZHAGateway: if zha_device is None: zha_device = ZHADevice(self._hass, zigpy_device, self) self._devices[zigpy_device.ieee] = zha_device - self.ha_device_registry.async_get_or_create( + device_registry_device = self.ha_device_registry.async_get_or_create( config_entry_id=self._config_entry.entry_id, connections={(CONNECTION_ZIGBEE, str(zha_device.ieee))}, identifiers={(DOMAIN, str(zha_device.ieee))}, @@ -308,10 +374,20 @@ class ZHAGateway: manufacturer=zha_device.manufacturer, model=zha_device.model, ) + zha_device.set_device_id(device_registry_device.id) entry = self.zha_storage.async_get_or_create(zha_device) zha_device.async_update_last_seen(entry.last_seen) return zha_device + @callback + def _async_get_or_create_group(self, zigpy_group): + """Get or create a ZHA group.""" + zha_group = self._groups.get(zigpy_group.group_id) + if zha_group is None: + zha_group = ZHAGroup(self._hass, self, zigpy_group) + self._groups[zigpy_group.group_id] = zha_group + return zha_group + @callback def async_device_became_available( self, sender, profile, cluster, src_ep, dst_ep, message @@ -320,13 +396,13 @@ class ZHAGateway: self.async_update_device(sender) @callback - def async_update_device(self, sender): + def async_update_device(self, sender: zigpy_dev.Device, available: bool = True): """Update device that has just become available.""" if sender.ieee in self.devices: device = self.devices[sender.ieee] # avoid a race condition during new joins if device.status is DeviceStatus.INITIALIZED: - device.update_available(True) + device.update_available(available) async def async_update_device_storage(self): """Update the devices in the store.""" @@ -336,9 +412,6 @@ class ZHAGateway: async def async_device_initialized(self, device): """Handle device joined and basic information discovered (async).""" - if device.nwk == 0x0000: - return - zha_device = self._async_get_or_create_device(device) _LOGGER.debug( @@ -362,9 +435,8 @@ class ZHAGateway: ) await self._async_device_joined(device, zha_device) - device_info = async_get_device_info( - self._hass, zha_device, self.ha_device_registry - ) + device_info = zha_device.async_get_info() + async_dispatcher_send( self._hass, ZHA_GW_MSG, @@ -438,6 +510,38 @@ class ZHAGateway: # will cause async_init to fire so don't explicitly call it zha_device.update_available(True) + async def async_create_zigpy_group(self, name, members): + """Create a new Zigpy Zigbee group.""" + # we start with one to fill any gaps from a user removing existing groups + group_id = 1 + while group_id in self.groups: + group_id += 1 + + # guard against group already existing + if self.async_get_group_by_name(name) is None: + self.application_controller.groups.add_group(group_id, name) + if members is not None: + tasks = [] + for ieee in members: + tasks.append(self.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + return self.groups.get(group_id) + + async def async_remove_zigpy_group(self, group_id): + """Remove a Zigbee group from Zigpy.""" + group = self.groups.get(group_id) + if group and group.members: + tasks = [] + for member in group.members: + tasks.append(member.async_remove_from_group(group_id)) + if tasks: + await asyncio.gather(*tasks) + else: + # we have members but none are tracked by ZHA for whatever reason + self.application_controller.groups.pop(group_id) + else: + self.application_controller.groups.pop(group_id) + async def shutdown(self): """Stop ZHA Controller Application.""" _LOGGER.debug("Shutting down ZHA ControllerApplication") diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py new file mode 100644 index 00000000000..92ce1f75360 --- /dev/null +++ b/homeassistant/components/zha/core/group.py @@ -0,0 +1,95 @@ +""" +Group for Zigbee Home Automation. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/integrations/zha/ +""" +import asyncio +import logging + +from homeassistant.core import callback + +from .helpers import LogMixin + +_LOGGER = logging.getLogger(__name__) + + +class ZHAGroup(LogMixin): + """ZHA Zigbee group object.""" + + def __init__(self, hass, zha_gateway, zigpy_group): + """Initialize the group.""" + self.hass = hass + self._zigpy_group = zigpy_group + self._zha_gateway = zha_gateway + + @property + def name(self): + """Return group name.""" + return self._zigpy_group.name + + @property + def group_id(self): + """Return group name.""" + return self._zigpy_group.group_id + + @property + def endpoint(self): + """Return the endpoint for this group.""" + return self._zigpy_group.endpoint + + @property + def members(self): + """Return the ZHA devices that are members of this group.""" + return [ + self._zha_gateway.devices.get(member_ieee[0]) + for member_ieee in self._zigpy_group.members.keys() + if member_ieee[0] in self._zha_gateway.devices + ] + + async def async_add_members(self, member_ieee_addresses): + """Add members to this group.""" + if len(member_ieee_addresses) > 1: + tasks = [] + for ieee in member_ieee_addresses: + tasks.append( + self._zha_gateway.devices[ieee].async_add_to_group(self.group_id) + ) + await asyncio.gather(*tasks) + else: + await self._zha_gateway.devices[ + member_ieee_addresses[0] + ].async_add_to_group(self.group_id) + + async def async_remove_members(self, member_ieee_addresses): + """Remove members from this group.""" + if len(member_ieee_addresses) > 1: + tasks = [] + for ieee in member_ieee_addresses: + tasks.append( + self._zha_gateway.devices[ieee].async_remove_from_group( + self.group_id + ) + ) + await asyncio.gather(*tasks) + else: + await self._zha_gateway.devices[ + member_ieee_addresses[0] + ].async_remove_from_group(self.group_id) + + @callback + def async_get_info(self): + """Get ZHA group info.""" + group_info = {} + group_info["group_id"] = self.group_id + group_info["name"] = self.name + group_info["members"] = [ + zha_device.async_get_info() for zha_device in self.members + ] + return group_info + + def log(self, level, msg, *args): + """Log a message.""" + msg = f"[%s](%s): {msg}" + args = (self.name, self.group_id) + args + _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 981a03fe7b5..e3ff446ba98 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -11,14 +11,7 @@ import zigpy.types from homeassistant.core import callback -from .const import ( - ATTR_NAME, - CLUSTER_TYPE_IN, - CLUSTER_TYPE_OUT, - DATA_ZHA, - DATA_ZHA_GATEWAY, - DOMAIN, -) +from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -131,28 +124,3 @@ class LogMixin: def error(self, msg, *args): """Error level log.""" return self.log(logging.ERROR, msg, *args) - - -@callback -def async_get_device_info(hass, device, ha_device_registry=None): - """Get ZHA device.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ret_device = {} - ret_device.update(device.device_info) - ret_device["entities"] = [ - { - "entity_id": entity_ref.reference_id, - ATTR_NAME: entity_ref.device_info[ATTR_NAME], - } - for entity_ref in zha_gateway.device_registry[device.ieee] - ] - - if ha_device_registry is not None: - reg_device = ha_device_registry.async_get_device( - {(DOMAIN, str(device.ieee))}, set() - ) - if reg_device is not None: - ret_device["user_given_name"] = reg_device.name_by_user - ret_device["device_reg_id"] = reg_device.id - ret_device["area_id"] = reg_device.area_id - return ret_device diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 37acffd39d0..311f8fa275f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -21,6 +21,7 @@ import zigpy_zigate.api import zigpy_zigate.zigbee.application from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT @@ -43,8 +44,11 @@ REMOTE_DEVICE_TYPES = { zigpy.profiles.zha.DeviceType.COLOR_DIMMER_SWITCH, zigpy.profiles.zha.DeviceType.COLOR_SCENE_CONTROLLER, zigpy.profiles.zha.DeviceType.DIMMER_SWITCH, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, zigpy.profiles.zha.DeviceType.NON_COLOR_SCENE_CONTROLLER, + zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH, zigpy.profiles.zha.DeviceType.REMOTE_CONTROL, zigpy.profiles.zha.DeviceType.SCENE_SELECTOR, ], @@ -63,6 +67,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, zcl.clusters.closures.DoorLock: LOCK, + zcl.clusters.closures.WindowCovering: COVER, zcl.clusters.general.AnalogInput.cluster_id: SENSOR, zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.OnOff: SWITCH, @@ -102,7 +107,6 @@ DEVICE_CLASS = { zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, }, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py new file mode 100644 index 00000000000..3eeb73a23fd --- /dev/null +++ b/homeassistant/components/zha/cover.py @@ -0,0 +1,183 @@ +"""Support for ZHA covers.""" +from datetime import timedelta +import functools +import logging + +from zigpy.zcl.foundation import Status + +from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .core.const import ( + CHANNEL_COVER, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, + SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, +) +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=60) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation cover from config entry.""" + + async def async_discover(discovery_info): + await _async_setup_entities( + hass, config_entry, async_add_entities, [discovery_info] + ) + + unsub = async_dispatcher_connect( + hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover + ) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + covers = hass.data.get(DATA_ZHA, {}).get(DOMAIN) + if covers is not None: + await _async_setup_entities( + hass, config_entry, async_add_entities, covers.values() + ) + del hass.data[DATA_ZHA][DOMAIN] + + +async def _async_setup_entities( + hass, config_entry, async_add_entities, discovery_infos +): + """Set up the ZHA covers.""" + entities = [] + for discovery_info in discovery_infos: + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] + + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaCover) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) + + +@STRICT_MATCH(channel_names=CHANNEL_COVER) +class ZhaCover(ZhaEntity, CoverDevice): + """Representation of a ZHA cover.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._cover_channel = self.cluster_channels.get(CHANNEL_COVER) + self._current_position = None + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._cover_channel, SIGNAL_ATTR_UPDATED, self.async_set_position + ) + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._state = last_state.state + if "current_position" in last_state.attributes: + self._current_position = last_state.attributes["current_position"] + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self.current_cover_position is None: + return None + return self.current_cover_position == 0 + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._state == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._state == STATE_CLOSING + + @property + def current_cover_position(self): + """Return the current position of ZHA cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._current_position + + @callback + def async_set_position(self, pos): + """Handle position update from channel.""" + _LOGGER.debug("setting position: %s", pos) + self._current_position = 100 - pos + if self._current_position == 0: + self._state = STATE_CLOSED + elif self._current_position == 100: + self._state = STATE_OPEN + self.async_schedule_update_ha_state() + + @callback + def async_set_state(self, state): + """Handle state update from channel.""" + _LOGGER.debug("state=%s", state) + self._state = state + self.async_schedule_update_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the window cover.""" + res = await self._cover_channel.up_open() + if isinstance(res, list) and res[1] is Status.SUCCESS: + self.async_set_state(STATE_OPENING) + + async def async_close_cover(self, **kwargs): + """Close the window cover.""" + res = await self._cover_channel.down_close() + if isinstance(res, list) and res[1] is Status.SUCCESS: + self.async_set_state(STATE_CLOSING) + + async def async_set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position.""" + new_pos = kwargs[ATTR_POSITION] + res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) + if isinstance(res, list) and res[1] is Status.SUCCESS: + self.async_set_state( + STATE_CLOSING if new_pos < self._current_position else STATE_OPENING + ) + + async def async_stop_cover(self, **kwargs): + """Stop the window cover.""" + res = await self._cover_channel.stop() + if isinstance(res, list) and res[1] is Status.SUCCESS: + self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED + self.async_schedule_update_ha_state() + + async def async_update(self): + """Attempt to retrieve the open/close state of the cover.""" + await super().async_update() + await self.async_get_state() + + async def async_get_state(self, from_cache=True): + """Fetch the current state.""" + _LOGGER.debug("polling current state") + if self._cover_channel: + pos = await self._cover_channel.get_attribute_value( + "current_position_lift_percentage", from_cache=from_cache + ) + _LOGGER.debug("read pos=%s", pos) + + if pos is not None: + self._current_position = 100 - pos + self._state = ( + STATE_OPEN if self.current_cover_position > 0 else STATE_CLOSED + ) + else: + self._current_position = None + self._state = None diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 0b001bdedbc..6a9dfc63432 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -99,16 +99,19 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): """Return entity availability.""" return self._available + @callback def async_set_available(self, available): """Set entity availability.""" self._available = available self.async_schedule_update_ha_state() + @callback def async_update_state_attribute(self, key, value): """Update a single device state attribute.""" self._device_state_attributes.update({key: value}) self.async_schedule_update_ha_state() + @callback def async_set_state(self, state): """Set the entity state.""" pass diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index f489447e530..6ad13d1c802 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -50,11 +50,6 @@ SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation fans.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation fan from config entry.""" @@ -141,6 +136,7 @@ class ZhaFan(ZhaEntity, FanEntity): """Return state attributes.""" return self.state_attributes + @callback def async_set_state(self, state): """Handle state update from channel.""" self._state = VALUE_TO_SPEED.get(state, self._state) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index eb7d3297b43..409cd339122 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -42,11 +42,6 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) PARALLEL_UPDATES = 5 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation lights.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" @@ -175,6 +170,7 @@ class Light(ZhaEntity, light.Light): """Flag supported features.""" return self._supported_features + @callback def async_set_state(self, state): """Set the state.""" self._state = bool(state) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index bf82252246c..b173c166a77 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -33,11 +33,6 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation locks.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation Door Lock from config entry.""" @@ -130,6 +125,7 @@ class ZhaDoorLock(ZhaEntity, LockDevice): await super().async_update() await self.async_get_state() + @callback def async_set_state(self, state): """Handle state update from channel.""" self._state = VALUE_TO_STATE.get(state, self._state) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e3d0eda3e02..f7f70db590a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.12.0", - "zha-quirks==0.0.31", + "bellows-homeassistant==0.13.1", + "zha-quirks==0.0.32", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.12.0", - "zigpy-xbee-homeassistant==0.8.0", - "zigpy-zigate==0.5.0" + "zigpy-homeassistant==0.13.0", + "zigpy-xbee-homeassistant==0.9.0", + "zigpy-zigate==0.5.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ce02bf11d9d..8b7dd894973 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -63,11 +63,6 @@ CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation sensor from config entry.""" @@ -128,7 +123,7 @@ class Sensor(ZhaEntity): async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - self._device_state_attributes = await self.async_state_attr_provider() + self._device_state_attributes.update(await self.async_state_attr_provider()) await self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state @@ -152,10 +147,9 @@ class Sensor(ZhaEntity): """Return the state of the entity.""" if self._state is None: return None - if isinstance(self._state, float): - return str(round(self._state, 2)) return self._state + @callback def async_set_state(self, state): """Handle state update from channel.""" if state is not None: @@ -209,6 +203,13 @@ class Battery(Sensor): state_attrs["battery_quantity"] = battery_quantity return state_attrs + @callback + def async_update_state_attribute(self, key, value): + """Update a single device state attribute.""" + if key == "battery_voltage": + self._device_state_attributes[key] = round(value / 10, 1) + self.async_schedule_update_ha_state() + @STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): @@ -225,7 +226,10 @@ class ElectricalMeasurement(Sensor): def formatter(self, value) -> int: """Return 'normalized' value.""" - return round(value * self._channel.multiplier / self._channel.divisor) + value = value * self._channel.multiplier / self._channel.divisor + if value < 100 and self._channel.divisor > 1: + return round(value, self._decimals) + return round(value) @STRICT_MATCH(channel_names=CHANNEL_MULTISTATE_INPUT) diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index cbd29925f62..1280ace34dc 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -23,11 +23,6 @@ _LOGGER = logging.getLogger(__name__) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up Zigbee Home Automation switches.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation switch from config entry.""" @@ -98,6 +93,7 @@ class Switch(ZhaEntity, SwitchDevice): self._state = False self.async_schedule_update_ha_state() + @callback def async_set_state(self, state): """Handle state update from channel.""" self._state = bool(state) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index e88993beee8..91a1338b671 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,36 +1,41 @@ """Support for the definition of zones.""" import logging -from typing import Set, cast +from typing import Dict, List, Optional, cast import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ICON, + CONF_ID, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, + SERVICE_RELOAD, +) +from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback +from homeassistant.helpers import ( + collection, + config_validation as cv, + entity, + entity_component, + entity_registry, + service, + storage, ) -from homeassistant.core import State, callback -from homeassistant.helpers import config_per_platform -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.loader import bind_hass -from homeassistant.util import slugify from homeassistant.util.location import distance -from .config_flow import configured_zones from .const import ATTR_PASSIVE, ATTR_RADIUS, CONF_PASSIVE, DOMAIN, HOME_ZONE -from .zone import Zone - -# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Unnamed zone" DEFAULT_PASSIVE = False DEFAULT_RADIUS = 100 @@ -40,29 +45,47 @@ ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) ICON_HOME = "mdi:home" ICON_IMPORT = "mdi:import" -# The config that zone accepts is the same as if it has platforms. -PLATFORM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), - vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - }, +CREATE_FIELDS = { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +} + + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_PASSIVE): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +} + + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)])}, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + @bind_hass -def async_active_zone(hass, latitude, longitude, radius=0): +def async_active_zone( + hass: HomeAssistant, latitude: float, longitude: float, radius: int = 0 +) -> Optional[State]: """Find the active zone for given latitude, longitude. This method must be run in the event loop. """ # Sort entity IDs so that we are deterministic if equal distance to 2 zones zones = ( - hass.states.get(entity_id) + cast(State, hass.states.get(entity_id)) for entity_id in sorted(hass.states.async_entity_ids(DOMAIN)) ) @@ -80,6 +103,9 @@ def async_active_zone(hass, latitude, longitude, radius=0): zone.attributes[ATTR_LONGITUDE], ) + if zone_dist is None: + continue + within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] closer_zone = closest is None or zone_dist < min_dist # type: ignore smaller_zone = ( @@ -95,79 +121,227 @@ def async_active_zone(hass, latitude, longitude, radius=0): return closest -async def async_setup(hass, config): - """Set up configured zones as well as Home Assistant zone if necessary.""" - hass.data[DOMAIN] = {} - entities: Set[str] = set() - zone_entries = configured_zones(hass) - for _, entry in config_per_platform(config, DOMAIN): - if slugify(entry[CONF_NAME]) not in zone_entries: - zone = Zone( - hass, - entry[CONF_NAME], - entry[CONF_LATITUDE], - entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), - entry.get(CONF_ICON), - entry.get(CONF_PASSIVE), - ) - zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, entry[CONF_NAME], entities - ) - hass.async_create_task(zone.async_update_ha_state()) - entities.add(zone.entity_id) +def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: + """Test if given latitude, longitude is in given zone. - if ENTITY_ID_HOME in entities or HOME_ZONE in zone_entries: - return True - - zone = Zone( - hass, - hass.config.location_name, - hass.config.latitude, - hass.config.longitude, - DEFAULT_RADIUS, - ICON_HOME, - False, + Async friendly. + """ + zone_dist = distance( + latitude, + longitude, + zone.attributes[ATTR_LATITUDE], + zone.attributes[ATTR_LONGITUDE], ) - zone.entity_id = ENTITY_ID_HOME - hass.async_create_task(zone.async_update_ha_state()) + + if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: + return False + return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS]) + + +class ZoneStorageCollection(collection.StorageCollection): + """Zone collection stored in storage.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + + async def _process_create_data(self, data: Dict) -> Dict: + """Validate the config is valid.""" + return cast(Dict, self.CREATE_SCHEMA(data)) @callback - def core_config_updated(_): + def _get_suggested_id(self, info: Dict) -> str: + """Suggest an ID based on the config.""" + return cast(str, info[CONF_NAME]) + + async def _update_data(self, data: dict, update_data: Dict) -> Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return {**data, **update_data} + + +class IDLessCollection(collection.ObservableCollection): + """A collection without IDs.""" + + counter = 0 + + async def async_load(self, data: List[dict]) -> None: + """Load the collection. Overrides existing data.""" + for item_id in list(self.data): + await self.notify_change(collection.CHANGE_REMOVED, item_id, None) + + self.data.clear() + + for item in data: + self.counter += 1 + item_id = f"fakeid-{self.counter}" + + self.data[item_id] = item + await self.notify_change(collection.CHANGE_ADDED, item_id, item) + + +async def async_setup(hass: HomeAssistant, config: Dict) -> bool: + """Set up configured zones as well as Home Assistant zone if necessary.""" + component = entity_component.EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() + + yaml_collection = IDLessCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, lambda conf: Zone(conf, False) + ) + + storage_collection = ZoneStorageCollection( + storage.Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}_storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, lambda conf: Zone(conf, True) + ) + + if DOMAIN in config: + await yaml_collection.async_load(config[DOMAIN]) + + await storage_collection.async_load() + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) + + async def _collection_changed( + change_type: str, item_id: str, config: Optional[Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: + return + + ent_reg = await entity_registry.async_get_registry(hass) + ent_reg.async_remove( + cast(str, ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) + ) + + storage_collection.async_add_listener(_collection_changed) + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Remove all zones and load new ones from config.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + return + await yaml_collection.async_load(conf[DOMAIN]) + + service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + if component.get_entity("zone.home"): + return True + + home_zone = Zone(_home_conf(hass), True,) + home_zone.entity_id = ENTITY_ID_HOME + await component.async_add_entities([home_zone]) # type: ignore + + async def core_config_updated(_: Event) -> None: """Handle core config updated.""" - zone.name = hass.config.location_name - zone.latitude = hass.config.latitude - zone.longitude = hass.config.longitude - zone.async_write_ha_state() + await home_zone.async_update_config(_home_conf(hass)) hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated) + hass.data[DOMAIN] = storage_collection + return True -async def async_setup_entry(hass, config_entry): +@callback +def _home_conf(hass: HomeAssistant) -> Dict: + """Return the home zone config.""" + return { + CONF_NAME: hass.config.location_name, + CONF_LATITUDE: hass.config.latitude, + CONF_LONGITUDE: hass.config.longitude, + CONF_RADIUS: DEFAULT_RADIUS, + CONF_ICON: ICON_HOME, + CONF_PASSIVE: False, + } + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: """Set up zone as config entry.""" - entry = config_entry.data - name = entry[CONF_NAME] - zone = Zone( - hass, - name, - entry[CONF_LATITUDE], - entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS, DEFAULT_RADIUS), - entry.get(CONF_ICON), - entry.get(CONF_PASSIVE, DEFAULT_PASSIVE), - ) - zone.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, None, hass) - hass.async_create_task(zone.async_update_ha_state()) - hass.data[DOMAIN][slugify(name)] = zone + storage_collection = cast(ZoneStorageCollection, hass.data[DOMAIN]) + + data = dict(config_entry.data) + data.setdefault(CONF_PASSIVE, DEFAULT_PASSIVE) + data.setdefault(CONF_RADIUS, DEFAULT_RADIUS) + + await storage_collection.async_create_item(data) + + hass.async_create_task(hass.config_entries.async_remove(config_entry.entry_id)) + return True -async def async_unload_entry(hass, config_entry): - """Unload a config entry.""" - zones = hass.data[DOMAIN] - name = slugify(config_entry.data[CONF_NAME]) - zone = zones.pop(name) - await zone.async_remove() +async def async_unload_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> bool: + """Will be called once we remove it.""" return True + + +class Zone(entity.Entity): + """Representation of a Zone.""" + + def __init__(self, config: Dict, editable: bool): + """Initialize the zone.""" + self._config = config + self._editable = editable + self._attrs: Optional[Dict] = None + self._generate_attrs() + + @property + def state(self) -> str: + """Return the state property really does nothing for a zone.""" + return "zoning" + + @property + def name(self) -> str: + """Return name.""" + return cast(str, self._config[CONF_NAME]) + + @property + def unique_id(self) -> Optional[str]: + """Return unique ID.""" + return self._config.get(CONF_ID) + + @property + def icon(self) -> Optional[str]: + """Return the icon if any.""" + return self._config.get(CONF_ICON) + + @property + def state_attributes(self) -> Optional[Dict]: + """Return the state attributes of the zone.""" + return self._attrs + + async def async_update_config(self, config: Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self._generate_attrs() + self.async_write_ha_state() + + @callback + def _generate_attrs(self) -> None: + """Generate new attrs based on config.""" + self._attrs = { + ATTR_HIDDEN: True, + ATTR_LATITUDE: self._config[CONF_LATITUDE], + ATTR_LONGITUDE: self._config[CONF_LONGITUDE], + ATTR_RADIUS: self._config[CONF_RADIUS], + ATTR_PASSIVE: self._config[CONF_PASSIVE], + ATTR_EDITABLE: self._editable, + } diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 4531ff7b834..bb34a83ad26 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -1,75 +1,13 @@ -"""Config flow to configure zone component.""" - -from typing import Set - -import voluptuous as vol +"""Config flow to configure zone component. +This is no longer in use. This file is around so that existing +config entries will remain to be loaded and then automatically +migrated to the storage collection. +""" from homeassistant import config_entries -from homeassistant.const import ( - CONF_ICON, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, -) -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import slugify -from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE - -# mypy: allow-untyped-defs, no-check-untyped-defs +from .const import DOMAIN # noqa # pylint:disable=unused-import -@callback -def configured_zones(hass: HomeAssistantType) -> Set[str]: - """Return a set of the configured zones.""" - return set( - (slugify(entry.data[CONF_NAME])) - for entry in ( - hass.config_entries.async_entries(DOMAIN) if hass.config_entries else [] - ) - ) - - -@config_entries.HANDLERS.register(DOMAIN) -class ZoneFlowHandler(config_entries.ConfigFlow): - """Zone config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize zone configuration flow.""" - pass - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - return await self.async_step_init(user_input) - - async def async_step_init(self, user_input=None): - """Handle a flow start.""" - errors = {} - - if user_input is not None: - name = slugify(user_input[CONF_NAME]) - if name not in configured_zones(self.hass) and name != HOME_ZONE: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - errors["base"] = "name_exists" - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Required(CONF_NAME): str, - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS): vol.Coerce(float), - vol.Optional(CONF_ICON): str, - vol.Optional(CONF_PASSIVE): bool, - } - ), - errors=errors, - ) +class ZoneConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Stub zone config flow class.""" diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index 8efed9ba7a6..d45399c3f31 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -1,7 +1,7 @@ { "domain": "zone", "name": "Zone", - "config_flow": true, + "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/zone", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/zone/services.yaml b/homeassistant/components/zone/services.yaml new file mode 100644 index 00000000000..550eee24fab --- /dev/null +++ b/homeassistant/components/zone/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload the YAML-based zone configuration. diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json deleted file mode 100644 index ff2c7c07c14..00000000000 --- a/homeassistant/components/zone/strings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "title": "Zone", - "step": { - "init": { - "title": "Define zone parameters", - "data": { - "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Radius", - "passive": "Passive", - "icon": "Icon" - } - } - }, - "error": { - "name_exists": "Name already exists" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py deleted file mode 100644 index f084492bd34..00000000000 --- a/homeassistant/components/zone/zone.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Zone entity and functionality.""" - -from typing import cast - -from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import State -from homeassistant.helpers.entity import Entity -from homeassistant.util.location import distance - -from .const import ATTR_PASSIVE, ATTR_RADIUS - -STATE = "zoning" - - -# mypy: allow-untyped-defs - - -def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: - """Test if given latitude, longitude is in given zone. - - Async friendly. - """ - zone_dist = distance( - latitude, - longitude, - zone.attributes[ATTR_LATITUDE], - zone.attributes[ATTR_LONGITUDE], - ) - - if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: - return False - return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS]) - - -class Zone(Entity): - """Representation of a Zone.""" - - name = None - - def __init__(self, hass, name, latitude, longitude, radius, icon, passive): - """Initialize the zone.""" - self.hass = hass - self.name = name - self.latitude = latitude - self.longitude = longitude - self._radius = radius - self._icon = icon - self._passive = passive - - @property - def state(self): - """Return the state property really does nothing for a zone.""" - return STATE - - @property - def icon(self): - """Return the icon if any.""" - return self._icon - - @property - def state_attributes(self): - """Return the state attributes of the zone.""" - data = { - ATTR_HIDDEN: True, - ATTR_LATITUDE: self.latitude, - ATTR_LONGITUDE: self.longitude, - ATTR_RADIUS: self._radius, - } - if self._passive: - data[ATTR_PASSIVE] = self._passive - return data diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index 68df3313de3..e4bafc44bee 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -14,11 +14,6 @@ from .const import COMMAND_CLASS_SENSOR_BINARY _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave binary sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave binary sensors from Config Entry.""" diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 2b421db70b5..840418fb063 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -128,11 +128,6 @@ DEFAULT_HVAC_MODES = [ ] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave climate devices.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Climate device from Config Entry.""" diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 95cc994e4ff..e6aa8028849 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -29,11 +29,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave covers.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Cover from Config Entry.""" diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index b77ab8dcf68..a2dbc3a4eab 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -28,11 +28,6 @@ VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave fans.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Fan from Config Entry.""" diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index e941b2a97dc..b32daf71f54 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -61,11 +61,6 @@ TEMP_WARM_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 * 2 + TEMP_COLOR_MIN TEMP_COLD_HASS = (TEMP_COLOR_MAX - TEMP_COLOR_MIN) / 3 + TEMP_COLOR_MIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave lights.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Light from Config Entry.""" @@ -385,7 +380,9 @@ class ZwaveColorLight(ZwaveDimmer): # white LED must be off in order for color to work self._white = 0 - if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: + if ( + ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs + ) and self._hs is not None: rgbw = "#" for colorval in color_util.color_hs_to_RGB(*self._hs): rgbw += format(colorval, "02x") diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index f84b1b5cfd4..44e73da320f 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -153,11 +153,6 @@ CLEAR_USERCODE_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave locks.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Lock from Config Entry.""" diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index c781a493b55..1fc6401f25b 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.8", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 08ee54415ad..b732e3569ed 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -11,11 +11,6 @@ from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave sensors.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Sensor from Config Entry.""" diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py index 3592f534074..4956e99a40e 100644 --- a/homeassistant/components/zwave/switch.py +++ b/homeassistant/components/zwave/switch.py @@ -11,11 +11,6 @@ from . import ZWaveDeviceEntity, workaround _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old method of setting up Z-Wave switches.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave Switch from Config Entry.""" diff --git a/homeassistant/config.py b/homeassistant/config.py index 6777c1ef5a5..f5870d683a0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -226,35 +226,34 @@ def get_default_config_dir() -> str: return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore -async def async_ensure_config_exists( - hass: HomeAssistant, config_dir: str -) -> Optional[str]: +async def async_ensure_config_exists(hass: HomeAssistant) -> bool: """Ensure a configuration file exists in given configuration directory. Creating a default one if needed. - Return path to the configuration file. + Return boolean if configuration dir is ready to go. """ - config_path = find_config_file(config_dir) + config_path = hass.config.path(YAML_CONFIG_FILE) - if config_path is None: - print("Unable to find configuration. Creating default one in", config_dir) - config_path = await async_create_default_config(hass, config_dir) + if os.path.isfile(config_path): + return True - return config_path + print( + "Unable to find configuration. Creating default one in", hass.config.config_dir + ) + return await async_create_default_config(hass) -async def async_create_default_config( - hass: HomeAssistant, config_dir: str -) -> Optional[str]: +async def async_create_default_config(hass: HomeAssistant) -> bool: """Create a default configuration file in given configuration directory. - Return path to new config file if success, None if failed. - This method needs to run in an executor. + Return if creation was successful. """ - return await hass.async_add_executor_job(_write_default_config, config_dir) + return await hass.async_add_executor_job( + _write_default_config, hass.config.config_dir + ) -def _write_default_config(config_dir: str) -> Optional[str]: +def _write_default_config(config_dir: str) -> bool: """Write the default config.""" config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) @@ -288,11 +287,11 @@ def _write_default_config(config_dir: str) -> Optional[str]: with open(scene_yaml_path, "wt"): pass - return config_path + return True except OSError: print("Unable to create default configuration file", config_path) - return None + return False async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: @@ -300,35 +299,16 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: This function allow a component inside the asyncio loop to reload its configuration by itself. Include package merge. - - This method is a coroutine. """ - - def _load_hass_yaml_config() -> Dict: - path = find_config_file(hass.config.config_dir) - if path is None: - raise HomeAssistantError( - f"Config file not found in: {hass.config.config_dir}" - ) - config = load_yaml_config_file(path) - return config - # Not using async_add_executor_job because this is an internal method. - config = await hass.loop.run_in_executor(None, _load_hass_yaml_config) + config = await hass.loop.run_in_executor( + None, load_yaml_config_file, hass.config.path(YAML_CONFIG_FILE) + ) core_config = config.get(CONF_CORE, {}) await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) return config -def find_config_file(config_dir: Optional[str]) -> Optional[str]: - """Look in given directory for supported configuration files.""" - if config_dir is None: - return None - config_path = os.path.join(config_dir, YAML_CONFIG_FILE) - - return config_path if os.path.isfile(config_path) else None - - def load_yaml_config_file(config_path: str) -> Dict[Any, Any]: """Parse a YAML configuration file. @@ -382,8 +362,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if version_obj < LooseVersion("0.92"): # 0.92 moved google/tts.py to google_translate/tts.py - config_path = find_config_file(hass.config.config_dir) - assert config_path is not None + config_path = hass.config.path(YAML_CONFIG_FILE) with open(config_path, "rt", encoding="utf-8") as config_file: config_raw = config_file.read() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6fb5595dac4..793e8be0045 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -817,13 +817,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): raise data_entry_flow.UnknownHandler @callback - def _abort_if_unique_id_configured(self) -> None: + def _abort_if_unique_id_configured(self, updates: Dict[Any, Any] = None) -> None: """Abort if the unique ID is already configured.""" + assert self.hass if self.unique_id is None: return - if self.unique_id in self._async_current_ids(): - raise data_entry_flow.AbortFlow("already_configured") + for entry in self._async_current_entries(): + if entry.unique_id == self.unique_id: + if updates is not None and not updates.items() <= entry.data.items(): + self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( self, unique_id: str, *, raise_on_progress: bool = True diff --git a/homeassistant/const.py b/homeassistant/const.py index 4f6d643b531..d374c85cada 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 104 -PATCH_VERSION = "3" +MINOR_VERSION = 105 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dcae6fd065e..cf77dae7fb2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ FLOWS = [ "elgato", "emulated_roku", "esphome", + "garmin_connect", "geofency", "geonetnz_quakes", "geonetnz_volcano", @@ -53,10 +54,12 @@ FLOWS = [ "luftdaten", "mailgun", "met", + "mikrotik", "mobile_app", "mqtt", "neato", "nest", + "netatmo", "notion", "opentherm_gw", "openuv", @@ -67,6 +70,7 @@ FLOWS = [ "ps4", "rainmachine", "ring", + "samsungtv", "sentry", "simplisafe", "smartthings", @@ -76,6 +80,7 @@ FLOWS = [ "soma", "somfy", "sonos", + "spotify", "starline", "tellduslive", "tesla", @@ -90,11 +95,11 @@ FLOWS = [ "upnp", "velbus", "vesync", + "vizio", "wemo", "withings", "wled", "wwlln", "zha", - "zone", "zwave" ] diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index adf3a345bbe..bea04484b11 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -24,7 +24,21 @@ SSDP = { ], "hue": [ { - "manufacturer": "Royal Philips Electronics" + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2012" + }, + { + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2015" + }, + { + "manufacturer": "Signify", + "modelName": "Philips hue bridge 2015" + } + ], + "samsungtv": [ + { + "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], "sonos": [ diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 306b3850a1b..9817dd69f81 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -24,6 +24,15 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_printer._tcp.local.": [ + "brother" + ], + "_spotify-connect._tcp.local.": [ + "spotify" + ], + "_viziocast._tcp.local.": [ + "vizio" + ], "_wled._tcp.local.": [ "wled" ] @@ -32,6 +41,9 @@ ZEROCONF = { HOMEKIT = { "BSB002": "hue", "LIFX": "lifx", + "Netatmo Relay": "netatmo", + "Presence": "netatmo", "TRADFRI": "tradfri", + "Welcome": "netatmo", "Wemo": "wemo" } diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 6ac1326545a..0beeb4da4e8 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,5 +1,6 @@ """Helper to check the configuration file.""" from collections import OrderedDict +import os from typing import List, NamedTuple, Optional import attr @@ -10,10 +11,10 @@ from homeassistant.config import ( CONF_CORE, CONF_PACKAGES, CORE_CONFIG_SCHEMA, + YAML_CONFIG_FILE, _format_config_error, config_per_platform, extract_domain_configs, - find_config_file, load_yaml_config_file, merge_packages_config, ) @@ -62,7 +63,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig This method is a coroutine. """ - config_dir = hass.config.config_dir result = HomeAssistantConfig() def _pack_error( @@ -79,9 +79,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig result.add_error(_format_config_error(ex, domain, config), domain, config) # Load configuration.yaml + config_path = hass.config.path(YAML_CONFIG_FILE) try: - config_path = await hass.async_add_executor_job(find_config_file, config_dir) - if not config_path: + if not await hass.async_add_executor_job(os.path.isfile, config_path): return result.add_error("File configuration.yaml not found.") config = await hass.async_add_executor_job(load_yaml_config_file, config_path) except FileNotFoundError: diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index dd0edbd09b9..1b3721788f5 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -10,9 +10,11 @@ from homeassistant.components import websocket_api from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify STORAGE_VERSION = 1 @@ -112,7 +114,7 @@ class ObservableCollection(ABC): class YamlCollection(ObservableCollection): - """Offer a fake CRUD interface on top of static YAML.""" + """Offer a collection based on static data.""" async def async_load(self, data: List[dict]) -> None: """Load the YAML collection. Overrides existing data.""" @@ -131,7 +133,7 @@ class YamlCollection(ObservableCollection): event = CHANGE_ADDED self.data[item_id] = item - await self.notify_change(event, item[CONF_ID], item) + await self.notify_change(event, item_id, item) for item_id in old_ids: self.data.pop(item_id) @@ -244,7 +246,7 @@ def attach_entity_component_collection( """Handle a collection change.""" if change_type == CHANGE_ADDED: entity = create_entity(cast(dict, config)) - await entity_component.async_add_entities([entity]) + await entity_component.async_add_entities([entity]) # type: ignore entities[item_id] = entity return @@ -259,6 +261,30 @@ def attach_entity_component_collection( collection.async_add_listener(_collection_changed) +@callback +def attach_entity_registry_cleaner( + hass: HomeAssistantType, + domain: str, + platform: str, + collection: ObservableCollection, +) -> None: + """Attach a listener to clean up entity registry on collection changes.""" + + async def _collection_changed( + change_type: str, item_id: str, config: Optional[Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != CHANGE_REMOVED: + return + + ent_reg = await entity_registry.async_get_registry(hass) + ent_to_remove = ent_reg.async_get_entity_id(domain, platform, item_id) + if ent_to_remove is not None: + ent_reg.async_remove(ent_to_remove) + + collection.async_add_listener(_collection_changed) + + class StorageCollectionWebsocket: """Class to expose storage collection management over websocket.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 02853f7615b..3500a3a4e3d 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,10 +1,11 @@ """Offer reusable conditions.""" import asyncio +from collections import deque from datetime import datetime, timedelta import functools as ft import logging import sys -from typing import Callable, Container, Optional, Union, cast +from typing import Callable, Container, Optional, Set, Union, cast from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( @@ -19,6 +20,7 @@ from homeassistant.const import ( CONF_BEFORE, CONF_BELOW, CONF_CONDITION, + CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_STATE, @@ -31,7 +33,7 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, WEEKDAYS, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date @@ -473,7 +475,7 @@ def zone( if latitude is None or longitude is None: return False - return zone_cmp.zone.in_zone( + return zone_cmp.in_zone( zone_ent, latitude, longitude, entity.attributes.get(ATTR_GPS_ACCURACY, 0) ) @@ -529,3 +531,50 @@ async def async_validate_condition_config( return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore return config + + +@callback +def async_extract_entities(config: ConfigType) -> Set[str]: + """Extract entities from a condition.""" + referenced = set() + to_process = deque([config]) + + while to_process: + config = to_process.popleft() + condition = config[CONF_CONDITION] + + if condition in ("and", "or"): + to_process.extend(config["conditions"]) + continue + + entity_id = config.get(CONF_ENTITY_ID) + + if entity_id is not None: + referenced.add(entity_id) + + return referenced + + +@callback +def async_extract_devices(config: ConfigType) -> Set[str]: + """Extract devices from a condition.""" + referenced = set() + to_process = deque([config]) + + while to_process: + config = to_process.popleft() + condition = config[CONF_CONDITION] + + if condition in ("and", "or"): + to_process.extend(config["conditions"]) + continue + + if condition != "device": + continue + + device_id = config.get(CONF_DEVICE_ID) + + if device_id is not None: + referenced.add(device_id) + + return referenced diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d29dae735f8..9baed41dd20 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -262,7 +262,6 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_discovery(self, user_input: dict = None) -> dict: """Handle a flow initialized by discovery.""" await self.async_set_unique_id(self.DOMAIN) - self._abort_if_unique_id_configured() assert self.hass is not None if self.hass.config_entries.async_entries(self.DOMAIN): diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e357a2ba622..852948220de 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -724,6 +724,8 @@ PLATFORM_SCHEMA = vol.Schema( PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +ENTITY_SERVICE_FIELDS = (ATTR_ENTITY_ID, ATTR_AREA_ID) + def make_entity_service_schema( schema: dict, *, extra: int = vol.PREVENT_EXTRA @@ -738,7 +740,7 @@ def make_entity_service_schema( }, extra=extra, ), - has_at_least_one_key(ATTR_ENTITY_ID, ATTR_AREA_ID), + has_at_least_one_key(*ENTITY_SERVICE_FIELDS), ) diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py new file mode 100644 index 00000000000..5bacbdb7d11 --- /dev/null +++ b/homeassistant/helpers/debounce.py @@ -0,0 +1,77 @@ +"""Debounce helper.""" +import asyncio +from logging import Logger +from typing import Any, Awaitable, Callable, Optional + +from homeassistant.core import HomeAssistant, callback + + +class Debouncer: + """Class to rate limit calls to a specific command.""" + + def __init__( + self, + hass: HomeAssistant, + logger: Logger, + cooldown: float, + immediate: bool, + function: Optional[Callable[..., Awaitable[Any]]] = None, + ): + """Initialize debounce. + + immediate: indicate if the function needs to be called right away and + wait 0.3s until executing next invocation. + function: optional and can be instantiated later. + """ + self.hass = hass + self.logger = logger + self.function = function + self.cooldown = cooldown + self.immediate = immediate + self._timer_task: Optional[asyncio.TimerHandle] = None + self._execute_at_end_of_timer: bool = False + + async def async_call(self) -> None: + """Call the function.""" + assert self.function is not None + + if self._timer_task: + if not self._execute_at_end_of_timer: + self._execute_at_end_of_timer = True + + return + + if self.immediate: + await self.hass.async_add_job(self.function) # type: ignore + else: + self._execute_at_end_of_timer = True + + self._timer_task = self.hass.loop.call_later( + self.cooldown, + lambda: self.hass.async_create_task(self._handle_timer_finish()), + ) + + async def _handle_timer_finish(self) -> None: + """Handle a finished timer.""" + assert self.function is not None + + self._timer_task = None + + if not self._execute_at_end_of_timer: + return + + self._execute_at_end_of_timer = False + + try: + await self.hass.async_add_job(self.function) # type: ignore + except Exception: # pylint: disable=broad-except + self.logger.exception("Unexpected exception from %s", self.function) + + @callback + def async_cancel(self) -> None: + """Cancel any scheduled call.""" + if self._timer_task: + self._timer_task.cancel() + self._timer_task = None + + self._execute_at_end_of_timer = False diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 512334c8d3c..0821b909dc7 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -260,6 +260,7 @@ class DeviceRegistry: return new + @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" del self.devices[device_id] @@ -375,3 +376,15 @@ async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: def async_entries_for_area(registry: DeviceRegistry, area_id: str) -> List[DeviceEntry]: """Return entries that match an area.""" return [device for device in registry.devices.values() if device.area_id == area_id] + + +@callback +def async_entries_for_config_entry( + registry: DeviceRegistry, config_entry_id: str +) -> List[DeviceEntry]: + """Return entries that match a config entry.""" + return [ + device + for device in registry.devices.values() + if config_entry_id in device.config_entries + ] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7ccc6c35613..a2a0ae840e0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -284,7 +284,7 @@ class Entity(ABC): self._async_write_ha_state() @callback - def async_write_ha_state(self): + def async_write_ha_state(self) -> None: """Write the state to the state machine.""" if self.hass is None: raise RuntimeError(f"Attribute hass is None for {self}") @@ -294,7 +294,7 @@ class Entity(ABC): f"No entity id specified for entity {self.name}" ) - self._async_write_ha_state() + self._async_write_ha_state() # type: ignore @callback def _async_write_ha_state(self): @@ -596,23 +596,17 @@ class ToggleEntity(Entity): """Turn the entity on.""" raise NotImplementedError() - def async_turn_on(self, **kwargs): - """Turn the entity on. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.turn_on, **kwargs)) + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await self.hass.async_add_job(ft.partial(self.turn_on, **kwargs)) def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" raise NotImplementedError() - def async_turn_off(self, **kwargs): - """Turn the entity off. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(ft.partial(self.turn_off, **kwargs)) + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self.hass.async_add_job(ft.partial(self.turn_off, **kwargs)) def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" @@ -621,11 +615,9 @@ class ToggleEntity(Entity): else: self.turn_on(**kwargs) - def async_toggle(self, **kwargs): - """Toggle the entity. - - This method must be run in the event loop and returns a coroutine. - """ + async def async_toggle(self, **kwargs): + """Toggle the entity.""" if self.is_on: - return self.async_turn_off(**kwargs) - return self.async_turn_on(**kwargs) + await self.async_turn_off(**kwargs) + else: + await self.async_turn_on(**kwargs) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 404fd4ed46d..e26dc5dfbea 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -3,20 +3,21 @@ import asyncio from datetime import timedelta from itertools import chain import logging +from types import ModuleType +from typing import Dict, Optional, cast from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_NAMESPACE, - CONF_SCAN_INTERVAL, - ENTITY_MATCH_ALL, -) +from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.config_validation import make_entity_service_schema -from homeassistant.helpers.service import async_extract_entity_ids +from homeassistant.helpers import ( + config_per_platform, + config_validation as cv, + discovery, + entity, + service, +) from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform @@ -40,15 +41,15 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: ) return - entity = entity_comp.get_entity(entity_id) + entity_obj = entity_comp.get_entity(entity_id) - if entity is None: + if entity_obj is None: logging.getLogger(__name__).warning( "Forced update failed. Entity %s not found.", entity_id ) return - await entity.async_update_ha_state(True) + await entity_obj.async_update_ha_state(True) class EntityComponent: @@ -61,7 +62,13 @@ class EntityComponent: - Listen for discovery events for platforms related to the domain. """ - def __init__(self, logger, domain, hass, scan_interval=DEFAULT_SCAN_INTERVAL): + def __init__( + self, + logger: logging.Logger, + domain: str, + hass: HomeAssistant, + scan_interval: timedelta = DEFAULT_SCAN_INTERVAL, + ): """Initialize an entity component.""" self.logger = logger self.hass = hass @@ -70,7 +77,9 @@ class EntityComponent: self.config = None - self._platforms = {domain: self._async_init_entity_platform(domain, None)} + self._platforms: Dict[str, EntityPlatform] = { + domain: self._async_init_entity_platform(domain, None) + } self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities @@ -83,12 +92,12 @@ class EntityComponent: platform.entities.values() for platform in self._platforms.values() ) - def get_entity(self, entity_id): + def get_entity(self, entity_id: str) -> Optional[entity.Entity]: """Get an entity.""" for platform in self._platforms.values(): - entity = platform.entities.get(entity_id) - if entity is not None: - return entity + entity_obj = cast(Optional[entity.Entity], platform.entities.get(entity_id)) + if entity_obj is not None: + return entity_obj return None def setup(self, config): @@ -166,39 +175,27 @@ class EntityComponent: await platform.async_reset() return True - async def async_extract_from_service(self, service, expand_group=True): + async def async_extract_from_service(self, service_call, expand_group=True): """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ - data_ent_id = service.data.get(ATTR_ENTITY_ID) - - if data_ent_id is None: - return [] - - if data_ent_id == ENTITY_MATCH_ALL: - return [entity for entity in self.entities if entity.available] - - entity_ids = await async_extract_entity_ids(self.hass, service, expand_group) - return [ - entity - for entity in self.entities - if entity.available and entity.entity_id in entity_ids - ] + return await service.async_extract_entities( + self.hass, self.entities, service_call, expand_group + ) @callback def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" if isinstance(schema, dict): - schema = make_entity_service_schema(schema) + schema = cv.make_entity_service_schema(schema) async def handle_service(call): """Handle the service.""" - service_name = f"{self.domain}.{name}" await self.hass.helpers.service.entity_service_call( - self._platforms.values(), func, call, service_name, required_features + self._platforms.values(), func, call, required_features ) self.hass.services.async_register(self.domain, name, handle_service, schema) @@ -251,7 +248,7 @@ class EntityComponent: if entity_id in platform.entities: await platform.async_remove_entity(entity_id) - async def async_prepare_reload(self, *, skip_reset=False): + async def async_prepare_reload(self, *, skip_reset: bool = False) -> Optional[dict]: """Prepare reloading this entity component. This method must be run in the event loop. @@ -264,25 +261,30 @@ class EntityComponent: integration = await async_get_integration(self.hass, self.domain) - conf = await conf_util.async_process_component_config( + processed_conf = await conf_util.async_process_component_config( self.hass, conf, integration ) - if conf is None: + if processed_conf is None: return None if not skip_reset: await self._async_reset() - return conf + + return processed_conf def _async_init_entity_platform( - self, platform_type, platform, scan_interval=None, entity_namespace=None - ): + self, + platform_type: str, + platform: Optional[ModuleType], + scan_interval: Optional[timedelta] = None, + entity_namespace: Optional[str] = None, + ) -> EntityPlatform: """Initialize an entity platform.""" if scan_interval is None: scan_interval = self.scan_interval - return EntityPlatform( + return EntityPlatform( # type: ignore hass=self.hass, logger=self.logger, domain=self.domain, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0e4d80ac080..8fedc198fe2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -7,6 +7,7 @@ from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, split_entity_id, valid_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers import config_validation as cv, service from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION @@ -194,7 +195,11 @@ class EntityPlatform: ) return False except Exception: # pylint: disable=broad-except - logger.exception("Error while setting up platform %s", self.platform_name) + logger.exception( + "Error while setting up %s platform for %s", + self.platform_name, + self.domain, + ) return False finally: warn_task.cancel() @@ -449,6 +454,33 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None + async def async_extract_from_service(self, service_call, expand_group=True): + """Extract all known and available entities from a service call. + + Will return an empty list if entities specified but unknown. + + This method must be run in the event loop. + """ + return await service.async_extract_entities( + self.hass, self.entities.values(), service_call, expand_group + ) + + @callback + def async_register_entity_service(self, name, schema, func, required_features=None): + """Register an entity service.""" + if isinstance(schema, dict): + schema = cv.make_entity_service_schema(schema) + + async def handle_service(call): + """Handle the service.""" + await service.entity_service_call( + self.hass, [self], func, call, required_features + ) + + self.hass.services.async_register( + self.platform_name, name, handle_service, schema + ) + async def _update_entity_states(self, now: datetime) -> None: """Update the states of all the polling entities. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 66d1bb94f60..635f7feba13 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -454,6 +454,18 @@ def async_entries_for_device( ] +@callback +def async_entries_for_config_entry( + registry: EntityRegistry, config_entry_id: str +) -> List[RegistryEntry]: + """Return entries that match a config entry.""" + return [ + entry + for entry in registry.entities.values() + if entry.config_entry_id == config_entry_id + ] + + async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" return { diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b3c8af6f50c..74faca6a1d2 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -225,7 +225,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @callback @bind_hass def async_track_point_in_utc_time( - hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime + hass: HomeAssistant, action: Callable[..., Any], point_in_time: datetime ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 0c3dbe96bc5..e8f0f9a6bac 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -115,6 +115,7 @@ class RestoreStateData: self.last_states: Dict[str, StoredState] = {} self.entity_ids: Set[str] = set() + @callback def async_get_stored_states(self) -> List[StoredState]: """Get the set of states which should be stored. diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 837a561181d..1cac4679d82 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -156,12 +156,70 @@ class Script: ACTION_DEVICE_AUTOMATION: self._async_device_automation, ACTION_ACTIVATE_SCENE: self._async_activate_scene, } + self._referenced_entities: Optional[Set[str]] = None + self._referenced_devices: Optional[Set[str]] = None @property def is_running(self) -> bool: """Return true if script is on.""" return self._cur != -1 + @property + def referenced_devices(self): + """Return a set of referenced devices.""" + if self._referenced_devices is not None: + return self._referenced_devices + + referenced = set() + + for step in self.sequence: + action = _determine_action(step) + + if action == ACTION_CHECK_CONDITION: + referenced |= condition.async_extract_devices(step) + + elif action == ACTION_DEVICE_AUTOMATION: + referenced.add(step[CONF_DEVICE_ID]) + + self._referenced_devices = referenced + return referenced + + @property + def referenced_entities(self): + """Return a set of referenced entities.""" + if self._referenced_entities is not None: + return self._referenced_entities + + referenced = set() + + for step in self.sequence: + action = _determine_action(step) + + if action == ACTION_CALL_SERVICE: + data = step.get(service.CONF_SERVICE_DATA) + if not data: + continue + + entity_ids = data.get(ATTR_ENTITY_ID) + + if entity_ids is None: + continue + + if isinstance(entity_ids, str): + entity_ids = [entity_ids] + + for entity_id in entity_ids: + referenced.add(entity_id) + + elif action == ACTION_CHECK_CONDITION: + referenced |= condition.async_extract_entities(step) + + elif action == ACTION_ACTIVATE_SCENE: + referenced.add(step[CONF_SCENE]) + + self._referenced_entities = referenced + return referenced + def run(self, variables=None, context=None): """Run script.""" asyncio.run_coroutine_threadsafe( @@ -214,6 +272,7 @@ class Script: """Stop running script.""" run_callback_threadsafe(self.hass.loop, self.async_stop).result() + @callback def async_stop(self) -> None: """Stop running script.""" if self._cur == -1: diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 16fabe251af..b30cab3fbd4 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,6 +1,6 @@ """Service calling related helpers.""" import asyncio -from functools import wraps +from functools import partial, wraps import logging from typing import Callable @@ -108,13 +108,31 @@ def extract_entity_ids(hass, service_call, expand_group=True): ).result() +@bind_hass +async def async_extract_entities(hass, entities, service_call, expand_group=True): + """Extract a list of entity objects from a service call. + + Will convert group entity ids to the entity ids it represents. + """ + data_ent_id = service_call.data.get(ATTR_ENTITY_ID) + + if data_ent_id == ENTITY_MATCH_ALL: + return [entity for entity in entities if entity.available] + + entity_ids = await async_extract_entity_ids(hass, service_call, expand_group) + + return [ + entity + for entity in entities + if entity.available and entity.entity_id in entity_ids + ] + + @bind_hass async def async_extract_entity_ids(hass, service_call, expand_group=True): """Extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. - - Async friendly. """ entity_ids = service_call.data.get(ATTR_ENTITY_ID) area_ids = service_call.data.get(ATTR_AREA_ID) @@ -244,9 +262,7 @@ def async_set_service_schema(hass, domain, service, schema): @bind_hass -async def entity_service_call( - hass, platforms, func, call, service_name="", required_features=None -): +async def entity_service_call(hass, platforms, func, call, required_features=None): """Handle an entity service call. Calls all platforms simultaneously. @@ -267,7 +283,11 @@ async def entity_service_call( # If the service function is a string, we'll pass it the service call data if isinstance(func, str): - data = {key: val for key, val in call.data.items() if key != ATTR_ENTITY_ID} + data = { + key: val + for key, val in call.data.items() + if key not in cv.ENTITY_SERVICE_FIELDS + } # If the service function is not a string, we pass the service call else: data = call @@ -307,6 +327,7 @@ async def entity_service_call( for platform in platforms: platform_entities = [] for entity in platform.entities.values(): + if entity.entity_id not in entity_ids: continue @@ -323,7 +344,7 @@ async def entity_service_call( tasks = [ _handle_service_platform_call( - func, data, entities, call.context, required_features + hass, func, data, entities, call.context, required_features ) for platform, entities in zip(platforms, platforms_entities) ] @@ -336,7 +357,7 @@ async def entity_service_call( async def _handle_service_platform_call( - func, data, entities, context, required_features + hass, func, data, entities, context, required_features ): """Handle a function call.""" tasks = [] @@ -354,9 +375,21 @@ async def _handle_service_platform_call( entity.async_set_context(context) if isinstance(func, str): - await getattr(entity, func)(**data) + result = hass.async_add_job(partial(getattr(entity, func), **data)) else: - await func(entity, data) + result = hass.async_add_job(func, entity, data) + + # Guard because callback functions do not return a task when passed to async_add_job. + if result is not None: + result = await result + + if asyncio.iscoroutine(result): + _LOGGER.error( + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", + func, + entity.entity_id, + ) + await result if entity.should_poll: tasks.append(entity.async_update_ha_state(True)) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py new file mode 100644 index 00000000000..dc990637e31 --- /dev/null +++ b/homeassistant/helpers/update_coordinator.py @@ -0,0 +1,135 @@ +"""Helpers to help coordinate updates.""" +import asyncio +from datetime import datetime, timedelta +import logging +from time import monotonic +from typing import Any, Awaitable, Callable, List, Optional + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + +from .debounce import Debouncer + + +class UpdateFailed(Exception): + """Raised when an update has failed.""" + + +class DataUpdateCoordinator: + """Class to manage fetching data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + name: str, + update_method: Callable[[], Awaitable], + update_interval: timedelta, + request_refresh_debouncer: Debouncer, + ): + """Initialize global data updater.""" + self.hass = hass + self.logger = logger + self.name = name + self.update_method = update_method + self.update_interval = update_interval + + self.data: Optional[Any] = None + + self._listeners: List[CALLBACK_TYPE] = [] + self._unsub_refresh: Optional[CALLBACK_TYPE] = None + self._request_refresh_task: Optional[asyncio.TimerHandle] = None + self.failed_last_update = False + self._debounced_refresh = request_refresh_debouncer + request_refresh_debouncer.function = self._async_do_refresh + + @callback + def async_add_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Listen for data updates.""" + schedule_refresh = not self._listeners + + self._listeners.append(update_callback) + + # This is the first listener, set up interval. + if schedule_refresh: + self._schedule_refresh() + + @callback + def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: + """Remove data update.""" + self._listeners.remove(update_callback) + + if not self._listeners and self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + async def async_refresh(self) -> None: + """Refresh the data.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + await self._async_do_refresh() + + @callback + def _schedule_refresh(self) -> None: + """Schedule a refresh.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + self._unsub_refresh = async_track_point_in_utc_time( + self.hass, self._handle_refresh_interval, utcnow() + self.update_interval + ) + + async def _handle_refresh_interval(self, _now: datetime) -> None: + """Handle a refresh interval occurrence.""" + self._unsub_refresh = None + await self._async_do_refresh() + + async def async_request_refresh(self) -> None: + """Request a refresh. + + Refresh will wait a bit to see if it can batch them. + """ + await self._debounced_refresh.async_call() + + async def _async_do_refresh(self) -> None: + """Time to update.""" + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + + self._debounced_refresh.async_cancel() + + try: + start = monotonic() + self.data = await self.update_method() + + except UpdateFailed as err: + if not self.failed_last_update: + self.logger.error("Error fetching %s data: %s", self.name, err) + self.failed_last_update = True + + except Exception as err: # pylint: disable=broad-except + self.failed_last_update = True + self.logger.exception( + "Unexpected error fetching %s data: %s", self.name, err + ) + + else: + if self.failed_last_update: + self.failed_last_update = False + self.logger.info("Fetching %s data recovered") + + finally: + self.logger.debug( + "Finished fetching %s data in %.3f seconds", + self.name, + monotonic() - start, + ) + self._schedule_refresh() + + for update_callback in self._listeners: + update_callback() diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 7a15410f96a..0f69f4600b2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -244,6 +244,16 @@ class Integration: """Return Integration Quality Scale.""" return cast(str, self.manifest.get("quality_scale")) + @property + def logo(self) -> Optional[str]: + """Return Integration Logo.""" + return cast(str, self.manifest.get("logo")) + + @property + def icon(self) -> Optional[str]: + """Return Integration Icon.""" + return cast(str, self.manifest.get("icon")) + @property def is_built_in(self) -> bool: """Test if package is a built-in integration.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c3e7dcea692..41e00c5d8de 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,17 +11,17 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200108.2 -importlib-metadata==1.3.0 +home-assistant-frontend==20200130.1 +importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2.0 +pyyaml==5.3 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.12 +sqlalchemy==1.3.13 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.24.4 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 857164a5634..c7ef1e93781 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,5 +1,4 @@ """Script to check the configuration file.""" - import argparse from collections import OrderedDict from glob import glob @@ -16,7 +15,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.0.2",) +REQUIREMENTS = ("colorlog==4.1.0",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index 0b5d1104997..10026127511 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -32,13 +32,14 @@ def run(args): os.makedirs(config_dir) hass = HomeAssistant() - config_path = hass.loop.run_until_complete(async_run(hass, config_dir)) + hass.config.config_dir = config_dir + config_path = hass.loop.run_until_complete(async_run(hass)) print("Configuration file:", config_path) return 0 -async def async_run(hass, config_dir): +async def async_run(hass): """Make sure config exists.""" - path = await config_util.async_ensure_config_exists(hass, config_dir) + path = await config_util.async_ensure_config_exists(hass) await hass.async_stop(force=True) return path diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f97e5ae2363..f62228b28f5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -55,7 +55,7 @@ async def _async_process_dependencies( """Ensure all dependencies are set up.""" blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST] - if blacklisted and name != "default_config": + if blacklisted and name not in ("default_config", "safe_mode"): _LOGGER.error( "Unable to set up dependencies of %s: " "found blacklisted dependencies: %s", @@ -134,8 +134,8 @@ async def _async_setup_component( # So we do it before validating config to catch these errors. try: component = integration.get_component() - except ImportError: - log_error("Unable to import component", integration.documentation) + except ImportError as err: + log_error(f"Unable to import component: {err}", integration.documentation) return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Setup failed for %s: unknown error", domain) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a617eba50f9..7bda3728612 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -174,6 +174,10 @@ async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]] except (aiohttp.ClientError, ValueError): return None + # ipapi allows 30k free requests/month. Some users exhaust those. + if raw_info.get("latitude") == "Sign up to access": + return None + return { "ip": raw_info.get("ip"), "country_code": raw_info.get("country"), diff --git a/pylintrc b/pylintrc index 0ffbb138f9e..fcc38ec0734 100644 --- a/pylintrc +++ b/pylintrc @@ -3,6 +3,7 @@ ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 +load-plugins=pylint_strict_informational persistent=no [BASIC] diff --git a/requirements_all.txt b/requirements_all.txt index 053bfbd9cf7..0f44cb7705c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,14 +5,14 @@ async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.11.28 -importlib-metadata==1.3.0 +importlib-metadata==1.4.0 jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2.0 +pyyaml==5.3 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 @@ -34,7 +34,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.6.0 +HAP-python==2.7.0 # homeassistant.components.mastodon Mastodon.py==1.5.0 @@ -105,7 +105,7 @@ WazeRouteCalculator==0.12 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.7 +abodepy==0.17.0 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 @@ -141,7 +141,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.4 +aiobotocore==0.11.1 # homeassistant.components.dnsip aiodns==2.0.0 @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.5 +aiokef==0.2.6 # homeassistant.components.lifx aiolifx==0.6.7 @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.7 +aiopylgtv==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -208,7 +208,7 @@ airly==0.0.2 aladdin_connect==0.3 # homeassistant.components.alarmdecoder -alarmdecoder==1.13.9 +alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage alpha_vantage==2.1.2 @@ -220,7 +220,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.38 +androidtv==0.0.39 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -235,7 +235,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.2 +apprise==0.8.3 # homeassistant.components.aprs aprslib==0.6.46 @@ -299,7 +299,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.12.0 +bellows-homeassistant==0.13.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -366,9 +366,6 @@ caldav==0.6.1 # homeassistant.components.cisco_mobility_express ciscomobilityexpress==0.3.3 -# homeassistant.components.ciscospark -ciscosparkapi==0.4.2 - # homeassistant.components.cppm_tracker clearpasspy==1.0.2 @@ -385,7 +382,7 @@ coinbase==2.1.0 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.0.2 +colorlog==4.1.0 # homeassistant.components.concord232 concord232==0.15 @@ -450,7 +447,7 @@ doorbirdpy==2.0.8 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.12 +dsmr_parser==0.18 # homeassistant.components.dweet dweepy==0.3.0 @@ -477,7 +474,7 @@ eliqonline==1.2.2 elkm1-lib==0.7.15 # homeassistant.components.emulated_roku -emulated_roku==0.2.0 +emulated_roku==0.2.1 # homeassistant.components.enocean enocean==0.50 @@ -486,7 +483,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.0.31 +env_canada==0.0.34 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -549,7 +546,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -# fritzconnection==0.8.4 +fritzconnection==1.2.0 # homeassistant.components.fritzdect fritzhome==1.0.4 @@ -557,6 +554,9 @@ fritzhome==1.0.4 # homeassistant.components.google_translate gTTS-token==1.1.3 +# homeassistant.components.garmin_connect +garminconnect==0.1.8 + # homeassistant.components.gearbest gearbest_parser==1.0.7 @@ -640,7 +640,7 @@ ha-ffmpeg==2.0 ha-philipsjs==0.0.8 # homeassistant.components.plugwise -haanna==0.13.5 +haanna==0.14.1 # homeassistant.components.habitica habitipy==0.2.0 @@ -670,7 +670,7 @@ hikvision==0.4 hkavr==0.0.5 # homeassistant.components.hlk_sw16 -hlk-sw16==0.0.7 +hlk-sw16==0.0.8 # homeassistant.components.pi_hole hole==0.5.0 @@ -679,10 +679,10 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.2 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.7 +homeassistant-pyozw==0.1.8 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -698,7 +698,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.4 +huawei-lte-api==1.4.7 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -709,7 +709,7 @@ hydrawiser==0.1.1 # i2csense==0.0.4 # homeassistant.components.iaqualink -iaqualink==0.3.0 +iaqualink==0.3.1 # homeassistant.components.watson_tts ibm-watson==4.0.1 @@ -721,7 +721,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.4.0 +ihcsdk==2.5.0 # homeassistant.components.incomfort incomfort-client==0.4.0 @@ -730,7 +730,7 @@ incomfort-client==0.4.0 influxdb==5.2.3 # homeassistant.components.insteon -insteonplm==0.16.5 +insteonplm==0.16.6 # homeassistant.components.iperf3 iperf3==0.1.11 @@ -769,7 +769,7 @@ konnected==0.1.5 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.6.0 +libpurecool==0.6.1 # homeassistant.components.foscam libpyfoscam==1.0 @@ -778,7 +778,7 @@ libpyfoscam==1.0 libpyvivotek==0.4.0 # homeassistant.components.mikrotik -librouteros==2.3.0 +librouteros==3.0.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 @@ -905,7 +905,7 @@ niko-home-control==0.2.1 niluclient==0.1.2 # homeassistant.components.nederlandse_spoorwegen -nsapi==2.7.4 +nsapi==3.0.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -953,7 +953,7 @@ openwebifpy==3.1.1 openwrt-luci-rpc==1.1.2 # homeassistant.components.oru -oru==0.1.9 +oru==0.1.11 # homeassistant.components.orvibo orvibo==1.1.1 @@ -1024,11 +1024,8 @@ pmsensor==0.4 # homeassistant.components.pocketcasts pocketcasts==0.1 -# homeassistant.components.postnl -postnl_api==1.2.2 - # homeassistant.components.reddit -praw==6.4.0 +praw==6.5.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 @@ -1046,7 +1043,7 @@ prometheus_client==0.7.1 protobuf==3.6.1 # homeassistant.components.proxmoxve -proxmoxer==1.0.3 +proxmoxer==1.0.4 # homeassistant.components.systemmonitor psutil==5.6.7 @@ -1122,7 +1119,7 @@ py_nextbusnext==0.1.4 pyads==3.0.7 # homeassistant.components.hisense_aehw4a1 -pyaehw4a1==0.3.1 +pyaehw4a1==0.3.4 # homeassistant.components.aftership pyaftership==0.1.2 @@ -1140,7 +1137,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.1.0 +pyatmo==3.2.2 # homeassistant.components.atome pyatome==0.1.1 @@ -1170,7 +1167,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==4.0.1 +pychromecast==4.1.1 # homeassistant.components.cmus pycmus==0.1.1 @@ -1188,13 +1185,13 @@ pycsspeechtts==1.0.3 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==1.6.1 +pydaikin==1.6.2 # homeassistant.components.danfoss_air pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==68 +pydeconz==69 # homeassistant.components.delijn pydelijn==0.5.1 @@ -1276,7 +1273,7 @@ pygogogate2==0.1.1 pygtfs==0.1.5 # homeassistant.components.version -pyhaversion==3.1.0 +pyhaversion==3.2.0 # homeassistant.components.heos pyheos==0.6.0 @@ -1297,13 +1294,13 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.1 +pyicloud==0.9.2 # homeassistant.components.intesishome -pyintesishome==1.5 +pyintesishome==1.6 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 @@ -1327,7 +1324,7 @@ pykwb==0.0.8 pylacrosse==0.4.0 # homeassistant.components.lastfm -pylast==3.1.0 +pylast==3.2.0 # homeassistant.components.launch_library pylaunches==0.2.0 @@ -1402,7 +1399,7 @@ pynuki==1.3.3 pynut2==2.1.2 # homeassistant.components.nws -pynws==0.8.1 +pynws==0.10.1 # homeassistant.components.nx584 pynx584==0.4 @@ -1419,6 +1416,9 @@ pyombi==0.1.10 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opnsense +pyopnsense==0.2.0 + # homeassistant.components.opple pyoppleio==1.0.5 @@ -1433,9 +1433,6 @@ pyotgw==0.5b1 # homeassistant.components.otp pyotp==2.3.0 -# homeassistant.components.owlet -pyowlet==1.0.3 - # homeassistant.components.openweathermap pyowm==2.10.0 @@ -1455,7 +1452,7 @@ pypjlink2==1.2.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.4 +pyps4-2ndscreen==1.0.6 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -1500,7 +1497,7 @@ pysher==1.0.1 pysignalclirestapi==0.1.4 # homeassistant.components.sma -pysma==0.3.4 +pysma==0.3.5 # homeassistant.components.smartthings pysmartapp==0.3.2 @@ -1571,6 +1568,9 @@ python-family-hub-local==0.0.2 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.sms +# python-gammu==2.12 + # homeassistant.components.gc100 python-gc100==1.0.3a @@ -1623,7 +1623,7 @@ python-sochain-api==0.0.2 python-songpal==0.11.2 # homeassistant.components.synologydsm -python-synology==0.3.0 +python-synology==0.4.0 # homeassistant.components.tado python-tado==0.2.9 @@ -1638,7 +1638,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.35 +python-velbus==2.0.36 # homeassistant.components.vlc python-vlc==1.1.2 @@ -1678,7 +1678,7 @@ pytradfri[async]==6.4.0 pytrafikverket==0.1.5.9 # homeassistant.components.ubee -pyubee==0.7 +pyubee==0.8 # homeassistant.components.uptimerobot pyuptimerobot==0.0.5 @@ -1696,7 +1696,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.0.12 +pyvizio==0.1.4 # homeassistant.components.velux pyvlx==0.2.12 @@ -1750,7 +1750,7 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.50 +rflink==0.0.51 # homeassistant.components.ring ring_doorbell==0.6.0 @@ -1813,13 +1813,16 @@ sentry-sdk==0.13.5 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.21.1 +shodan==1.21.3 + +# homeassistant.components.sighthound +simplehound==0.3 # homeassistant.components.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.3.6 +simplisafe-python==6.1.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 @@ -1874,6 +1877,9 @@ somecomfort==0.5.2 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 +# homeassistant.components.marytts +speak2mary==1.4.0 + # homeassistant.components.speedtestdotnet speedtest-cli==2.1.2 @@ -1884,11 +1890,11 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy-homeassistant==2.4.4.dev1 +spotipy==2.7.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.12 +sqlalchemy==1.3.13 # homeassistant.components.starline starline==0.1.3 @@ -1957,7 +1963,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.2.3 +teslajsonpy==0.3.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 @@ -2035,7 +2041,6 @@ vtjp==0.1.14 vultr==0.1.2 # homeassistant.components.panasonic_viera -# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==1.1.6 @@ -2104,7 +2109,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.01.01 +youtube_dl==2020.01.24 # homeassistant.components.zengge zengge==0.2 @@ -2113,7 +2118,7 @@ zengge==0.2 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.31 +zha-quirks==0.0.32 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2125,13 +2130,13 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.12.0 +zigpy-homeassistant==0.13.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.8.0 +zigpy-xbee-homeassistant==0.9.0 # homeassistant.components.zha -zigpy-zigate==0.5.0 +zigpy-zigate==0.5.1 # homeassistant.components.zoneminder zm-py==0.4.0 diff --git a/requirements_test.txt b/requirements_test.txt index f4fb13a417c..b8ab2c23040 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,13 +7,14 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 mypy==0.761 -pre-commit==1.21.0 +pre-commit==2.0.0 pylint==2.4.4 astroid==2.3.3 +pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.3.2 +pytest==5.3.4 requests_mock==1.7.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f52cc125f92..33d81e4b24d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.6.0 +HAP-python==2.7.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks @@ -26,7 +26,7 @@ RtmAPI==0.7.2 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.7 +abodepy==0.17.0 # homeassistant.components.androidtv adb-shell==0.1.1 @@ -53,7 +53,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.4 +aiobotocore==0.11.1 # homeassistant.components.esphome aioesphomeapi==2.6.1 @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.7 +aiopylgtv==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -87,13 +87,13 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.38 +androidtv==0.0.39 # homeassistant.components.apns apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.2 +apprise==0.8.3 # homeassistant.components.aprs aprslib==0.6.46 @@ -112,7 +112,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.12.0 +bellows-homeassistant==0.13.1 # homeassistant.components.bom bomradarloop==0.1.3 @@ -133,7 +133,7 @@ caldav==0.6.1 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.0.2 +colorlog==4.1.0 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart @@ -162,7 +162,7 @@ directpy==0.5 distro==1.4.0 # homeassistant.components.dsmr -dsmr_parser==0.12 +dsmr_parser==0.18 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 @@ -171,7 +171,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.emulated_roku -emulated_roku==0.2.0 +emulated_roku==0.2.1 # homeassistant.components.season ephem==3.7.7.0 @@ -185,6 +185,9 @@ foobot_async==0.3.1 # homeassistant.components.google_translate gTTS-token==1.1.3 +# homeassistant.components.garmin_connect +garminconnect==0.1.8 + # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 @@ -244,10 +247,10 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.2 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.7 +homeassistant-pyozw==0.1.8 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -260,10 +263,10 @@ homematicip==0.10.15 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.4 +huawei-lte-api==1.4.7 # homeassistant.components.iaqualink -iaqualink==0.3.0 +iaqualink==0.3.1 # homeassistant.components.influxdb influxdb==5.2.3 @@ -278,7 +281,10 @@ keyring==20.0.0 keyrings.alt==3.4.0 # homeassistant.components.dyson -libpurecool==0.6.0 +libpurecool==0.6.1 + +# homeassistant.components.mikrotik +librouteros==3.0.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 @@ -350,7 +356,7 @@ plexwebsocket==0.0.6 pmsensor==0.4 # homeassistant.components.reddit -praw==6.4.0 +praw==6.5.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 @@ -390,7 +396,7 @@ pyRFXtrx==0.25 py_nextbusnext==0.1.4 # homeassistant.components.hisense_aehw4a1 -pyaehw4a1==0.3.1 +pyaehw4a1==0.3.4 # homeassistant.components.almond pyalmond==0.0.2 @@ -398,6 +404,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.netatmo +pyatmo==3.2.2 + # homeassistant.components.blackbird pyblackbird==0.5 @@ -405,16 +414,16 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==4.0.1 +pychromecast==4.1.1 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==1.6.1 +pydaikin==1.6.2 # homeassistant.components.deconz -pydeconz==68 +pydeconz==69 # homeassistant.components.zwave pydispatcher==2.0.5 @@ -436,7 +445,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.version -pyhaversion==3.1.0 +pyhaversion==3.2.0 # homeassistant.components.heos pyheos==0.6.0 @@ -445,10 +454,10 @@ pyheos==0.6.0 pyhomematic==0.1.63 # homeassistant.components.icloud -pyicloud==0.9.1 +pyicloud==0.9.2 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 @@ -478,7 +487,7 @@ pymodbus==1.5.2 pymonoprice==0.3 # homeassistant.components.nws -pynws==0.8.1 +pynws==0.10.1 # homeassistant.components.nx584 pynx584==0.4 @@ -486,6 +495,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opnsense +pyopnsense==0.2.0 + # homeassistant.components.opentherm_gw pyotgw==0.5b1 @@ -498,13 +510,13 @@ pyotp==2.3.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.4 +pyps4-2ndscreen==1.0.6 # homeassistant.components.qwikswitch pyqwikswitch==0.93 # homeassistant.components.sma -pysma==0.3.4 +pysma==0.3.5 # homeassistant.components.smartthings pysmartapp==0.3.2 @@ -537,7 +549,7 @@ python-miio==0.4.8 python-nest==4.1.0 # homeassistant.components.velbus -python-velbus==2.0.35 +python-velbus==2.0.36 # homeassistant.components.awair python_awair==0.0.4 @@ -554,6 +566,9 @@ pyvera==0.3.7 # homeassistant.components.vesync pyvesync==1.1.0 +# homeassistant.components.vizio +pyvizio==0.1.4 + # homeassistant.components.html5 pywebpush==1.9.2 @@ -564,7 +579,7 @@ regenmaschine==1.5.1 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.50 +rflink==0.0.51 # homeassistant.components.ring ring_doorbell==0.6.0 @@ -578,8 +593,11 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.sentry sentry-sdk==0.13.5 +# homeassistant.components.sighthound +simplehound==0.3 + # homeassistant.components.simplisafe -simplisafe-python==5.3.6 +simplisafe-python==6.1.0 # homeassistant.components.sleepiq sleepyq==0.7 @@ -593,9 +611,15 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.marytts +speak2mary==1.4.0 + +# homeassistant.components.spotify +spotipy==2.7.1 + # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.12 +sqlalchemy==1.3.13 # homeassistant.components.starline starline==0.1.3 @@ -616,7 +640,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.tesla -teslajsonpy==0.2.3 +teslajsonpy==0.3.0 # homeassistant.components.toon toonapilib==3.2.4 @@ -643,7 +667,6 @@ vsure==1.5.4 vultr==0.1.2 # homeassistant.components.panasonic_viera -# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==1.1.6 @@ -673,16 +696,16 @@ yahooweather==0.10 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.31 +zha-quirks==0.0.32 # homeassistant.components.zha zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.12.0 +zigpy-homeassistant==0.13.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.8.0 +zigpy-xbee-homeassistant==0.9.0 # homeassistant.components.zha -zigpy-zigate==0.5.0 +zigpy-zigate==0.5.1 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 7a20962ff7c..8af2cbb6123 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,4 +5,4 @@ black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 isort==v4.3.21 -pydocstyle==5.0.1 +pydocstyle==5.0.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e64427baf71..3b30bf04363 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -25,7 +25,6 @@ COMMENT_REQUIREMENTS = ( "envirophat", "evdev", "face_recognition", - "fritzconnection", "i2csense", "opencv-python-headless", "py_noaa", @@ -34,6 +33,7 @@ COMMENT_REQUIREMENTS = ( "PySwitchbot", "pySwitchmate", "python-eq3bt", + "python-gammu", "python-lirc", "pyuserinput", "raspihats", diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 99e32e57f43..a1541ef68c9 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -1,10 +1,12 @@ """Validate manifests.""" import pathlib import sys +from time import monotonic from . import ( codeowners, config_flow, + coverage, dependencies, json, manifest, @@ -18,6 +20,7 @@ PLUGINS = [ json, codeowners, config_flow, + coverage, dependencies, manifest, services, @@ -48,7 +51,17 @@ def main(): integrations = Integration.load_dir(pathlib.Path("homeassistant/components")) for plugin in PLUGINS: - plugin.validate(integrations, config) + try: + start = monotonic() + print(f"Validating {plugin.__name__.split('.')[-1]}...", end="", flush=True) + plugin.validate(integrations, config) + print(" done in {:.2f}s".format(monotonic() - start)) + except RuntimeError as err: + print() + print() + print("Error!") + print(err) + return 1 # When we generate, all errors that are fixable will be ignored, # as generating them will be fixed. diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py new file mode 100644 index 00000000000..dc94b36e6d8 --- /dev/null +++ b/script/hassfest/coverage.py @@ -0,0 +1,50 @@ +"""Validate coverage files.""" +from pathlib import Path +from typing import Dict + +from .model import Config, Integration + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate coverage.""" + coverage_path = config.root / ".coveragerc" + + not_found = [] + checking = False + + with coverage_path.open("rt") as fp: + for line in fp: + line = line.strip() + + if not line or line.startswith("#"): + continue + + if not checking: + if line == "omit =": + checking = True + continue + + # Finished + if line == "[report]": + break + + path = Path(line) + + # Discard wildcard + while "*" in path.name: + path = path.parent + + if not path.exists(): + not_found.append(line) + + if not not_found: + return + + errors = [] + + if not_found: + errors.append( + f".coveragerc references files that don't exist: {', '.join(not_found)}." + ) + + raise RuntimeError(" ".join(errors)) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index e6bd6551786..7852953dc92 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -1,11 +1,17 @@ """Manifest validation.""" from typing import Dict +from urllib.parse import urlparse import voluptuous as vol from voluptuous.humanize import humanize_error from .model import Integration +DOCUMENTATION_URL_SCHEMA = "https" +DOCUMENTATION_URL_HOST = "www.home-assistant.io" +DOCUMENTATION_URL_PATH_PREFIX = "/integrations/" +DOCUMENTATION_URL_EXCEPTIONS = ["https://www.home-assistant.io/hassio"] + SUPPORTED_QUALITY_SCALES = [ "gold", "internal", @@ -13,6 +19,25 @@ SUPPORTED_QUALITY_SCALES = [ "silver", ] + +def documentation_url(value: str) -> str: + """Validate that a documentation url has the correct path and domain.""" + if value in DOCUMENTATION_URL_EXCEPTIONS: + return value + + parsed_url = urlparse(value) + if not parsed_url.scheme == DOCUMENTATION_URL_SCHEMA: + raise vol.Invalid("Documentation url is not prefixed with https") + if not parsed_url.netloc == DOCUMENTATION_URL_HOST: + raise vol.Invalid("Documentation url not hosted at www.home-assistant.io") + if not parsed_url.path.startswith(DOCUMENTATION_URL_PATH_PREFIX): + raise vol.Invalid( + "Documentation url does not begin with www.home-assistant.io/integrations" + ) + + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -23,12 +48,16 @@ MANIFEST_SCHEMA = vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), - vol.Required("documentation"): str, + vol.Required("documentation"): vol.All( + vol.Url(), documentation_url # pylint: disable=no-value-for-parameter + ), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Required("requirements"): [str], vol.Required("dependencies"): [str], vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], + vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter + vol.Optional("icon"): vol.Url(), # pylint: disable=no-value-for-parameter } ) diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 5ee2076ecf4..a32e07f4aac 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -43,6 +43,7 @@ def generate_and_validate(integrations: Dict[str, Integration]): content = fp.read() if ( " async_step_ssdp" not in content + and "AbstractOAuth2FlowHandler" not in content and "register_discovery_flow" not in content ): integration.add_error("ssdp", "Config flow has no async_step_ssdp") diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 2a1bb936871..7e1b7eae727 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -41,10 +41,12 @@ def generate_and_validate(integrations: Dict[str, Integration]): with open(str(integration.path / "config_flow.py")) as fp: content = fp.read() uses_discovery_flow = "register_discovery_flow" in content + uses_oauth2_flow = "AbstractOAuth2FlowHandler" in content if ( service_types and not uses_discovery_flow + and not uses_oauth2_flow and " async_step_zeroconf" not in content ): integration.add_error( @@ -55,6 +57,7 @@ def generate_and_validate(integrations: Dict[str, Integration]): if ( homekit_models and not uses_discovery_flow + and not uses_oauth2_flow and " async_step_homekit" not in content ): integration.add_error( diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 94ac009fd9c..8fa2814e54f 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -72,20 +72,20 @@ def main(): if args.template != "integration": generate.generate(args.template, info) - pipe_null = "" if args.develop else "> /dev/null" + pipe_null = {} if args.develop else {"stdout": subprocess.DEVNULL} print("Running hassfest to pick up new information.") - subprocess.run(f"python -m script.hassfest {pipe_null}", shell=True) + subprocess.run(["python", "-m", "script.hassfest"], **pipe_null) print() print("Running gen_requirements_all to pick up new information.") - subprocess.run(f"python -m script.gen_requirements_all {pipe_null}", shell=True) + subprocess.run(["python", "-m", "script.gen_requirements_all"], **pipe_null) print() if args.develop: print("Running tests") print(f"$ pytest -vvv tests/components/{info.domain}") - subprocess.run(f"pytest -vvv tests/components/{info.domain}", shell=True) + subprocess.run(["pytest", "-vvv", "tests/components/{info.domain}"]) print() docs.print_relevant_docs(args.template, info) diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index e2452b5324d..bfa1d65c257 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -13,22 +13,49 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) +class PlaceholderHub: + """Placeholder class to make tests pass. + + TODO Remove this placeholder class and replace with things from your PyPI package. + """ + + def __init__(self, host): + """Initialize.""" + self.host = host + + async def authenticate(self, username, password) -> bool: + """Test if we can authenticate with the host.""" + return True + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ # TODO validate the data can be used to set up a connection. + + # If your PyPI package is not built with async, pass your methods + # to the executor: + # await hass.async_add_executor_job( + # your_validate_func, data["username"], data["password"] + # ) + + hub = PlaceholderHub(data["host"]) + + if not await hub.authenticate(data["username"], data["password"]): + raise InvalidAuth + # If you cannot connect: # throw CannotConnect # If the authentication is wrong: # InvalidAuth - # Return some info we want to store in the config entry. + # Return info that you want to store in the config entry. return {"title": "Name of the device"} -class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index b68adc897bb..3d829b5cc32 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,12 +1,10 @@ """Test the NEW_NAME config flow.""" -from unittest.mock import patch +from asynctest import patch from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN -from tests.common import mock_coro - async def test_form(hass): """Test we get the form.""" @@ -18,13 +16,12 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", - return_value=mock_coro({"title": "Test Title"}), + "homeassistant.components.NEW_DOMAIN.config_flow.PlaceholderHub.authenticate", + return_value=True, ), patch( - "homeassistant.components.NEW_DOMAIN.async_setup", return_value=mock_coro(True) + "homeassistant.components.NEW_DOMAIN.async_setup", return_value=True ) as mock_setup, patch( - "homeassistant.components.NEW_DOMAIN.async_setup_entry", - return_value=mock_coro(True), + "homeassistant.components.NEW_DOMAIN.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -36,7 +33,7 @@ async def test_form(hass): ) assert result2["type"] == "create_entry" - assert result2["title"] == "Test Title" + assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "1.1.1.1", "username": "test-username", @@ -54,7 +51,7 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + "homeassistant.components.NEW_DOMAIN.config_flow.PlaceholderHub.authenticate", side_effect=InvalidAuth, ): result2 = await hass.config_entries.flow.async_configure( @@ -77,7 +74,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + "homeassistant.components.NEW_DOMAIN.config_flow.PlaceholderHub.authenticate", side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 1414636474d..cb2489e4279 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -67,6 +67,7 @@ async def async_get_conditions( return conditions +@callback def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: @@ -78,6 +79,7 @@ def async_condition_from_config( else: state = STATE_OFF + @callback def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: """Test if an entity is a certain state.""" return condition.state(hass, config[ATTR_ENTITY_ID], state) diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index d58957030dc..34217a61f9e 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index 0ea584f474d..82540566318 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/setup.py b/setup.py index cf84577b558..521b9f2678c 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ REQUIRES = [ "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.11.28", - "importlib-metadata==1.3.0", + "importlib-metadata==1.4.0", "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. @@ -46,7 +46,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.2.0", + "pyyaml==5.3", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", diff --git a/tests/bandit.yaml b/tests/bandit.yaml index 79812cba56f..ebd284eaa01 100644 --- a/tests/bandit.yaml +++ b/tests/bandit.yaml @@ -1,6 +1,9 @@ # https://bandit.readthedocs.io/en/latest/config.html tests: + - B108 + - B306 + - B307 - B313 - B314 - B315 @@ -9,3 +12,6 @@ tests: - B318 - B319 - B320 + - B325 + - B602 + - B604 diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index a5ca3981a5a..1f14e96ed37 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -5,8 +5,8 @@ from airly.exceptions import AirlyError from asynctest import patch from homeassistant import data_entry_flow -from homeassistant.components.airly import config_flow from homeassistant.components.airly.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from tests.common import MockConfigEntry, load_fixture @@ -21,13 +21,12 @@ CONFIG = { async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.AirlyFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["step_id"] == SOURCE_USER async def test_invalid_api_key(hass): @@ -36,10 +35,10 @@ async def test_invalid_api_key(hass): "airly._private._RequestsHandler.get", side_effect=AirlyError(403, {"message": "Invalid authentication credentials"}), ): - flow = config_flow.AirlyFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=CONFIG) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) assert result["errors"] == {"base": "auth"} @@ -50,10 +49,10 @@ async def test_invalid_location(hass): "airly._private._RequestsHandler.get", return_value=json.loads(load_fixture("airly_no_station.json")), ): - flow = config_flow.AirlyFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=CONFIG) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) assert result["errors"] == {"base": "wrong_location"} @@ -65,13 +64,16 @@ async def test_duplicate_error(hass): "airly._private._RequestsHandler.get", return_value=json.loads(load_fixture("airly_valid_station.json")), ): - MockConfigEntry(domain=DOMAIN, data=CONFIG).add_to_hass(hass) - flow = config_flow.AirlyFlowHandler() - flow.hass = hass + MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass( + hass + ) - result = await flow.async_step_user(user_input=CONFIG) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - assert result["errors"] == {CONF_NAME: "name_exists"} + assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_create_entry(hass): @@ -81,10 +83,10 @@ async def test_create_entry(hass): "airly._private._RequestsHandler.get", return_value=json.loads(load_fixture("airly_valid_station.json")), ): - flow = config_flow.AirlyFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=CONFIG) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 9c086e1fc50..f8f4f5f4697 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -315,12 +315,22 @@ async def test_report_fan_speed_state(hass): hass.states.async_set( "fan.off", "off", - {"friendly_name": "Off fan", "speed": "off", "supported_features": 1}, + { + "friendly_name": "Off fan", + "speed": "off", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) hass.states.async_set( "fan.low_speed", "on", - {"friendly_name": "Low speed fan", "speed": "low", "supported_features": 1}, + { + "friendly_name": "Low speed fan", + "speed": "low", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) hass.states.async_set( "fan.medium_speed", @@ -329,12 +339,18 @@ async def test_report_fan_speed_state(hass): "friendly_name": "Medium speed fan", "speed": "medium", "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], }, ) hass.states.async_set( "fan.high_speed", "on", - {"friendly_name": "High speed fan", "speed": "high", "supported_features": 1}, + { + "friendly_name": "High speed fan", + "speed": "high", + "supported_features": 1, + "speed_list": ["off", "low", "medium", "high"], + }, ) properties = await reported_properties(hass, "fan.off") @@ -361,25 +377,24 @@ async def test_report_fan_speed_state(hass): async def test_report_fan_oscillating(hass): """Test ToggleController reports fan oscillating correctly.""" hass.states.async_set( - "fan.off", + "fan.oscillating_off", "off", - {"friendly_name": "Off fan", "speed": "off", "supported_features": 3}, + {"friendly_name": "fan oscillating off", "supported_features": 2}, ) hass.states.async_set( - "fan.low_speed", + "fan.oscillating_on", "on", { - "friendly_name": "Low speed fan", - "speed": "low", + "friendly_name": "Fan oscillating on", "oscillating": True, - "supported_features": 3, + "supported_features": 2, }, ) - properties = await reported_properties(hass, "fan.off") + properties = await reported_properties(hass, "fan.oscillating_off") properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF") - properties = await reported_properties(hass, "fan.low_speed") + properties = await reported_properties(hass, "fan.oscillating_on") properties.assert_equal("Alexa.ToggleController", "toggleState", "ON") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 51b1ed83982..ca6b1e1ccb6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +import homeassistant.components.vacuum as vacuum from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter @@ -130,7 +131,7 @@ async def discovery_test(device, hass, expected_endpoints=1): def get_capability(capabilities, capability_name, instance=None): """Search a set of capabilities for a specific one.""" for capability in capabilities: - if instance and capability["instance"] == instance: + if instance and capability.get("instance") == instance: return capability if not instance and capability["interface"] == capability_name: return capability @@ -497,11 +498,11 @@ async def test_variable_fan(hass): async def test_oscillating_fan(hass): - """Test oscillating fan discovery.""" + """Test oscillating fan with ToggleController.""" device = ( "fan.test_3", "off", - {"friendly_name": "Test fan 3", "supported_features": 3}, + {"friendly_name": "Test fan 3", "supported_features": 2}, ) appliance = await discovery_test(device, hass) @@ -510,10 +511,7 @@ async def test_oscillating_fan(hass): assert appliance["friendlyName"] == "Test fan 3" capabilities = assert_endpoint_capabilities( appliance, - "Alexa.PercentageController", "Alexa.PowerController", - "Alexa.PowerLevelController", - "Alexa.RangeController", "Alexa.ToggleController", "Alexa.EndpointHealth", "Alexa", @@ -558,13 +556,13 @@ async def test_oscillating_fan(hass): async def test_direction_fan(hass): - """Test direction fan discovery.""" + """Test fan direction with modeController.""" device = ( "fan.test_4", "on", { "friendly_name": "Test fan 4", - "supported_features": 5, + "supported_features": 4, "direction": "forward", }, ) @@ -575,10 +573,7 @@ async def test_direction_fan(hass): assert appliance["friendlyName"] == "Test fan 4" capabilities = assert_endpoint_capabilities( appliance, - "Alexa.PercentageController", "Alexa.PowerController", - "Alexa.PowerLevelController", - "Alexa.RangeController", "Alexa.ModeController", "Alexa.EndpointHealth", "Alexa", @@ -667,17 +662,14 @@ async def test_direction_fan(hass): async def test_fan_range(hass): - """Test fan discovery with range controller. - - This one has variable speed. - """ + """Test fan speed with rangeController.""" device = ( "fan.test_5", "off", { "friendly_name": "Test fan 5", "supported_features": 1, - "speed_list": ["low", "medium", "high"], + "speed_list": ["off", "low", "medium", "high", "turbo", 5, "warp_speed"], "speed": "medium", }, ) @@ -701,24 +693,106 @@ async def test_fan_range(hass): assert range_capability is not None assert range_capability["instance"] == "fan.speed" + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 6 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "off", "locale": "en-US"}} + ] + }, + } in presets + + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "low", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}}, + ] + }, + } in presets + + assert { + "rangeValue": 2, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "medium", "locale": "en-US"}} + ] + }, + } in presets + + assert {"rangeValue": 5} not in presets + + assert { + "rangeValue": 6, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "warp speed", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}}, + ] + }, + } in presets + call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", "fan#test_5", "fan.set_speed", hass, - payload={"rangeValue": "1"}, + payload={"rangeValue": 1}, instance="fan.speed", ) assert call.data["speed"] == "low" + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": 5}, + instance="fan.speed", + ) + assert call.data["speed"] == 5 + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": 6}, + instance="fan.speed", + ) + assert call.data["speed"] == "warp_speed" + await assert_range_changes( hass, - [("low", "-1"), ("high", "1"), ("medium", "0")], + [ + ("low", -1, False), + ("high", 1, False), + ("medium", 0, False), + ("warp_speed", 99, False), + ], "Alexa.RangeController", "AdjustRangeValue", "fan#test_5", - False, "fan.set_speed", "speed", instance="fan.speed", @@ -733,7 +807,7 @@ async def test_fan_range_off(hass): { "friendly_name": "Test fan 6", "supported_features": 1, - "speed_list": ["low", "medium", "high"], + "speed_list": ["off", "low", "medium", "high"], "speed": "high", }, ) @@ -745,18 +819,17 @@ async def test_fan_range_off(hass): "fan#test_6", "fan.turn_off", hass, - payload={"rangeValue": "0"}, + payload={"rangeValue": 0}, instance="fan.speed", ) assert call.data["speed"] == "off" await assert_range_changes( hass, - [("off", "-3")], + [("off", -3, False), ("off", -99, False)], "Alexa.RangeController", "AdjustRangeValue", "fan#test_6", - False, "fan.turn_off", "speed", instance="fan.speed", @@ -1395,7 +1468,11 @@ async def test_cover_position_range(hass): assert appliance["friendlyName"] == "Test cover range" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -1463,7 +1540,7 @@ async def test_cover_position_range(hass): "cover#test_range", "cover.set_cover_position", hass, - payload={"rangeValue": "50"}, + payload={"rangeValue": 50}, instance="cover.position", ) assert call.data["position"] == 50 @@ -1474,7 +1551,7 @@ async def test_cover_position_range(hass): "cover#test_range", "cover.close_cover", hass, - payload={"rangeValue": "0"}, + payload={"rangeValue": 0}, instance="cover.position", ) properties = msg["context"]["properties"][0] @@ -1488,7 +1565,7 @@ async def test_cover_position_range(hass): "cover#test_range", "cover.open_cover", hass, - payload={"rangeValue": "100"}, + payload={"rangeValue": 100}, instance="cover.position", ) properties = msg["context"]["properties"][0] @@ -1496,13 +1573,40 @@ async def test_cover_position_range(hass): assert properties["namespace"] == "Alexa.RangeController" assert properties["value"] == 100 - await assert_range_changes( - hass, - [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_range", + "cover.open_cover", + hass, + payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_range", + "cover.close_cover", + hass, + payload={"rangeValueDelta": -99, "rangeValueDeltaDefault": False}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + await assert_range_changes( + hass, + [(25, -5, False), (35, 5, False), (50, 1, True), (10, -1, True)], "Alexa.RangeController", "AdjustRangeValue", "cover#test_range", - False, "cover.set_cover_position", "position", instance="cover.position", @@ -1529,21 +1633,13 @@ async def assert_percentage_changes( async def assert_range_changes( - hass, - adjustments, - namespace, - name, - endpoint, - delta_default, - service, - changed_parameter, - instance, + hass, adjustments, namespace, name, endpoint, service, changed_parameter, instance ): """Assert an API request making range changes works. AdjustRangeValue are examples of such requests. """ - for result_range, adjustment in adjustments: + for result_range, adjustment, delta_default in adjustments: payload = { "rangeValueDelta": adjustment, "rangeValueDeltaDefault": delta_default, @@ -2268,6 +2364,7 @@ async def test_alarm_control_panel_disarmed(hass): "code_arm_required": False, "code_format": "number", "code": "1234", + "supported_features": 31, }, ) appliance = await discovery_test(device, hass) @@ -2284,6 +2381,10 @@ async def test_alarm_control_panel_disarmed(hass): assert security_panel_capability is not None configuration = security_panel_capability["configuration"] assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"] + assert {"value": "DISARMED"} in configuration["supportedArmStates"] + assert {"value": "ARMED_STAY"} in configuration["supportedArmStates"] + assert {"value": "ARMED_AWAY"} in configuration["supportedArmStates"] + assert {"value": "ARMED_NIGHT"} in configuration["supportedArmStates"] properties = await reported_properties(hass, "alarm_control_panel#test_1") properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") @@ -2335,6 +2436,7 @@ async def test_alarm_control_panel_armed(hass): "code_arm_required": False, "code_format": "FORMAT_NUMBER", "code": "1234", + "supported_features": 3, }, ) appliance = await discovery_test(device, hass) @@ -2373,11 +2475,15 @@ async def test_alarm_control_panel_armed(hass): async def test_alarm_control_panel_code_arm_required(hass): - """Test alarm_control_panel with code_arm_required discovery.""" + """Test alarm_control_panel with code_arm_required not in discovery.""" device = ( "alarm_control_panel.test_3", "disarmed", - {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, + { + "friendly_name": "Test Alarm Control Panel 3", + "code_arm_required": True, + "supported_features": 3, + }, ) await discovery_test(device, hass, expected_endpoints=0) @@ -2389,7 +2495,7 @@ async def test_range_unsupported_domain(hass): context = Context() request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test") - request["directive"]["payload"] = {"rangeValue": "1"} + request["directive"]["payload"] = {"rangeValue": 1} request["directive"]["header"]["instance"] = "switch.speed" msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) @@ -2420,6 +2526,36 @@ async def test_mode_unsupported_domain(hass): assert msg["payload"]["type"] == "INVALID_DIRECTIVE" +async def test_cover(hass): + """Test garage cover discovery and powerController.""" + device = ( + "cover.test", + "off", + { + "friendly_name": "Test cover", + "supported_features": 3, + "device_class": "garage", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test" + assert appliance["displayCategories"][0] == "GARAGE_DOOR" + assert appliance["friendlyName"] == "Test cover" + + assert_endpoint_capabilities( + appliance, + "Alexa.ModeController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + "Alexa", + ) + + await assert_power_controller_works( + "cover#test", "cover.open_cover", "cover.close_cover", hass + ) + + async def test_cover_position_mode(hass): """Test cover discovery and position using modeController.""" device = ( @@ -2438,7 +2574,11 @@ async def test_cover_position_mode(hass): assert appliance["friendlyName"] == "Test cover mode" capabilities = assert_endpoint_capabilities( - appliance, "Alexa", "Alexa.ModeController", "Alexa.EndpointHealth" + appliance, + "Alexa.PowerController", + "Alexa.ModeController", + "Alexa.EndpointHealth", + "Alexa", ) mode_capability = get_capability(capabilities, "Alexa.ModeController") @@ -2657,7 +2797,11 @@ async def test_cover_tilt_position_range(hass): assert appliance["friendlyName"] == "Test cover tilt range" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -2679,10 +2823,10 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.set_cover_tilt_position", hass, - payload={"rangeValue": "50"}, + payload={"rangeValue": 50}, instance="cover.tilt", ) - assert call.data["position"] == 50 + assert call.data["tilt_position"] == 50 call, msg = await assert_request_calls_service( "Alexa.RangeController", @@ -2690,7 +2834,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.close_cover_tilt", hass, - payload={"rangeValue": "0"}, + payload={"rangeValue": 0}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] @@ -2704,7 +2848,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.open_cover_tilt", hass, - payload={"rangeValue": "100"}, + payload={"rangeValue": 100}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] @@ -2712,13 +2856,40 @@ async def test_cover_tilt_position_range(hass): assert properties["namespace"] == "Alexa.RangeController" assert properties["value"] == 100 - await assert_range_changes( - hass, - [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_tilt_range", + "cover.open_cover_tilt", + hass, + payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, + instance="cover.tilt", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_tilt_range", + "cover.close_cover_tilt", + hass, + payload={"rangeValueDelta": -99, "rangeValueDeltaDefault": False}, + instance="cover.tilt", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + await assert_range_changes( + hass, + [(25, -5, False), (35, 5, False), (50, 1, True), (10, -1, True)], "Alexa.RangeController", "AdjustRangeValue", "cover#test_tilt_range", - False, "cover.set_cover_tilt_position", "tilt_position", instance="cover.tilt", @@ -2745,7 +2916,11 @@ async def test_cover_semantics_position_and_tilt(hass): assert appliance["friendlyName"] == "Test cover semantics" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + "Alexa", ) # Assert for Position Semantics @@ -2869,18 +3044,17 @@ async def test_input_number(hass): "input_number#test_slider", "input_number.set_value", hass, - payload={"rangeValue": "10"}, + payload={"rangeValue": 10}, instance="input_number.value", ) assert call.data["value"] == 10 await assert_range_changes( hass, - [(25, "-5"), (35, "5"), (-20, "-100"), (35, "100")], + [(25, -5, False), (35, 5, False), (-20, -100, False), (35, 100, False)], "Alexa.RangeController", "AdjustRangeValue", "input_number#test_slider", - False, "input_number.set_value", "value", instance="input_number.value", @@ -2955,18 +3129,23 @@ async def test_input_number_float(hass): "input_number#test_slider_float", "input_number.set_value", hass, - payload={"rangeValue": "0.333"}, + payload={"rangeValue": 0.333}, instance="input_number.value", ) assert call.data["value"] == 0.333 await assert_range_changes( hass, - [(0.4, "-0.1"), (0.6, "0.1"), (0, "-100"), (1, "100"), (0.51, "0.01")], + [ + (0.4, -0.1, False), + (0.6, 0.1, False), + (0, -100, False), + (1, 100, False), + (0.51, 0.01, False), + ], "Alexa.RangeController", "AdjustRangeValue", "input_number#test_slider_float", - False, "input_number.set_value", "value", instance="input_number.value", @@ -3095,3 +3274,268 @@ async def test_media_player_eq_bands_not_supported(hass): assert msg["header"]["name"] == "ErrorResponse" assert msg["header"]["namespace"] == "Alexa" assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_timer_hold(hass): + """Test timer hold.""" + device = ( + "timer.laundry", + "active", + {"friendly_name": "Laundry", "duration": "00:01:00", "remaining": "00:50:00"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "timer#laundry" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Laundry" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.TimeHoldController" + ) + + time_hold_capability = get_capability(capabilities, "Alexa.TimeHoldController") + assert time_hold_capability is not None + configuration = time_hold_capability["configuration"] + assert configuration["allowRemoteResume"] is True + + await assert_request_calls_service( + "Alexa.TimeHoldController", "Hold", "timer#laundry", "timer.pause", hass + ) + + +async def test_timer_resume(hass): + """Test timer resume.""" + device = ( + "timer.laundry", + "paused", + {"friendly_name": "Laundry", "duration": "00:01:00", "remaining": "00:50:00"}, + ) + await discovery_test(device, hass) + + await assert_request_calls_service( + "Alexa.TimeHoldController", "Resume", "timer#laundry", "timer.start", hass + ) + + +async def test_vacuum_discovery(hass): + """Test vacuum discovery.""" + device = ( + "vacuum.test_1", + "docked", + { + "friendly_name": "Test vacuum 1", + "supported_features": vacuum.SUPPORT_TURN_ON + | vacuum.SUPPORT_TURN_OFF + | vacuum.SUPPORT_START + | vacuum.SUPPORT_STOP + | vacuum.SUPPORT_PAUSE, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "vacuum#test_1" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test vacuum 1" + + assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.TimeHoldController", + "Alexa.EndpointHealth", + "Alexa", + ) + + +async def test_vacuum_fan_speed(hass): + """Test vacuum fan speed with rangeController.""" + device = ( + "vacuum.test_2", + "cleaning", + { + "friendly_name": "Test vacuum 2", + "supported_features": vacuum.SUPPORT_TURN_ON + | vacuum.SUPPORT_TURN_OFF + | vacuum.SUPPORT_START + | vacuum.SUPPORT_STOP + | vacuum.SUPPORT_PAUSE + | vacuum.SUPPORT_FAN_SPEED, + "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], + "fan_speed": "medium", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "vacuum#test_2" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test vacuum 2" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.TimeHoldController", + "Alexa.EndpointHealth", + "Alexa", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "vacuum.fan_speed" + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 5 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "off", "locale": "en-US"}} + ] + }, + } in presets + + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "low", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}}, + ] + }, + } in presets + + assert { + "rangeValue": 2, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "medium", "locale": "en-US"}} + ] + }, + } in presets + + assert { + "rangeValue": 5, + "presetResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "super sucker", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}}, + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "vacuum#test_2", + "vacuum.set_fan_speed", + hass, + payload={"rangeValue": 1}, + instance="vacuum.fan_speed", + ) + assert call.data["fan_speed"] == "low" + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "vacuum#test_2", + "vacuum.set_fan_speed", + hass, + payload={"rangeValue": 5}, + instance="vacuum.fan_speed", + ) + assert call.data["fan_speed"] == "super_sucker" + + await assert_range_changes( + hass, + [ + ("low", -1, False), + ("high", 1, False), + ("medium", 0, False), + ("super_sucker", 99, False), + ], + "Alexa.RangeController", + "AdjustRangeValue", + "vacuum#test_2", + "vacuum.set_fan_speed", + "fan_speed", + instance="vacuum.fan_speed", + ) + + +async def test_vacuum_pause(hass): + """Test vacuum pause with TimeHoldController.""" + device = ( + "vacuum.test_3", + "cleaning", + { + "friendly_name": "Test vacuum 3", + "supported_features": vacuum.SUPPORT_TURN_ON + | vacuum.SUPPORT_TURN_OFF + | vacuum.SUPPORT_START + | vacuum.SUPPORT_STOP + | vacuum.SUPPORT_PAUSE + | vacuum.SUPPORT_FAN_SPEED, + "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], + "fan_speed": "medium", + }, + ) + appliance = await discovery_test(device, hass) + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.RangeController", + "Alexa.TimeHoldController", + "Alexa.EndpointHealth", + "Alexa", + ) + + time_hold_capability = get_capability(capabilities, "Alexa.TimeHoldController") + assert time_hold_capability is not None + configuration = time_hold_capability["configuration"] + assert configuration["allowRemoteResume"] is True + + await assert_request_calls_service( + "Alexa.TimeHoldController", "Hold", "vacuum#test_3", "vacuum.start_pause", hass + ) + + +async def test_vacuum_resume(hass): + """Test vacuum resume with TimeHoldController.""" + device = ( + "vacuum.test_4", + "docked", + { + "friendly_name": "Test vacuum 4", + "supported_features": vacuum.SUPPORT_TURN_ON + | vacuum.SUPPORT_TURN_OFF + | vacuum.SUPPORT_START + | vacuum.SUPPORT_STOP + | vacuum.SUPPORT_PAUSE + | vacuum.SUPPORT_FAN_SPEED, + "fan_speed_list": ["off", "low", "medium", "high", "turbo", "super_sucker"], + "fan_speed": "medium", + }, + ) + await discovery_test(device, hass) + + await assert_request_calls_service( + "Alexa.TimeHoldController", + "Resume", + "vacuum#test_4", + "vacuum.start_pause", + hass, + ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 4cd2a18a833..42a8ab48279 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -49,6 +49,7 @@ async def test_report_state_instance(hass, aioclient_mock): "friendly_name": "Test fan", "supported_features": 3, "speed": "off", + "speed_list": ["off", "low", "high"], "oscillating": False, }, ) @@ -62,6 +63,7 @@ async def test_report_state_instance(hass, aioclient_mock): "friendly_name": "Test fan", "supported_features": 3, "speed": "high", + "speed_list": ["off", "low", "high"], "oscillating": True, }, ) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 0aaa870c57b..82287877eaf 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.androidtv.media_player import ( CONF_ADB_SERVER_IP, CONF_ADBKEY, CONF_APPS, + CONF_EXCLUDE_UNNAMED_APPS, KEYS, SERVICE_ADB_COMMAND, SERVICE_DOWNLOAD, @@ -28,6 +29,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PLATFORM, + SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_PLAYING, @@ -299,7 +301,11 @@ async def test_setup_with_adbkey(hass): async def _test_sources(hass, config0): """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" config = config0.copy() - config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + config[DOMAIN][CONF_APPS] = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": "", + } patch_key, entity_id = _setup(config) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ @@ -315,14 +321,16 @@ async def _test_sources(hass, config0): patch_update = patchers.patch_androidtv_update( "playing", "com.app.test1", - ["com.app.test1", "com.app.test2"], + ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], "hdmi", False, 1, ) else: patch_update = patchers.patch_firetv_update( - "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], ) with patch_update: @@ -331,20 +339,22 @@ async def _test_sources(hass, config0): assert state is not None assert state.state == STATE_PLAYING assert state.attributes["source"] == "TEST 1" - assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] + assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": patch_update = patchers.patch_androidtv_update( "playing", "com.app.test2", - ["com.app.test2", "com.app.test1"], + ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], "hdmi", True, 0, ) else: patch_update = patchers.patch_firetv_update( - "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], ) with patch_update: @@ -353,7 +363,7 @@ async def _test_sources(hass, config0): assert state is not None assert state.state == STATE_PLAYING assert state.attributes["source"] == "com.app.test2" - assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] return True @@ -368,10 +378,82 @@ async def test_firetv_sources(hass): assert await _test_sources(hass, CONFIG_FIRETV_ADB_SERVER) +async def _test_exclude_sources(hass, config0, expected_sources): + """Test that sources (i.e., apps) are handled correctly when the `exclude_unnamed_apps` config parameter is provided.""" + config = config0.copy() + config[DOMAIN][CONF_APPS] = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": "", + } + patch_key, entity_id = _setup(config) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + [ + "com.app.test1", + "com.app.test2", + "com.app.test3", + "com.app.test4", + "com.app.test5", + ], + "hdmi", + False, + 1, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", + "com.app.test1", + [ + "com.app.test1", + "com.app.test2", + "com.app.test3", + "com.app.test4", + "com.app.test5", + ], + ) + + with patch_update: + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "TEST 1" + assert sorted(state.attributes["source_list"]) == expected_sources + + return True + + +async def test_androidtv_exclude_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Android TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" + config = CONFIG_ANDROIDTV_ADB_SERVER.copy() + config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + assert await _test_exclude_sources(hass, config, ["TEST 1"]) + + +async def test_firetv_exclude_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_EXCLUDE_UNNAMED_APPS] = True + assert await _test_exclude_sources(hass, config, ["TEST 1"]) + + async def _test_select_source(hass, config0, source, expected_arg, method_patch): """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" config = config0.copy() - config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1", "com.app.test3": None} patch_key, entity_id = _setup(config) with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ @@ -428,6 +510,17 @@ async def test_androidtv_select_source_launch_app_id_no_name(hass): ) +async def test_androidtv_select_source_launch_app_hidden(hass): + """Test that an app can be launched using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test3", + "com.app.test3", + patchers.PATCH_LAUNCH_APP, + ) + + async def test_androidtv_select_source_stop_app_id(hass): """Test that an app can be stopped using its app ID.""" assert await _test_select_source( @@ -461,6 +554,17 @@ async def test_androidtv_select_source_stop_app_id_no_name(hass): ) +async def test_androidtv_select_source_stop_app_hidden(hass): + """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test3", + "com.app.test3", + patchers.PATCH_STOP_APP, + ) + + async def test_firetv_select_source_launch_app_id(hass): """Test that an app can be launched using its app ID.""" assert await _test_select_source( @@ -494,6 +598,17 @@ async def test_firetv_select_source_launch_app_id_no_name(hass): ) +async def test_firetv_select_source_launch_app_hidden(hass): + """Test that an app can be launched using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test3", + "com.app.test3", + patchers.PATCH_LAUNCH_APP, + ) + + async def test_firetv_select_source_stop_app_id(hass): """Test that an app can be stopped using its app ID.""" assert await _test_select_source( @@ -527,6 +642,17 @@ async def test_firetv_select_source_stop_app_id_no_name(hass): ) +async def test_firetv_select_source_stop_hidden(hass): + """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test3", + "com.app.test3", + patchers.PATCH_STOP_APP, + ) + + async def _test_setup_fail(hass, config): """Test that the entity is not created when the ADB connection is not established.""" patch_key, entity_id = _setup(config) @@ -820,3 +946,25 @@ async def test_upload(hass): blocking=True, ) patch_push.assert_called_with(local_path, device_path) + + +async def test_androidtv_volume_set(hass): + """Test setting the volume for an Android TV device.""" + patch_key, entity_id = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + + with patch( + "androidtv.basetv.BaseTV.set_volume_level", return_value=0.5 + ) as patch_set_volume_level: + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: entity_id, "volume_level": 0.5}, + blocking=True, + ) + + patch_set_volume_level.assert_called_with(0.5) diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py index 1683e1951a0..f90c1e2bcca 100644 --- a/tests/components/aurora/test_binary_sensor.py +++ b/tests/components/aurora/test_binary_sensor.py @@ -74,11 +74,11 @@ class TestAuroraSensorSetUp(unittest.TestCase): entities.append(entity) config = {"name": "Test", "forecast_threshold": 1} - self.hass.config.longitude = 5 - self.hass.config.latitude = 5 + self.hass.config.longitude = 18.987 + self.hass.config.latitude = 69.648 aurora.setup_platform(self.hass, config, mock_add_entities) aurora_component = entities[0] - assert aurora_component.aurora_data.visibility_level == "5" + assert aurora_component.aurora_data.visibility_level == "16" assert aurora_component.is_on diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index 26d19d6fa47..340bb6c1e95 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -11,7 +11,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index 05e30458ef3..5daca51d0a1 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -11,7 +11,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b0196fdfe60..c27a0262a4e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest import homeassistant.components.automation as automation +from homeassistant.components.automation import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -29,7 +30,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -496,14 +497,13 @@ async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_onl } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) - await hass.async_block_till_done() - await common.async_reload(hass, Context(user_id=hass_admin_user.id)) - await hass.async_block_till_done() - # De-flake ?! + with pytest.raises(Unauthorized): + await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) await hass.async_block_till_done() + await common.async_reload(hass, Context(user_id=hass_admin_user.id)) + await hass.async_block_till_done() + # De-flake ?! + await hass.async_block_till_done() assert hass.states.get("automation.hello") is None assert hass.states.get("automation.bye") is not None @@ -551,9 +551,8 @@ async def test_reload_config_when_invalid_config(hass, calls): autospec=True, return_value={automation.DOMAIN: "not valid"}, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) - await hass.async_block_till_done() + await common.async_reload(hass) + await hass.async_block_till_done() assert hass.states.get("automation.hello") is None @@ -590,9 +589,8 @@ async def test_reload_config_handles_load_fails(hass, calls): "homeassistant.config.load_yaml_config_file", side_effect=HomeAssistantError("bla"), ): - with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) - await hass.async_block_till_done() + await common.async_reload(hass) + await hass.async_block_till_done() assert hass.states.get("automation.hello") is not None @@ -925,3 +923,102 @@ async def test_automation_restore_last_triggered_with_initial_state(hass): assert state assert state.state == STATE_ON assert state.attributes["last_triggered"] == time + + +async def test_extraction_functions(hass): + """Test extraction functions.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + { + "alias": "test1", + "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "condition": { + "condition": "state", + "entity_id": "light.condition_state", + "state": "on", + }, + "action": [ + { + "service": "test.script", + "data": {"entity_id": "light.in_both"}, + }, + { + "service": "test.script", + "data": {"entity_id": "light.in_first"}, + }, + { + "domain": "light", + "device_id": "device-in-both", + "entity_id": "light.bla", + "type": "turn_on", + }, + ], + }, + { + "alias": "test2", + "trigger": { + "platform": "device", + "domain": "light", + "type": "turned_on", + "entity_id": "light.trigger_2", + "device_id": "trigger-device-2", + }, + "condition": { + "condition": "device", + "device_id": "condition-device", + "domain": "light", + "type": "is_on", + "entity_id": "light.bla", + }, + "action": [ + { + "service": "test.script", + "data": {"entity_id": "light.in_both"}, + }, + { + "condition": "state", + "entity_id": "sensor.condition", + "state": "100", + }, + {"scene": "scene.hello"}, + { + "domain": "light", + "device_id": "device-in-both", + "entity_id": "light.bla", + "type": "turn_on", + }, + { + "domain": "light", + "device_id": "device-in-last", + "entity_id": "light.bla", + "type": "turn_on", + }, + ], + }, + ] + }, + ) + + assert set(automation.automations_with_entity(hass, "light.in_both")) == { + "automation.test1", + "automation.test2", + } + assert set(automation.entities_in_automation(hass, "automation.test1")) == { + "sensor.trigger_1", + "light.condition_state", + "light.in_both", + "light.in_first", + } + assert set(automation.automations_with_device(hass, "device-in-both")) == { + "automation.test1", + "automation.test2", + } + assert set(automation.devices_in_automation(hass, "automation.test2")) == { + "trigger-device-2", + "condition-device", + "device-in-both", + "device-in-last", + } diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 75fbc03a589..710b16d1b48 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -22,7 +22,7 @@ ENTITY_OTHER_SWITCH_NUMBER = 2 @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") @@ -54,7 +54,7 @@ def mock_lj(hass): mock_lj.on_switch_pressed.side_effect = on_switch_pressed mock_lj.on_switch_released.side_effect = on_switch_released - config = {"litejet": {"port": "/tmp/this_will_be_mocked"}} + config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}} assert hass.loop.run_until_complete( setup.async_setup_component(hass, litejet.DOMAIN, config) ) diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 9dbe93a7998..b8c369f5e63 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -17,7 +17,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index c6c1fd83184..17cb8e38136 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -20,7 +20,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index b6f9a50cf9d..9d4fa9a1100 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -20,7 +20,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index d9566b8f464..27e0d4f6965 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -20,7 +20,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index d84fd18fb6b..511f8a305e6 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -18,7 +18,7 @@ from tests.common import ( @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 70d647a1241..2c0574c3238 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -11,7 +11,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 44ad20e16f0..cb031486b6f 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -11,7 +11,7 @@ from tests.components.automation import common @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index ecf5e86bdad..1ac24e03702 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -36,7 +36,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 404def66491..6234d464f52 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -36,7 +36,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 5f81be7c1ea..3f07fca49f0 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -5,30 +5,23 @@ from asynctest import patch from brother import SnmpError, UnsupportedModel from homeassistant import data_entry_flow -from homeassistant.components.brother import config_flow from homeassistant.components.brother.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TYPE +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_TYPE -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture -CONFIG = { - CONF_HOST: "localhost", - CONF_NAME: "Printer", - CONF_TYPE: "laser", -} +CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["step_id"] == SOURCE_USER async def test_create_entry_with_hostname(hass): @@ -37,18 +30,14 @@ async def test_create_entry_with_hostname(hass): "brother.Brother._get_data", return_value=json.loads(load_fixture("brother_printer_data.json")), ): - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - flow.context = {} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] - assert result["data"][CONF_NAME] == CONFIG[CONF_NAME] + assert result["data"][CONF_TYPE] == CONFIG[CONF_TYPE] async def test_create_entry_with_ip_address(hass): @@ -57,31 +46,24 @@ async def test_create_entry_with_ip_address(hass): "brother.Brother._get_data", return_value=json.loads(load_fixture("brother_printer_data.json")), ): - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - flow.context = {} - result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "user"}, - data={CONF_NAME: "Name", CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}, + context={"source": SOURCE_USER}, + data={CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" - assert result["data"][CONF_NAME] == "Name" + assert result["data"][CONF_TYPE] == "laser" async def test_invalid_hostname(hass): """Test invalid hostname in user_input.""" - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": "user"}, - data={CONF_NAME: "Name", CONF_HOST: "invalid/hostname", CONF_TYPE: "laser"}, + context={"source": SOURCE_USER}, + data={CONF_HOST: "invalid/hostname", CONF_TYPE: "laser"}, ) assert result["errors"] == {CONF_HOST: "wrong_host"} @@ -90,11 +72,8 @@ async def test_invalid_hostname(hass): async def test_connection_error(hass): """Test connection to host error.""" with patch("brother.Brother._get_data", side_effect=ConnectionError()): - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["errors"] == {"base": "connection_error"} @@ -103,11 +82,8 @@ async def test_connection_error(hass): async def test_snmp_error(hass): """Test SNMP error.""" with patch("brother.Brother._get_data", side_effect=SnmpError("error")): - flow = config_flow.BrotherConfigFlow() - flow.hass = hass - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["errors"] == {"base": "snmp_error"} @@ -116,12 +92,116 @@ async def test_snmp_error(hass): async def test_unsupported_model_error(hass): """Test unsupported printer model error.""" with patch("brother.Brother._get_data", side_effect=UnsupportedModel("error")): - flow = config_flow.BrotherConfigFlow() - flow.hass = hass result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"}, data=CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unsupported_model" + + +async def test_device_exists_abort(hass): + """Test we abort config flow if Brother printer already configured.""" + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( + hass + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_no_data(hass): + """Test we abort if zeroconf provides no data.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_zeroconf_not_brother_printer_error(hass): + """Test we abort zeroconf flow if printer isn't Brother.""" + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "example.local.", "name": "Another Printer"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_brother_printer" + + +async def test_zeroconf_snmp_error(hass): + """Test we abort zeroconf flow on SNMP error.""" + with patch("brother.Brother._get_data", side_effect=SnmpError("error")): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "example.local.", "name": "Brother Printer"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_zeroconf_device_exists_abort(hass): + """Test we abort zeroconf flow if Brother printer already configured.""" + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "example.local.", "name": "Brother Printer"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_confirm_create_entry(hass): + """Test zeroconf confirmation and create config entry.""" + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "example.local.", "name": "Brother Printer"}, + ) + + assert result["step_id"] == "zeroconf_confirm" + assert result["description_placeholders"]["model"] == "HL-L2340DW" + assert result["description_placeholders"]["serial_number"] == "0123456789" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TYPE: "laser"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "HL-L2340DW 0123456789" + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 6faac295d54..0a3c67d97d3 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -10,11 +10,9 @@ from homeassistant.util import dt as dt_util EPSILON_DELTA = 0.0000000001 -def radar_map_url(dim: int = 512) -> str: +def radar_map_url(dim: int = 512, country_code: str = "NL") -> str: """Build map url, defaulting to 512 wide (as in component).""" - return ("https://api.buienradar.nl/image/1.0/RadarMapNL?w={dim}&h={dim}").format( - dim=dim - ) + return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w={dim}&h={dim}" async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): @@ -110,6 +108,29 @@ async def test_dimension(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 1 +async def test_belgium_country(aioclient_mock, hass, hass_client): + """Test that it actually adheres to another country like Belgium.""" + aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") + + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "buienradar", + "country_code": "BE", + } + }, + ) + + client = await hass_client() + + await client.get("/api/camera_proxy/camera.config_test") + + assert aioclient_mock.call_count == 1 + + async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): """Test that it does not cache a failure response.""" aioclient_mock.get(radar_map_url(), text="hello world", status=401) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index de48a1d48f3..23c04fe347e 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -6,24 +6,15 @@ from unittest.mock import PropertyMock, mock_open, patch import pytest -from homeassistant.components import camera, http +from homeassistant.components import camera from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_ENTITY_PICTURE, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - get_test_instance_port, - mock_coro, -) +from tests.common import mock_coro from tests.components.camera import common @@ -55,96 +46,53 @@ def setup_camera_prefs(hass): return common.mock_camera_prefs(hass, "camera.demo_camera") -class TestSetupCamera: - """Test class for setup camera.""" - - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component(self): - """Set up demo platform on camera component.""" - config = {camera.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, camera.DOMAIN): - setup_component(self.hass, camera.DOMAIN, config) +@pytest.fixture +async def image_mock_url(hass): + """Fixture for get_image tests.""" + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) -class TestGetImage: - """Test class for camera.""" +async def test_get_image_from_camera(hass, image_mock_url): + """Grab an image from camera entity.""" - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - setup_component( - self.hass, - http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: get_test_instance_port()}}, - ) - - config = {camera.DOMAIN: {"platform": "demo"}} - - setup_component(self.hass, camera.DOMAIN, config) - - state = self.hass.states.get("camera.demo_camera") - self.url = "{0}{1}".format( - self.hass.config.api.base_url, state.attributes.get(ATTR_ENTITY_PICTURE) - ) - - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - - @patch( + with patch( "homeassistant.components.demo.camera.DemoCamera.camera_image", autospec=True, return_value=b"Test", - ) - def test_get_image_from_camera(self, mock_camera): - """Grab an image from camera entity.""" - self.hass.start() + ) as mock_camera: + image = await camera.async_get_image(hass, "camera.demo_camera") - image = asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() + assert mock_camera.called + assert image.content == b"Test" - assert mock_camera.called - assert image.content == b"Test" - def test_get_image_without_exists_camera(self): - """Try to get image without exists camera.""" - with patch( - "homeassistant.helpers.entity_component.EntityComponent.get_entity", - return_value=None, - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() +async def test_get_image_without_exists_camera(hass, image_mock_url): + """Try to get image without exists camera.""" + with patch( + "homeassistant.helpers.entity_component.EntityComponent.get_entity", + return_value=None, + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") - def test_get_image_with_timeout(self): - """Try to get image with timeout.""" - with patch( - "homeassistant.components.camera.Camera.async_camera_image", - side_effect=asyncio.TimeoutError, - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() - def test_get_image_fails(self): - """Try to get image with timeout.""" - with patch( - "homeassistant.components.camera.Camera.async_camera_image", - return_value=mock_coro(None), - ), pytest.raises(HomeAssistantError): - asyncio.run_coroutine_threadsafe( - camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop - ).result() +async def test_get_image_with_timeout(hass, image_mock_url): + """Try to get image with timeout.""" + with patch( + "homeassistant.components.camera.Camera.async_camera_image", + side_effect=asyncio.TimeoutError, + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") + + +async def test_get_image_fails(hass, image_mock_url): + """Try to get image with timeout.""" + with patch( + "homeassistant.components.camera.Camera.async_camera_image", + return_value=mock_coro(None), + ), pytest.raises(HomeAssistantError): + await camera.async_get_image(hass, "camera.demo_camera") async def test_snapshot_service(hass, mock_camera): @@ -154,7 +102,7 @@ async def test_snapshot_service(hass, mock_camera): with patch( "homeassistant.components.camera.open", mopen, create=True ), patch.object(hass.config, "is_allowed_path", return_value=True): - common.async_snapshot(hass, "/tmp/bla") + common.async_snapshot(hass, "/test/snapshot.jpg") await hass.async_block_till_done() mock_write = mopen().write diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index c8aaf0e1967..431849ae761 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index d9bfd6d5ba4..eda215ebd0f 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 2338f0eaa1e..50402af2bd1 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -121,7 +121,7 @@ async def test_handler_google_actions(hass): device = devices[0] assert device["id"] == "switch.test" assert device["name"]["name"] == "Config name" - assert device["name"]["nicknames"] == ["Config alias"] + assert device["name"]["nicknames"] == ["Config name", "Config alias"] assert device["type"] == "action.devices.types.SWITCH" assert device["roomHint"] == "living room" diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 662ab0c969c..1cd76581bdc 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -44,7 +44,9 @@ def test_query_state_value(rs): result = rs._query_state_value("runme") assert "foo bar" == result assert mock_run.call_count == 1 - assert mock_run.call_args == mock.call("runme", shell=True) + assert mock_run.call_args == mock.call( + "runme", shell=True, # nosec # shell by design + ) async def test_state_value(hass): diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index b345a219d3f..45ffa1d08ec 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,6 +1,7 @@ """Test Automation config panel.""" import json -from unittest.mock import patch + +from asynctest import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config @@ -47,7 +48,7 @@ async def test_update_device_config(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.post( "/api/config/automation/config/moon", data=json.dumps({"trigger": [], "action": [], "condition": []}), @@ -89,11 +90,12 @@ async def test_bad_formatted_automations(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.post( "/api/config/automation/config/moon", data=json.dumps({"trigger": [], "action": [], "condition": []}), ) + await hass.async_block_till_done() assert resp.status == 200 result = await resp.json() @@ -107,8 +109,31 @@ async def test_bad_formatted_automations(hass, hass_client): async def test_delete_automation(hass, hass_client): """Test deleting an automation.""" + ent_reg = await hass.helpers.entity_registry.async_get_registry() + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": "test.automation"}, + }, + { + "id": "moon", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": "test.automation"}, + }, + ] + }, + ) + + assert len(ent_reg.entities) == 2 + with patch.object(config, "SECTIONS", ["automation"]): - await async_setup_component(hass, "config", {}) + assert await async_setup_component(hass, "config", {}) client = await hass_client() @@ -126,8 +151,9 @@ async def test_delete_automation(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.delete("/api/config/automation/config/sun") + await hass.async_block_till_done() assert resp.status == 200 result = await resp.json() @@ -135,3 +161,5 @@ async def test_delete_automation(hass, hass_client): assert len(written) == 1 assert written[0][0]["id"] == "moon" + + assert len(ent_reg.entities) == 1 diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index 45c1f40d4ad..d8c9ea19b70 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,6 +1,7 @@ """Test Customize config panel.""" import json -from unittest.mock import patch + +from asynctest import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config @@ -53,6 +54,8 @@ async def test_update_entity(hass, hass_client): hass.states.async_set("hello.world", "state", {"a": "b"}) with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write + ), patch( + "homeassistant.config.async_hass_config_yaml", return_value={}, ): resp = await client.post( "/api/config/customize/config/hello.world", @@ -60,6 +63,7 @@ async def test_update_entity(hass, hass_client): {"name": "Beer", "entities": ["light.top", "light.bottom"]} ), ) + await hass.async_block_till_done() assert resp.status == 200 result = await resp.json() diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 1b79f30a5b6..49d168e2796 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -61,6 +61,7 @@ async def test_update_device_config(hass, hass_client): {"name": "Beer", "entities": ["light.top", "light.bottom"]} ), ) + await hass.async_block_till_done() assert resp.status == 200 result = await resp.json() diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index b40c895b620..b51628f87ae 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,6 +1,7 @@ """Test Automation config panel.""" import json -from unittest.mock import patch + +from asynctest import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config @@ -29,7 +30,7 @@ async def test_update_scene(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.post( "/api/config/scene/config/light_off", data=json.dumps( @@ -86,7 +87,7 @@ async def test_bad_formatted_scene(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.post( "/api/config/scene/config/light_off", data=json.dumps( @@ -114,8 +115,23 @@ async def test_bad_formatted_scene(hass, hass_client): async def test_delete_scene(hass, hass_client): """Test deleting a scene.""" + ent_reg = await hass.helpers.entity_registry.async_get_registry() + + assert await async_setup_component( + hass, + "scene", + { + "scene": [ + {"id": "light_on", "name": "Light on", "entities": {}}, + {"id": "light_off", "name": "Light off", "entities": {}}, + ] + }, + ) + + assert len(ent_reg.entities) == 2 + with patch.object(config, "SECTIONS", ["scene"]): - await async_setup_component(hass, "config", {}) + assert await async_setup_component(hass, "config", {}) client = await hass_client() @@ -133,8 +149,9 @@ async def test_delete_scene(hass, hass_client): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write - ): + ), patch("homeassistant.config.async_hass_config_yaml", return_value={}): resp = await client.delete("/api/config/scene/config/light_on") + await hass.async_block_till_done() assert resp.status == 200 result = await resp.json() @@ -142,3 +159,5 @@ async def test_delete_scene(hass, hass_client): assert len(written) == 1 assert written[0][0]["id"] == "light_off" + + assert len(ent_reg.entities) == 1 diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 13c6fd8701f..b355053ad36 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -38,7 +38,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 3f82babc2ed..50738e2c549 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -38,7 +38,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 92dd95fc0c6..d79f80b96b0 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -205,7 +205,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): ) assert result["type"] == "abort" - assert result["reason"] == "updated_instance" + assert result["reason"] == "already_configured" assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" @@ -382,7 +382,7 @@ async def test_ssdp_discovery_update_configuration(hass): ) assert result["type"] == "abort" - assert result["reason"] == "updated_instance" + assert result["reason"] == "already_configured" assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" @@ -469,7 +469,7 @@ async def test_hassio_discovery_update_configuration(hass): ) assert result["type"] == "abort" - assert result["reason"] == "updated_instance" + assert result["reason"] == "already_configured" assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" assert gateway.config_entry.data[config_flow.CONF_PORT] == 8080 assert gateway.config_entry.data[config_flow.CONF_API_KEY] == "updated" diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 69584f630d6..349b359d9b8 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -101,7 +101,7 @@ async def test_deconz_events(hass): mock_listener = Mock() unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) - gateway.api.sensors["4"].async_update({"state": {"gesture": 2}}) + gateway.api.sensors["4"].async_update({"state": {"gesture": 0}}) await hass.async_block_till_done() assert len(mock_listener.mock_calls) == 1 @@ -109,7 +109,7 @@ async def test_deconz_events(hass): "id": "switch_4", "unique_id": "00:00:00:00:00:00:00:04", "event": 1000, - "gesture": 2, + "gesture": 0, } unsub() diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 8658eed3eb5..fbe3dd0bb32 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -59,6 +59,12 @@ LIGHTS = { "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, + "4": { + "name": "On off light", + "state": {"on": True, "reachable": True}, + "type": "On and Off light", + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, } @@ -91,18 +97,25 @@ async def test_lights_and_groups(hass): assert "light.light_group" in gateway.deconz_ids assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids - # 4 entities - assert len(hass.states.async_all()) == 4 + assert "light.on_off_light" in gateway.deconz_ids + + assert len(hass.states.async_all()) == 5 rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "on" assert rgb_light.attributes["brightness"] == 255 assert rgb_light.attributes["hs_color"] == (224.235, 100.0) assert rgb_light.attributes["is_deconz_group"] is False + assert rgb_light.attributes["supported_features"] == 61 tunable_white_light = hass.states.get("light.tunable_white_light") assert tunable_white_light.state == "on" assert tunable_white_light.attributes["color_temp"] == 2500 + assert tunable_white_light.attributes["supported_features"] == 2 + + on_off_light = hass.states.get("light.on_off_light") + assert on_off_light.state == "on" + assert on_off_light.attributes["supported_features"] == 0 light_group = hass.states.get("light.light_group") assert light_group.state == "on" @@ -219,7 +232,7 @@ async def test_disable_light_groups(hass): assert "light.empty_group" not in gateway.deconz_ids assert "light.on_off_switch" not in gateway.deconz_ids # 3 entities - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 4 rgb_light = hass.states.get("light.rgb_light") assert rgb_light is not None diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 533aaddf4eb..2229031fa90 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -104,11 +104,11 @@ async def test_sensors(hass): assert "sensor.switch_1_battery_level" not in gateway.deconz_ids assert "sensor.switch_2" not in gateway.deconz_ids assert "sensor.switch_2_battery_level" in gateway.deconz_ids - assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.daylight_sensor" not in gateway.deconz_ids assert "sensor.power_sensor" in gateway.deconz_ids assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 5 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -129,7 +129,7 @@ async def test_sensors(hass): assert switch_2_battery_level.state == "100" daylight_sensor = hass.states.get("sensor.daylight_sensor") - assert daylight_sensor.state == "dawn" + assert daylight_sensor is None power_sensor = hass.states.get("sensor.power_sensor") assert power_sensor.state == "6" @@ -182,11 +182,11 @@ async def test_allow_clip_sensors(hass): assert "sensor.switch_1_battery_level" not in gateway.deconz_ids assert "sensor.switch_2" not in gateway.deconz_ids assert "sensor.switch_2_battery_level" in gateway.deconz_ids - assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.daylight_sensor" not in gateway.deconz_ids assert "sensor.power_sensor" in gateway.deconz_ids assert "sensor.consumption_sensor" in gateway.deconz_ids assert "sensor.clip_light_level_sensor" in gateway.deconz_ids - assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_all()) == 6 light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" @@ -207,7 +207,7 @@ async def test_allow_clip_sensors(hass): assert switch_2_battery_level.state == "100" daylight_sensor = hass.states.get("sensor.daylight_sensor") - assert daylight_sensor.state == "dawn" + assert daylight_sensor is None power_sensor = hass.states.get("sensor.power_sensor") assert power_sensor.state == "6" diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 553e4f1f167..bb48a6243c6 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -38,6 +38,13 @@ SWITCHES = { "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:03-00", }, + "5": { + "id": "On off relay id", + "name": "On off relay", + "state": {"on": True, "reachable": True}, + "type": "On/Off light", + "uniqueid": "00:00:00:00:00:00:00:04-00", + }, } @@ -68,7 +75,8 @@ async def test_switches(hass): assert "switch.smart_plug" in gateway.deconz_ids assert "switch.warning_device" in gateway.deconz_ids assert "switch.unsupported_switch" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 4 + assert "switch.on_off_relay" in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 on_off_switch = hass.states.get("switch.on_off_switch") assert on_off_switch.state == "on" @@ -79,6 +87,9 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "on" + on_off_relay = hass.states.get("switch.on_off_relay") + assert on_off_relay.state == "on" + state_changed_event = { "t": "event", "e": "changed", diff --git a/tests/components/derivative/__init__.py b/tests/components/derivative/__init__.py new file mode 100644 index 00000000000..870bbd317d2 --- /dev/null +++ b/tests/components/derivative/__init__.py @@ -0,0 +1 @@ +"""Tests for the derivative component.""" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py new file mode 100644 index 00000000000..05ce55223d0 --- /dev/null +++ b/tests/components/derivative/test_sensor.py @@ -0,0 +1,220 @@ +"""The tests for the derivative sensor platform.""" +from datetime import timedelta +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + + +async def test_state(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "derivative", + "source": "sensor.energy", + "unit": "kW", + "round": 2, + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1, {}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=3600) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, 1, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get("sensor.derivative") + assert state is not None + + # Testing a energy sensor at 1 kWh for 1hour = 0kW + assert round(float(state.state), config["sensor"]["round"]) == 0.0 + + assert state.attributes.get("unit_of_measurement") == "kW" + + +async def _setup_sensor(hass, config): + default_config = { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "round": 2, + } + + config = {"sensor": dict(default_config, **config)} + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 0, {}) + await hass.async_block_till_done() + + return config, entity_id + + +async def setup_tests(hass, config, times, values, expected_state): + """Test derivative sensor state.""" + config, entity_id = await _setup_sensor(hass, config) + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in zip(times, values): + now = dt_util.utcnow() + timedelta(seconds=time) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get("sensor.power") + assert state is not None + + assert round(float(state.state), config["sensor"]["round"]) == expected_state + + return state + + +async def test_dataSet1(hass): + """Test derivative sensor state.""" + await setup_tests( + hass, + {"unit_time": "s"}, + times=[20, 30, 40, 50], + values=[10, 30, 5, 0], + expected_state=-0.5, + ) + + +async def test_dataSet2(hass): + """Test derivative sensor state.""" + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 0], expected_state=-0.5 + ) + + +async def test_dataSet3(hass): + """Test derivative sensor state.""" + state = await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 10], expected_state=0.5 + ) + + assert state.attributes.get("unit_of_measurement") == "/s" + + +async def test_dataSet4(hass): + """Test derivative sensor state.""" + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[5, 5], expected_state=0 + ) + + +async def test_dataSet5(hass): + """Test derivative sensor state.""" + await setup_tests( + hass, {"unit_time": "s"}, times=[20, 30], values=[10, -10], expected_state=-2 + ) + + +async def test_dataSet6(hass): + """Test derivative sensor state.""" + await setup_tests(hass, {}, times=[0, 60], values=[0, 1 / 60], expected_state=1) + + +async def test_data_moving_average_for_discrete_sensor(hass): + """Test derivative sensor state.""" + # We simulate the following situation: + # The temperature rises 1 °C per minute for 1 hour long. + # There is a data point every second, however, the sensor returns + # the temperature rounded down to an integer value. + # We use a time window of 10 minutes and therefore we can expect + # (because the true derivative is 1 °C/min) an error of less than 10%. + + temperature_values = [] + for temperature in range(60): + temperature_values += [temperature] * 60 + time_window = 600 + + times = list(range(len(temperature_values))) + config, entity_id = await _setup_sensor( + hass, {"time_window": {"seconds": time_window}, "unit_time": "min", "round": 1} + ) # two minute window + + for time, value in zip(times, temperature_values): + now = dt_util.utcnow() + timedelta(seconds=time) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, value, {}, force_update=True) + await hass.async_block_till_done() + + if time_window < time < times[-1] - time_window: + state = hass.states.get("sensor.power") + derivative = round(float(state.state), config["sensor"]["round"]) + # Test that the error is never more than + # (time_window_in_minutes / true_derivative * 100) = 10% + assert abs(1 - derivative) <= 0.1 + + +async def test_prefix(hass): + """Test derivative sensor state using a power source.""" + config = { + "sensor": { + "platform": "derivative", + "name": "derivative", + "source": "sensor.power", + "round": 2, + "unit_prefix": "k", + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set( + entity_id, 1000, {"unit_of_measurement": "W"}, force_update=True + ) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=3600) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set( + entity_id, 1000, {"unit_of_measurement": "W"}, force_update=True + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.derivative") + assert state is not None + + # Testing a power sensor at 1000 Watts for 1hour = 0kW/h + assert round(float(state.state), config["sensor"]["round"]) == 0.0 + assert state.attributes.get("unit_of_measurement") == "kW/h" + + +async def test_suffix(hass): + """Test derivative sensor state using a network counter source.""" + config = { + "sensor": { + "platform": "derivative", + "name": "derivative", + "source": "sensor.bytes_per_second", + "round": 2, + "unit_prefix": "k", + "unit_time": "s", + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1000, {}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(seconds=10) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set(entity_id, 1000, {}, force_update=True) + await hass.async_block_till_done() + + state = hass.states.get("sensor.derivative") + assert state is not None + + # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes/s2 + assert round(float(state.state), config["sensor"]["round"]) == 0.0 diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 5d997a485a5..651d989d105 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -610,7 +610,7 @@ async def test_automation_with_bad_condition(hass, caplog): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 15cd28e8fae..950ace24335 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 5f6b124a3d5..367a86eabb4 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -820,6 +820,67 @@ async def test_purecool_update_state(devices, login, hass): assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() +@asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) +@asynctest.patch( + "libpurecool.dyson.DysonAccount.devices", + return_value=[_get_dyson_purecool_device()], +) +async def test_purecool_update_state_filter_inv(devices, login, hass): + """Test state TP06 carbon filter state.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + event = { + "msg": "CURRENT-STATE", + "product-state": { + "fpwr": "OFF", + "fdir": "ON", + "auto": "ON", + "oscs": "ON", + "oson": "ON", + "nmod": "ON", + "rhtm": "ON", + "fnst": "FAN", + "ercd": "11E1", + "wacd": "NONE", + "nmdv": "0004", + "fnsp": "0002", + "bril": "0002", + "corf": "ON", + "cflr": "INV", + "hflr": "0075", + "sltm": "OFF", + "osal": "0055", + "osau": "0105", + "ancp": "CUST", + }, + } + device.state = DysonPureCoolV2State(json.dumps(event)) + + for call in device.add_message_listener.call_args_list: + callback = call[0][0] + if type(callback.__self__) == dyson.DysonPureCoolDevice: + callback(device.state) + + await hass.async_block_till_done() + fan_state = hass.states.get("fan.living_room") + attributes = fan_state.attributes + + assert fan_state.state == "off" + assert attributes[dyson.ATTR_NIGHT_MODE] is True + assert attributes[dyson.ATTR_AUTO_MODE] is True + assert attributes[dyson.ATTR_ANGLE_LOW] == 55 + assert attributes[dyson.ATTR_ANGLE_HIGH] == 105 + assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is True + assert attributes[dyson.ATTR_TIMER] == "OFF" + assert attributes[dyson.ATTR_HEPA_FILTER] == 75 + assert attributes[dyson.ATTR_CARBON_FILTER] == "INV" + assert attributes[dyson.ATTR_DYSON_SPEED] == int(FanSpeed.FAN_SPEED_2.value) + assert attributes[ATTR_SPEED] is SPEED_LOW + assert attributes[ATTR_OSCILLATING] is False + assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() + + @asynctest.patch("libpurecool.dyson.DysonAccount.login", return_value=True) @asynctest.patch( "libpurecool.dyson.DysonAccount.devices", diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 2fb5c48e768..51c3da7f08d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -32,10 +32,20 @@ from homeassistant.components.emulated_hue.hue_api import ( HueOneLightStateView, HueUsernameView, ) -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, get_test_instance_port +from tests.common import ( + async_fire_time_changed, + async_mock_service, + get_test_instance_port, +) HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() @@ -228,7 +238,65 @@ async def test_light_without_brightness_supported(hass_hue, hue_client): ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True - assert light_without_brightness_json["type"] == "On/off light" + assert light_without_brightness_json["type"] == "Dimmable light" + + +async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): + """Test that light without brightness can be turned off.""" + hass_hue.states.async_set("light.no_brightness", "on", {}) + + # Check if light can be turned off + turn_off_calls = async_mock_service(hass_hue, light.DOMAIN, SERVICE_TURN_OFF) + + no_brightness_result = await perform_put_light_state( + hass_hue, hue_client, "light.no_brightness", False + ) + no_brightness_result_json = await no_brightness_result.json() + + assert no_brightness_result.status == 200 + assert "application/json" in no_brightness_result.headers["content-type"] + assert len(no_brightness_result_json) == 1 + + # Verify that SERVICE_TURN_OFF has been called + await hass_hue.async_block_till_done() + assert 1 == len(turn_off_calls) + call = turn_off_calls[-1] + + assert light.DOMAIN == call.domain + assert SERVICE_TURN_OFF == call.service + assert "light.no_brightness" in call.data[ATTR_ENTITY_ID] + + +async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client): + """Test that light without brightness can be turned on.""" + hass_hue.states.async_set("light.no_brightness", "off", {}) + + # Check if light can be turned on + turn_on_calls = async_mock_service(hass_hue, light.DOMAIN, SERVICE_TURN_ON) + + no_brightness_result = await perform_put_light_state( + hass_hue, + hue_client, + "light.no_brightness", + True, + # Some remotes, like HarmonyHub send brightness value regardless of light's features + brightness=0, + ) + + no_brightness_result_json = await no_brightness_result.json() + + assert no_brightness_result.status == 200 + assert "application/json" in no_brightness_result.headers["content-type"] + assert len(no_brightness_result_json) == 1 + + # Verify that SERVICE_TURN_ON has been called + await hass_hue.async_block_till_done() + assert 1 == len(turn_on_calls) + call = turn_on_calls[-1] + + assert light.DOMAIN == call.domain + assert SERVICE_TURN_ON == call.service + assert "light.no_brightness" in call.data[ATTR_ENTITY_ID] @pytest.mark.parametrize( diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 5897b80659a..ea002275153 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -4,10 +4,11 @@ import unittest from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE +import defusedxml.ElementTree as ET import requests from homeassistant import const, setup -from homeassistant.components import emulated_hue, http +from homeassistant.components import emulated_hue from tests.common import get_test_home_assistant, get_test_instance_port @@ -28,10 +29,6 @@ class TestEmulatedHue(unittest.TestCase): """Set up the class.""" cls.hass = hass = get_test_home_assistant() - setup.setup_component( - hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} - ) - with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): setup.setup_component( hass, @@ -52,8 +49,6 @@ class TestEmulatedHue(unittest.TestCase): def test_description_xml(self): """Test the description.""" - import defusedxml.ElementTree as ET - result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) assert result.status_code == 200 diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 796ddd93d26..fc8bfc318bb 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -97,7 +97,7 @@ class TestBanSensor(unittest.TestCase): def test_single_ban(self): """Test that log is parsed correctly for single ban.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("single_ban")) @@ -112,7 +112,7 @@ class TestBanSensor(unittest.TestCase): def test_ipv6_ban(self): """Test that log is parsed correctly for IPV6 bans.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("ipv6_ban")) @@ -127,7 +127,7 @@ class TestBanSensor(unittest.TestCase): def test_multiple_ban(self): """Test that log is parsed correctly for multiple ban.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("multi_ban")) @@ -148,7 +148,7 @@ class TestBanSensor(unittest.TestCase): def test_unban_all(self): """Test that log is parsed correctly when unbanning.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("unban_all")) @@ -166,7 +166,7 @@ class TestBanSensor(unittest.TestCase): def test_unban_one(self): """Test that log is parsed correctly when unbanning one ip.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("unban_one")) @@ -184,7 +184,7 @@ class TestBanSensor(unittest.TestCase): def test_multi_jail(self): """Test that log is parsed correctly when using multiple jails.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor1 = BanSensor("fail2ban", "jail_one", log_parser) sensor2 = BanSensor("fail2ban", "jail_two", log_parser) assert sensor1.name == "fail2ban jail_one" @@ -205,7 +205,7 @@ class TestBanSensor(unittest.TestCase): def test_ban_active_after_update(self): """Test that ban persists after subsequent update.""" - log_parser = BanLogParser("/tmp") + log_parser = BanLogParser("/test/fail2ban.log") sensor = BanSensor("fail2ban", "jail_one", log_parser) assert sensor.name == "fail2ban jail_one" mock_fh = MockOpen(read_data=fake_log("single_ban")) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index e665f9d5ddc..939fee154c5 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 3d4f4229965..b44ba22d8e5 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index bd22730e82f..f9f25192211 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,7 +1,7 @@ """The tests for Home Assistant frontend.""" import re -from unittest.mock import patch +from asynctest import patch import pytest from homeassistant.components.frontend import ( @@ -173,7 +173,7 @@ async def test_themes_reload_themes(hass, hass_ws_client): client = await hass_ws_client(hass) with patch( - "homeassistant.components.frontend.load_yaml_config_file", + "homeassistant.components.frontend.async_hass_config_yaml", return_value={DOMAIN: {CONF_THEMES: {"sad": {"primary-color": "blue"}}}}, ): await hass.services.async_call( diff --git a/tests/components/garmin_connect/__init__py b/tests/components/garmin_connect/__init__py new file mode 100644 index 00000000000..26de06ae0ac --- /dev/null +++ b/tests/components/garmin_connect/__init__py @@ -0,0 +1 @@ +"""Tests for the Garmin Connect component.""" diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py new file mode 100644 index 00000000000..276b6f46871 --- /dev/null +++ b/tests/components/garmin_connect/test_config_flow.py @@ -0,0 +1,100 @@ +"""Test the Garmin Connect config flow.""" +from unittest.mock import patch + +from garminconnect import ( + GarminConnectAuthenticationError, + GarminConnectConnectionError, + GarminConnectTooManyRequestsError, +) +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.garmin_connect.const import DOMAIN +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +MOCK_CONF = { + CONF_ID: "First Lastname", + CONF_USERNAME: "my@email.address", + CONF_PASSWORD: "mypassw0rd", +} + + +@pytest.fixture(name="mock_garmin_connect") +def mock_garmin(): + """Mock Garmin.""" + with patch("homeassistant.components.garmin_connect.config_flow.Garmin",) as garmin: + garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + yield garmin.return_value + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_user(hass, mock_garmin_connect): + """Test registering an integration and finishing flow works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == MOCK_CONF + + +async def test_connection_error(hass, mock_garmin_connect): + """Test for connection error.""" + mock_garmin_connect.login.side_effect = GarminConnectConnectionError("errormsg") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_authentication_error(hass, mock_garmin_connect): + """Test for authentication error.""" + mock_garmin_connect.login.side_effect = GarminConnectAuthenticationError("errormsg") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_toomanyrequest_error(hass, mock_garmin_connect): + """Test for toomanyrequests error.""" + mock_garmin_connect.login.side_effect = GarminConnectTooManyRequestsError( + "errormsg" + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "too_many_requests"} + + +async def test_unknown_error(hass, mock_garmin_connect): + """Test for unknown error.""" + mock_garmin_connect.login.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_abort_if_already_setup(hass, mock_garmin_connect): + """Test abort if already setup.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index edb12f06f33..c0b5aa7b193 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -22,6 +22,7 @@ class MockConfig(helpers.AbstractConfig): *, secure_devices_pin=None, should_expose=None, + should_2fa=None, entity_config=None, hass=None, local_sdk_webhook_id=None, @@ -103,7 +104,10 @@ DEMO_DEVICES = [ }, { "id": "light.ceiling_lights", - "name": {"name": "Roof Lights", "nicknames": ["top lights", "ceiling lights"]}, + "name": { + "name": "Roof Lights", + "nicknames": ["Roof Lights", "top lights", "ceiling lights"], + }, "traits": [ "action.devices.traits.OnOff", "action.devices.traits.Brightness", diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 112935f0160..f5e3e505a28 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -145,38 +145,6 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): assert call[3] == MOCK_HEADER -async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): - """Test the function to call the homegraph api.""" - config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), - ) - await config.async_initialize() - - aioclient_mock.post(MOCK_URL, status=200, json={}) - - res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) - assert res == 200 - assert aioclient_mock.call_count == 1 - - call = aioclient_mock.mock_calls[0] - assert call[1].query == {"key": "dummy_key"} - assert call[2] == MOCK_JSON - - -async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): - """Test the function to call the homegraph api.""" - config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), - ) - await config.async_initialize() - - aioclient_mock.post(MOCK_URL, status=666, json={}) - - res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) - assert res == 666 - assert aioclient_mock.call_count == 1 - - async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" agent_user_id = "user" diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 2773f3c3329..0df2b032b5a 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -3,17 +3,21 @@ from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component -GA_API_KEY = "Agdgjsj399sdfkosd932ksd" +from .test_http import DUMMY_CONFIG async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" + aioclient_mock.post( + ga.const.HOMEGRAPH_TOKEN_URL, + status=200, + json={"access_token": "1234", "expires_in": 3600}, + ) + aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) await async_setup_component( - hass, - "google_assistant", - {"google_assistant": {"project_id": "test_project", "api_key": GA_API_KEY}}, + hass, "google_assistant", {"google_assistant": DUMMY_CONFIG}, ) assert aioclient_mock.call_count == 0 @@ -24,4 +28,4 @@ async def test_request_sync_service(aioclient_mock, hass): context=Context(user_id="123"), ) - assert aioclient_mock.call_count == 1 + assert aioclient_mock.call_count == 2 # token + request diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 7ffe9cda477..aa073c699f8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -82,6 +82,7 @@ async def test_sync_message(hass): config, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -91,7 +92,10 @@ async def test_sync_message(hass): "devices": [ { "id": "light.demo_light", - "name": {"name": "Demo Light", "nicknames": ["Hello", "World"]}, + "name": { + "name": "Demo Light", + "nicknames": ["Demo Light", "Hello", "World"], + }, "traits": [ trait.TRAIT_BRIGHTNESS, trait.TRAIT_ONOFF, @@ -115,7 +119,7 @@ async def test_sync_message(hass): assert len(events) == 1 assert events[0].event_type == EVENT_SYNC_RECEIVED - assert events[0].data == {"request_id": REQ_ID} + assert events[0].data == {"request_id": REQ_ID, "source": "cloud"} # pylint: disable=redefined-outer-name @@ -148,6 +152,7 @@ async def test_sync_in_area(hass, registries): config, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -181,7 +186,7 @@ async def test_sync_in_area(hass, registries): assert len(events) == 1 assert events[0].event_type == EVENT_SYNC_RECEIVED - assert events[0].data == {"request_id": REQ_ID} + assert events[0].data == {"request_id": REQ_ID, "source": "cloud"} async def test_query_message(hass): @@ -220,6 +225,7 @@ async def test_query_message(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -247,11 +253,23 @@ async def test_query_message(hass): assert len(events) == 3 assert events[0].event_type == EVENT_QUERY_RECEIVED - assert events[0].data == {"request_id": REQ_ID, "entity_id": "light.demo_light"} + assert events[0].data == { + "request_id": REQ_ID, + "entity_id": "light.demo_light", + "source": "cloud", + } assert events[1].event_type == EVENT_QUERY_RECEIVED - assert events[1].data == {"request_id": REQ_ID, "entity_id": "light.another_light"} + assert events[1].data == { + "request_id": REQ_ID, + "entity_id": "light.another_light", + "source": "cloud", + } assert events[2].event_type == EVENT_QUERY_RECEIVED - assert events[2].data == {"request_id": REQ_ID, "entity_id": "light.non_existing"} + assert events[2].data == { + "request_id": REQ_ID, + "entity_id": "light.non_existing", + "source": "cloud", + } async def test_execute(hass): @@ -300,6 +318,7 @@ async def test_execute(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -341,6 +360,7 @@ async def test_execute(hass): "command": "action.devices.commands.OnOff", "params": {"on": True}, }, + "source": "cloud", } assert events[1].event_type == EVENT_COMMAND_RECEIVED assert events[1].data == { @@ -350,6 +370,7 @@ async def test_execute(hass): "command": "action.devices.commands.BrightnessAbsolute", "params": {"brightness": 20}, }, + "source": "cloud", } assert events[2].event_type == EVENT_COMMAND_RECEIVED assert events[2].data == { @@ -359,6 +380,7 @@ async def test_execute(hass): "command": "action.devices.commands.OnOff", "params": {"on": True}, }, + "source": "cloud", } assert events[3].event_type == EVENT_COMMAND_RECEIVED assert events[3].data == { @@ -368,6 +390,7 @@ async def test_execute(hass): "command": "action.devices.commands.BrightnessAbsolute", "params": {"brightness": 20}, }, + "source": "cloud", } assert len(service_events) == 2 @@ -424,6 +447,7 @@ async def test_raising_error_trait(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -448,6 +472,7 @@ async def test_raising_error_trait(hass): "command": "action.devices.commands.ThermostatTemperatureSetpoint", "params": {"thermostatTemperatureSetpoint": 10}, }, + "source": "cloud", } @@ -483,6 +508,7 @@ async def test_unavailable_state_does_sync(hass): BASIC_CONFIG, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -515,7 +541,7 @@ async def test_unavailable_state_does_sync(hass): assert len(events) == 1 assert events[0].event_type == EVENT_SYNC_RECEIVED - assert events[0].data == {"request_id": REQ_ID} + assert events[0].data == {"request_id": REQ_ID, "source": "cloud"} @pytest.mark.parametrize( @@ -545,6 +571,7 @@ async def test_device_class_switch(hass, device_class, google_type): BASIC_CONFIG, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -589,6 +616,7 @@ async def test_device_class_binary_sensor(hass, device_class, google_type): BASIC_CONFIG, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -629,6 +657,7 @@ async def test_device_class_cover(hass, device_class, google_type): BASIC_CONFIG, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -669,6 +698,7 @@ async def test_device_media_player(hass, device_class, google_type): BASIC_CONFIG, "test-agent", {"requestId": REQ_ID, "inputs": [{"intent": "action.devices.SYNC"}]}, + const.SOURCE_CLOUD, ) assert result == { @@ -702,6 +732,7 @@ async def test_query_disconnect(hass): config, "test-agent", {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, + const.SOURCE_CLOUD, ) assert result is None assert len(mock_disconnect.mock_calls) == 1 @@ -751,6 +782,7 @@ async def test_trait_execute_adding_query_data(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -817,6 +849,7 @@ async def test_identify(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -851,8 +884,11 @@ async def test_reachable_devices(hass): # Not passed in as google_id hass.states.async_set("light.not_mentioned", "on") + # Has 2FA + hass.states.async_set("lock.has_2fa", "on") + config = MockConfig( - should_expose=lambda state: state.entity_id != "light.not_expose" + should_expose=lambda state: state.entity_id != "light.not_expose", ) user_agent_id = "mock-user-id" @@ -898,9 +934,19 @@ async def test_reachable_devices(hass): "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, + { + "id": "lock.has_2fa", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": proxy_device_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, {"id": proxy_device_id, "customData": {}}, ], }, + const.SOURCE_CLOUD, ) assert result == { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index f59d4006d29..232da039ea7 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -51,11 +51,15 @@ _LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" -BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None) +BASIC_DATA = helpers.RequestData( + BASIC_CONFIG, "test-agent", const.SOURCE_CLOUD, REQ_ID, None +) PIN_CONFIG = MockConfig(secure_devices_pin="1234") -PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None) +PIN_DATA = helpers.RequestData( + PIN_CONFIG, "test-agent", const.SOURCE_CLOUD, REQ_ID, None +) async def test_brightness_light(hass): diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ee52a551cb8..febe261c9e4 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -429,9 +429,8 @@ class TestComponentsGroup(unittest.TestCase): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - common.reload(self.hass) - self.hass.block_till_done() + common.reload(self.hass) + self.hass.block_till_done() assert sorted(self.hass.states.entity_ids()) == [ "group.all_tests", diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 091270c12c4..9a50da4ce41 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -32,7 +32,7 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): with patch( "homeassistant.components.hassio.HassIO.update_hass_api", return_value=mock_coro({"result": "ok"}), - ), patch( + ) as hass_api, patch( "homeassistant.components.hassio.HassIO.update_hass_timezone", return_value=mock_coro({"result": "ok"}), ), patch( @@ -42,6 +42,8 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): hass.state = CoreState.starting hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) + return hass_api.call_args[0][1] + @pytest.fixture def hassio_client(hassio_stubs, hass, hass_client): @@ -55,6 +57,15 @@ def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) +@pytest.fixture +async def hassio_client_supervisor(hass, aiohttp_client, hassio_stubs): + """Return an authenticated HTTP client.""" + access_token = hass.auth.async_create_access_token(hassio_stubs) + return await aiohttp_client( + hass.http.app, headers={"Authorization": f"Bearer {access_token}"}, + ) + + @pytest.fixture def hassio_handler(hass, aioclient_mock): """Create mock hassio handler.""" diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index c7fe3459e41..189273c5802 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -6,14 +6,14 @@ from homeassistant.exceptions import HomeAssistantError from tests.common import mock_coro -async def test_login_success(hass, hassio_client): +async def test_auth_success(hass, hassio_client_supervisor): """Test no auth needed for .""" with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", Mock(return_value=mock_coro()), ) as mock_login: - resp = await hassio_client.post( + resp = await hassio_client_supervisor.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, ) @@ -23,12 +23,12 @@ async def test_login_success(hass, hassio_client): mock_login.assert_called_with("test", "123456") -async def test_login_error(hass, hassio_client): - """Test no auth needed for error.""" +async def test_auth_fails_no_supervisor(hass, hassio_client): + """Test if only supervisor can access.""" with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", - Mock(side_effect=HomeAssistantError()), + Mock(return_value=mock_coro()), ) as mock_login: resp = await hassio_client.post( "/api/hassio_auth", @@ -36,32 +36,66 @@ async def test_login_error(hass, hassio_client): ) # Check we got right response - assert resp.status == 403 + assert resp.status == 401 + assert not mock_login.called + + +async def test_auth_fails_no_auth(hass, hassio_noauth_client): + """Test if only supervisor can access.""" + with patch( + "homeassistant.auth.providers.homeassistant." + "HassAuthProvider.async_validate_login", + Mock(return_value=mock_coro()), + ) as mock_login: + resp = await hassio_noauth_client.post( + "/api/hassio_auth", + json={"username": "test", "password": "123456", "addon": "samba"}, + ) + + # Check we got right response + assert resp.status == 401 + assert not mock_login.called + + +async def test_login_error(hass, hassio_client_supervisor): + """Test no auth needed for error.""" + with patch( + "homeassistant.auth.providers.homeassistant." + "HassAuthProvider.async_validate_login", + Mock(side_effect=HomeAssistantError()), + ) as mock_login: + resp = await hassio_client_supervisor.post( + "/api/hassio_auth", + json={"username": "test", "password": "123456", "addon": "samba"}, + ) + + # Check we got right response + assert resp.status == 401 mock_login.assert_called_with("test", "123456") -async def test_login_no_data(hass, hassio_client): +async def test_login_no_data(hass, hassio_client_supervisor): """Test auth with no data -> error.""" with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", Mock(side_effect=HomeAssistantError()), ) as mock_login: - resp = await hassio_client.post("/api/hassio_auth") + resp = await hassio_client_supervisor.post("/api/hassio_auth") # Check we got right response assert resp.status == 400 assert not mock_login.called -async def test_login_no_username(hass, hassio_client): +async def test_login_no_username(hass, hassio_client_supervisor): """Test auth with no username in data -> error.""" with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", Mock(side_effect=HomeAssistantError()), ) as mock_login: - resp = await hassio_client.post( + resp = await hassio_client_supervisor.post( "/api/hassio_auth", json={"password": "123456", "addon": "samba"} ) @@ -70,14 +104,14 @@ async def test_login_no_username(hass, hassio_client): assert not mock_login.called -async def test_login_success_extra(hass, hassio_client): +async def test_login_success_extra(hass, hassio_client_supervisor): """Test auth with extra data.""" with patch( "homeassistant.auth.providers.homeassistant." "HassAuthProvider.async_validate_login", Mock(return_value=mock_coro()), ) as mock_login: - resp = await hassio_client.post( + resp = await hassio_client_supervisor.post( "/api/hassio_auth", json={ "username": "test", @@ -90,3 +124,67 @@ async def test_login_success_extra(hass, hassio_client): # Check we got right response assert resp.status == 200 mock_login.assert_called_with("test", "123456") + + +async def test_password_success(hass, hassio_client_supervisor): + """Test no auth needed for .""" + with patch( + "homeassistant.components.hassio.auth.HassIOPasswordReset._change_password", + Mock(return_value=mock_coro()), + ) as mock_change: + resp = await hassio_client_supervisor.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) + + # Check we got right response + assert resp.status == 200 + mock_change.assert_called_with("test", "123456") + + +async def test_password_fails_no_supervisor(hass, hassio_client): + """Test if only supervisor can access.""" + with patch( + "homeassistant.auth.providers.homeassistant.Data.async_save", + Mock(return_value=mock_coro()), + ) as mock_save: + resp = await hassio_client.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) + + # Check we got right response + assert resp.status == 401 + assert not mock_save.called + + +async def test_password_fails_no_auth(hass, hassio_noauth_client): + """Test if only supervisor can access.""" + with patch( + "homeassistant.auth.providers.homeassistant.Data.async_save", + Mock(return_value=mock_coro()), + ) as mock_save: + resp = await hassio_noauth_client.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) + + # Check we got right response + assert resp.status == 401 + assert not mock_save.called + + +async def test_password_no_user(hass, hassio_client_supervisor): + """Test no auth needed for .""" + with patch( + "homeassistant.auth.providers.homeassistant.Data.async_save", + Mock(return_value=mock_coro()), + ) as mock_save: + resp = await hassio_client_supervisor.post( + "/api/hassio_auth/password_reset", + json={"username": "test", "password": "123456"}, + ) + + # Check we got right response + assert resp.status == 500 + assert not mock_save.called diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 52cb3232ca6..5789dde64c1 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -60,7 +60,7 @@ async def test_forward_request_no_auth_for_panel( async def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): - """Test no auth needed for .""" + """Test no auth needed for logo.""" aioclient_mock.get("http://127.0.0.1/addons/bl_b392/logo", text="response") resp = await hassio_client.get("/api/hassio/addons/bl_b392/logo") @@ -74,6 +74,21 @@ async def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 +async def test_forward_request_no_auth_for_icon(hassio_client, aioclient_mock): + """Test no auth needed for icon.""" + aioclient_mock.get("http://127.0.0.1/addons/bl_b392/icon", text="response") + + resp = await hassio_client.get("/api/hassio/addons/bl_b392/icon") + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "response" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + + async def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1e227f943ed..2751062dedf 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -52,7 +52,7 @@ async def test_setup_api_panel(hass, aioclient_mock): assert panels.get("hassio").to_response() == { "component_name": "custom", "icon": "hass:home-assistant", - "title": "Hass.io", + "title": "Supervisor", "url_path": "hassio", "require_admin": True, "config": { diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 0f9bf2d8b3e..354751be0d2 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -63,11 +63,6 @@ async def setup_platform(hass, config_entry, config): await hass.async_block_till_done() -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await media_player.async_setup_platform(None, None, None) - - async def test_state_attributes(hass, config_entry, config, controller): """Tests the state attributes.""" await setup_platform(hass, config_entry, config) diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index d3bbac44df8..c423f66c7b8 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest import voluptuous as vol +from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -17,7 +18,7 @@ async def test_reload_config_service(hass): "homeassistant.config.load_yaml_config_file", autospec=True, return_value={"scene": {"name": "Hallo", "entities": {"light.kitchen": "on"}}}, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call("scene", "reload", blocking=True) await hass.async_block_till_done() @@ -27,7 +28,7 @@ async def test_reload_config_service(hass): "homeassistant.config.load_yaml_config_file", autospec=True, return_value={"scene": {"name": "Bye", "entities": {"light.kitchen": "on"}}}, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call("scene", "reload", blocking=True) await hass.async_block_till_done() @@ -209,3 +210,51 @@ async def test_ensure_no_intersection(hass): await hass.async_block_till_done() assert "entities and snapshot_entities must not overlap" in str(ex.value) assert hass.states.get("scene.hallo") is None + + +async def test_scenes_with_entity(hass): + """Test finding scenes with a specific entity.""" + assert await async_setup_component( + hass, + "scene", + { + "scene": [ + {"name": "scene_1", "entities": {"light.kitchen": "on"}}, + {"name": "scene_2", "entities": {"light.living_room": "off"}}, + { + "name": "scene_3", + "entities": {"light.kitchen": "on", "light.living_room": "off"}, + }, + ] + }, + ) + + assert sorted(ha_scene.scenes_with_entity(hass, "light.kitchen")) == [ + "scene.scene_1", + "scene.scene_3", + ] + + +async def test_entities_in_scene(hass): + """Test finding entities in a scene.""" + assert await async_setup_component( + hass, + "scene", + { + "scene": [ + {"name": "scene_1", "entities": {"light.kitchen": "on"}}, + {"name": "scene_2", "entities": {"light.living_room": "off"}}, + { + "name": "scene_3", + "entities": {"light.kitchen": "on", "light.living_room": "off"}, + }, + ] + }, + ) + + for scene_id, entities in ( + ("scene.scene_1", ["light.kitchen"]), + ("scene.scene_2", ["light.living_room"]), + ("scene.scene_3", ["light.kitchen", "light.living_room"]), + ): + assert ha_scene.entities_in_scene(hass, scene_id) == entities diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index fb73c132e30..87d4fbdcc2b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -151,6 +151,12 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events): assert acc.char_target_position.value == 60 assert acc.char_position_state.value == 1 + hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 70.0}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 70 + assert acc.char_target_position.value == 70 + assert acc.char_position_state.value == 1 + hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_POSITION: 50}) await hass.async_block_till_done() assert acc.char_current_position.value == 50 diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index fa19f573c7c..b0b06447f8a 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,5 +1,5 @@ """Initializer helpers for HomematicIP fake server.""" -from asynctest import MagicMock, Mock, patch +from asynctest import CoroutineMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -9,14 +9,21 @@ from homeassistant import config_entries from homeassistant.components.homematicip_cloud import ( DOMAIN as HMIPC_DOMAIN, async_setup as hmip_async_setup, - const as hmipc, - hap as hmip_hap, ) +from homeassistant.components.homematicip_cloud.const import ( + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, + HMIPC_PIN, +) +from homeassistant.components.homematicip_cloud.hap import HomematicipHAP +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.setup import async_setup_component from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry @pytest.fixture(name="mock_connection") @@ -30,8 +37,8 @@ def mock_connection_fixture() -> AsyncConnection: connection._restCall.side_effect = ( # pylint: disable=protected-access _rest_call_side_effect ) - connection.api_call.return_value = mock_coro(True) - connection.init.side_effect = mock_coro(True) + connection.api_call = CoroutineMock(return_value=True) + connection.init = CoroutineMock(side_effect=True) return connection @@ -40,17 +47,18 @@ def mock_connection_fixture() -> AsyncConnection: def hmip_config_entry_fixture() -> config_entries.ConfigEntry: """Create a mock config entriy for homematic ip cloud.""" entry_data = { - hmipc.HMIPC_HAPID: HAPID, - hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, - hmipc.HMIPC_NAME: "", - hmipc.HMIPC_PIN: HAPPIN, + HMIPC_HAPID: HAPID, + HMIPC_AUTHTOKEN: AUTH_TOKEN, + HMIPC_NAME: "", + HMIPC_PIN: HAPPIN, } config_entry = MockConfigEntry( version=1, domain=HMIPC_DOMAIN, title=HAPID, + unique_id=HAPID, data=entry_data, - source="import", + source=SOURCE_IMPORT, connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, system_options={"disable_new_entities": False}, ) @@ -67,7 +75,7 @@ def default_mock_home_fixture(mock_connection) -> AsyncHome: @pytest.fixture(name="default_mock_hap") async def default_mock_hap_fixture( hass: HomeAssistantType, mock_connection, hmip_config_entry -) -> hmip_hap.HomematicipHAP: +) -> HomematicipHAP: """Create a mocked homematic access point.""" return await get_mock_hap(hass, mock_connection, hmip_config_entry) @@ -76,25 +84,27 @@ async def get_mock_hap( hass: HomeAssistantType, mock_connection, hmip_config_entry: config_entries.ConfigEntry, -) -> hmip_hap.HomematicipHAP: +) -> HomematicipHAP: """Create a mocked homematic access point.""" - hass.config.components.add(HMIPC_DOMAIN) - hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) home_name = hmip_config_entry.data["name"] mock_home = ( HomeTemplate(connection=mock_connection, home_name=home_name) .init_home() .get_async_home_mock() ) - with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): - assert await hap.async_setup() - mock_home.on_update(hap.async_update) - mock_home.on_create(hap.async_create_entity) - hass.data[HMIPC_DOMAIN] = {HAPID: hap} + hmip_config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.get_hap", + return_value=mock_home, + ): + assert await async_setup_component(hass, HMIPC_DOMAIN, {}) is True await hass.async_block_till_done() + hap = hass.data[HMIPC_DOMAIN][HAPID] + mock_home.on_update(hap.async_update) + mock_home.on_create(hap.async_create_entity) return hap @@ -103,10 +113,10 @@ def hmip_config_fixture() -> ConfigType: """Create a config for homematic ip cloud.""" entry_data = { - hmipc.HMIPC_HAPID: HAPID, - hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, - hmipc.HMIPC_NAME: "", - hmipc.HMIPC_PIN: HAPPIN, + HMIPC_HAPID: HAPID, + HMIPC_AUTHTOKEN: AUTH_TOKEN, + HMIPC_NAME: "", + HMIPC_PIN: HAPPIN, } return {HMIPC_DOMAIN: [entry_data]} @@ -121,7 +131,7 @@ def dummy_config_fixture() -> ConfigType: @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( hass: HomeAssistantType, default_mock_hap, dummy_config -) -> hmip_hap.HomematicipHAP: +) -> HomematicipHAP: """Create a fake homematic access point with hass services.""" await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index afaf71c67b5..01e820e7565 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,167 +1,180 @@ """Tests for HomematicIP Cloud config flow.""" -from unittest.mock import patch +from asynctest import patch -from homeassistant.components.homematicip_cloud import config_flow, const, hap as hmipc +from homeassistant.components.homematicip_cloud.const import ( + DOMAIN as HMIPC_DOMAIN, + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, + HMIPC_PIN, +) -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry + +DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} + +IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} async def test_flow_works(hass): - """Test config flow works.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass + """Test config flow.""" - hap = hmipc.HomematicipAuth(hass, config) - with patch.object(hap, "get_auth", return_value=mock_coro()), patch.object( - hmipc.HomematicipAuth, "async_checkbutton", return_value=mock_coro(True) - ), patch.object( - hmipc.HomematicipAuth, "async_setup", return_value=mock_coro(True) - ), patch.object( - hmipc.HomematicipAuth, "async_register", return_value=mock_coro(True) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=False, ): - hap.authtoken = "ABC" - result = await flow.async_step_init(user_input=config) + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + ) - assert hap.authtoken == "ABC" - assert result["type"] == "create_entry" + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {"base": "press_the_button"} + + flow = next( + ( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + ) + assert flow["context"]["unique_id"] == "ABC123" + + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == "create_entry" + assert result["title"] == "ABC123" + assert result["data"] == {"hapid": "ABC123", "authtoken": True, "name": "hmip"} + assert result["result"].unique_id == "ABC123" async def test_flow_init_connection_error(hass): """Test config flow with accesspoint connection error.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - with patch.object( - hmipc.HomematicipAuth, "async_setup", return_value=mock_coro(False) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=False, ): - result = await flow.async_step_init(user_input=config) - assert result["type"] == "form" + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" async def test_flow_link_connection_error(hass): """Test config flow client registration connection error.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - with patch.object( - hmipc.HomematicipAuth, "async_setup", return_value=mock_coro(True) - ), patch.object( - hmipc.HomematicipAuth, "async_checkbutton", return_value=mock_coro(True) - ), patch.object( - hmipc.HomematicipAuth, "async_register", return_value=mock_coro(False) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=False, ): - result = await flow.async_step_init(user_input=config) - assert result["type"] == "abort" + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + ) + + assert result["type"] == "abort" + assert result["reason"] == "connection_aborted" async def test_flow_link_press_button(hass): """Test config flow ask for pressing the blue button.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - with patch.object( - hmipc.HomematicipAuth, "async_setup", return_value=mock_coro(True) - ), patch.object( - hmipc.HomematicipAuth, "async_checkbutton", return_value=mock_coro(False) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=False, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, ): - result = await flow.async_step_init(user_input=config) - assert result["type"] == "form" - assert result["errors"] == {"base": "press_the_button"} + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {"base": "press_the_button"} async def test_init_flow_show_form(hass): """Test config flow shows up with a form.""" - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - result = await flow.async_step_init(user_input=None) - assert result["type"] == "form" - - -async def test_init_flow_user_show_form(hass): - """Test config flow shows up with a form.""" - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"} + ) assert result["type"] == "form" + assert result["step_id"] == "init" async def test_init_already_configured(hass): """Test accesspoint is already configured.""" - MockConfigEntry( - domain=const.DOMAIN, data={const.HMIPC_HAPID: "ABC123"} - ).add_to_hass(hass) - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } + MockConfigEntry(domain=HMIPC_DOMAIN, unique_id="ABC123").add_to_hass(hass) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG + ) - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - result = await flow.async_step_init(user_input=config) assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_import_config(hass): """Test importing a host with an existing config file.""" - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - result = await flow.async_step_import( - { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } - ) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG + ) assert result["type"] == "create_entry" assert result["title"] == "ABC123" - assert result["data"] == { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } + assert result["data"] == {"authtoken": "123", "hapid": "ABC123", "name": "hmip"} + assert result["result"].unique_id == "ABC123" async def test_import_existing_config(hass): """Test abort of an existing accesspoint from config.""" - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - - MockConfigEntry( - domain=const.DOMAIN, data={hmipc.HMIPC_HAPID: "ABC123"} - ).add_to_hass(hass) - - result = await flow.async_step_import( - { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } - ) + MockConfigEntry(domain=HMIPC_DOMAIN, unique_id="ABC123").add_to_hass(hass) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", + return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG + ) assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 9626cc0620f..4ce6283d64d 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,9 +1,14 @@ """Common tests for HomematicIP devices.""" +from asynctest import patch +from homematicip.base.enums import EventType + +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import get_mock_hap -from .helper import async_manipulate_test_data, get_and_check_entity_basics +from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics async def test_hmip_remove_device(hass, default_mock_hap): @@ -35,6 +40,51 @@ async def test_hmip_remove_device(hass, default_mock_hap): assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 +async def test_hmip_add_device(hass, default_mock_hap, hmip_config_entry): + """Test Remove of hmip device.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count - 1 + assert len(entity_registry.entities) == pre_entity_count - 3 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + + reloaded_hap = HomematicipHAP(hass, hmip_config_entry) + with patch( + "homeassistant.components.homematicip_cloud.HomematicipHAP", + return_value=reloaded_hap, + ), patch.object(reloaded_hap, "async_connect"), patch.object( + reloaded_hap, "get_hap", return_value=default_mock_hap.home + ), patch( + "homeassistant.components.homematicip_cloud.hap.asyncio.sleep" + ): + default_mock_hap.home.fire_create_event(event_type=EventType.DEVICE_ADDED) + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count + assert len(entity_registry.entities) == pre_entity_count + new_hap = hass.data[HMIPC_DOMAIN][HAPID] + assert len(new_hap.hmip_device_by_entity_id) == pre_mapping_count + + async def test_hmip_remove_group(hass, default_mock_hap): """Test Remove of hmip group.""" entity_id = "switch.strom_group" @@ -56,7 +106,6 @@ async def test_hmip_remove_group(hass, default_mock_hap): pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) hmip_device.fire_remove_event() - await hass.async_block_till_done() assert len(device_registry.devices) == pre_device_count diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 324649ef515..e42dfe8fb4e 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -5,95 +5,79 @@ from homematicip.aio.auth import AsyncAuth from homematicip.base.base_connection import HmipConnectionError import pytest -from homeassistant.components.homematicip_cloud import ( - DOMAIN as HMIPC_DOMAIN, - const, - errors, - hap as hmipc, +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.const import ( + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, + HMIPC_PIN, ) +from homeassistant.components.homematicip_cloud.errors import HmipcConnectionError from homeassistant.components.homematicip_cloud.hap import ( HomematicipAuth, HomematicipHAP, ) +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN -from tests.common import mock_coro, mock_coro_func - async def test_auth_setup(hass): """Test auth setup for client registration.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipAuth(hass, config) - with patch.object(hap, "get_auth", return_value=mock_coro()): - assert await hap.async_setup() + config = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} + hmip_auth = HomematicipAuth(hass, config) + with patch.object(hmip_auth, "get_auth"): + assert await hmip_auth.async_setup() async def test_auth_setup_connection_error(hass): """Test auth setup connection error behaviour.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipAuth(hass, config) - with patch.object(hap, "get_auth", side_effect=errors.HmipcConnectionError): - assert not await hap.async_setup() + config = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} + hmip_auth = HomematicipAuth(hass, config) + with patch.object(hmip_auth, "get_auth", side_effect=HmipcConnectionError): + assert not await hmip_auth.async_setup() async def test_auth_auth_check_and_register(hass): """Test auth client registration.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipAuth(hass, config) - hap.auth = Mock() + config = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} + + hmip_auth = HomematicipAuth(hass, config) + hmip_auth.auth = Mock(spec=AsyncAuth) with patch.object( - hap.auth, "isRequestAcknowledged", return_value=mock_coro(True) + hmip_auth.auth, "isRequestAcknowledged", return_value=True ), patch.object( - hap.auth, "requestAuthToken", return_value=mock_coro("ABC") + hmip_auth.auth, "requestAuthToken", return_value="ABC" ), patch.object( - hap.auth, "confirmAuthToken", return_value=mock_coro() + hmip_auth.auth, "confirmAuthToken" ): - assert await hap.async_checkbutton() - assert await hap.async_register() == "ABC" + assert await hmip_auth.async_checkbutton() + assert await hmip_auth.async_register() == "ABC" async def test_auth_auth_check_and_register_with_exception(hass): """Test auth client registration.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipAuth(hass, config) - hap.auth = Mock(spec=AsyncAuth) + config = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} + hmip_auth = HomematicipAuth(hass, config) + hmip_auth.auth = Mock(spec=AsyncAuth) with patch.object( - hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError - ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): - assert not await hap.async_checkbutton() - assert await hap.async_register() is False + hmip_auth.auth, "isRequestAcknowledged", side_effect=HmipConnectionError + ), patch.object( + hmip_auth.auth, "requestAuthToken", side_effect=HmipConnectionError + ): + assert not await hmip_auth.async_checkbutton() + assert await hmip_auth.async_register() is False -async def test_hap_setup_works(aioclient_mock): +async def test_hap_setup_works(): """Test a successful setup of a accesspoint.""" hass = Mock() entry = Mock() home = Mock() - entry.data = { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipHAP(hass, entry) - with patch.object(hap, "get_hap", return_value=mock_coro(home)): + entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} + hap = HomematicipHAP(hass, entry) + with patch.object(hap, "get_hap", return_value=home): assert await hap.async_setup() assert hap.home is home @@ -112,44 +96,27 @@ async def test_hap_setup_connection_error(): """Test a failed accesspoint setup.""" hass = Mock() entry = Mock() - entry.data = { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipHAP(hass, entry) - with patch.object( - hap, "get_hap", side_effect=errors.HmipcConnectionError - ), pytest.raises(ConfigEntryNotReady): + entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} + hap = HomematicipHAP(hass, entry) + with patch.object(hap, "get_hap", side_effect=HmipcConnectionError), pytest.raises( + ConfigEntryNotReady + ): await hap.async_setup() assert not hass.async_add_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls -async def test_hap_reset_unloads_entry_if_setup(): +async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap): """Test calling reset while the entry has been setup.""" - hass = Mock() - entry = Mock() - home = Mock() - home.disable_events = mock_coro_func() - entry.data = { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - } - hap = hmipc.HomematicipHAP(hass, entry) - with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() - - assert hap.home is home - assert not hass.services.async_register.mock_calls - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 - - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - await hap.async_reset() - - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8 + assert hass.data[HMIPC_DOMAIN][HAPID] == default_mock_hap + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) + assert len(config_entries) == 1 + # hap_reset is called during unload + await hass.config_entries.async_unload(config_entries[0].entry_id) + # entry is unloaded + assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert hass.data[HMIPC_DOMAIN] == {} async def test_hap_create(hass, hmip_config_entry, simple_mock_home): @@ -160,7 +127,7 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home): with patch( "homeassistant.components.homematicip_cloud.hap.AsyncHome", return_value=simple_mock_home, - ), patch.object(hap, "async_connect", return_value=mock_coro(None)): + ), patch.object(hap, "async_connect"): assert await hap.async_setup() @@ -185,11 +152,7 @@ async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): async def test_auth_create(hass, simple_mock_auth): """Mock AsyncAuth to execute get_auth.""" - config = { - const.HMIPC_HAPID: HAPID, - const.HMIPC_PIN: HAPPIN, - const.HMIPC_NAME: "hmip", - } + config = {HMIPC_HAPID: HAPID, HMIPC_PIN: HAPPIN, HMIPC_NAME: "hmip"} hmip_auth = HomematicipAuth(hass, config) assert hmip_auth @@ -204,11 +167,7 @@ async def test_auth_create(hass, simple_mock_auth): async def test_auth_create_exception(hass, simple_mock_auth): """Mock AsyncAuth to execute get_auth.""" - config = { - const.HMIPC_HAPID: HAPID, - const.HMIPC_PIN: HAPPIN, - const.HMIPC_NAME: "hmip", - } + config = {HMIPC_HAPID: HAPID, HMIPC_PIN: HAPPIN, HMIPC_NAME: "hmip"} hmip_auth = HomematicipAuth(hass, config) simple_mock_auth.connectionRequest.side_effect = HmipConnectionError assert hmip_auth diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index eb51c3ece38..ee63dba3c97 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,155 +1,119 @@ """Test HomematicIP Cloud setup process.""" -from unittest.mock import patch +from asynctest import CoroutineMock, Mock, patch -from homeassistant.components import homematicip_cloud as hmipc +from homeassistant.components.homematicip_cloud.const import ( + CONF_ACCESSPOINT, + CONF_AUTHTOKEN, + DOMAIN as HMIPC_DOMAIN, + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, +) +from homeassistant.components.homematicip_cloud.hap import HomematicipHAP +from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component -from tests.common import Mock, MockConfigEntry, mock_coro +from tests.common import MockConfigEntry async def test_config_with_accesspoint_passed_to_config_entry(hass): """Test that config for a accesspoint are loaded via config entry.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hmipc, "configured_haps", return_value=[] - ): - assert ( - await async_setup_component( - hass, - hmipc.DOMAIN, - { - hmipc.DOMAIN: { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "name", - } - }, - ) - is True - ) - # Flow started for the access point - assert len(mock_config_entries.flow.mock_calls) >= 2 + entry_config = { + CONF_ACCESSPOINT: "ABC123", + CONF_AUTHTOKEN: "123", + CONF_NAME: "name", + } + # no config_entry exists + assert len(hass.config_entries.async_entries(HMIPC_DOMAIN)) == 0 + # no acccesspoint exists + assert not hass.data.get(HMIPC_DOMAIN) + + assert ( + await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config}) + is True + ) + + # config_entry created for access point + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].data == { + "authtoken": "123", + "hapid": "ABC123", + "name": "name", + } + # defined access_point created for config_entry + assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP) async def test_config_already_registered_not_passed_to_config_entry(hass): """Test that an already registered accesspoint does not get imported.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hmipc, "configured_haps", return_value=["ABC123"] - ): - assert ( - await async_setup_component( - hass, - hmipc.DOMAIN, - { - hmipc.DOMAIN: { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "name", - } - }, - ) - is True - ) - # No flow started - assert not mock_config_entries.flow.mock_calls + mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"} + MockConfigEntry(domain=HMIPC_DOMAIN, data=mock_config).add_to_hass(hass) - -async def test_setup_entry_successful(hass): - """Test setup entry is successful.""" - entry = MockConfigEntry( - domain=hmipc.DOMAIN, - data={ - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - }, - ) - entry.add_to_hass(hass) - with patch.object(hmipc, "HomematicipHAP") as mock_hap: - instance = mock_hap.return_value - instance.async_setup.return_value = mock_coro(True) - instance.home.id = "1" - instance.home.modelType = "mock-type" - instance.home.name = "mock-name" - instance.home.currentAPVersion = "mock-ap-version" - - assert ( - await async_setup_component( - hass, - hmipc.DOMAIN, - { - hmipc.DOMAIN: { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "hmip", - } - }, - ) - is True - ) - - assert len(mock_hap.mock_calls) >= 2 - - -async def test_setup_defined_accesspoint(hass): - """Test we initiate config entry for the accesspoint.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hmipc, "configured_haps", return_value=[] - ): - mock_config_entries.flow.async_init.return_value = mock_coro() - assert ( - await async_setup_component( - hass, - hmipc.DOMAIN, - { - hmipc.DOMAIN: { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "hmip", - } - }, - ) - is True - ) - - assert len(mock_config_entries.flow.mock_calls) == 1 - assert mock_config_entries.flow.mock_calls[0][2]["data"] == { - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", + # one config_entry exists + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].data == { + "authtoken": "123", + "hapid": "ABC123", + "name": "name", } + # config_enty has no unique_id + assert not config_entries[0].unique_id + + entry_config = { + CONF_ACCESSPOINT: "ABC123", + CONF_AUTHTOKEN: "123", + CONF_NAME: "name", + } + assert ( + await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config}) + is True + ) + + # no new config_entry created / still one config_entry + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].data == { + "authtoken": "123", + "hapid": "ABC123", + "name": "name", + } + # config_enty updated with unique_id + assert config_entries[0].unique_id == "ABC123" async def test_unload_entry(hass): """Test being able to unload an entry.""" - entry = MockConfigEntry( - domain=hmipc.DOMAIN, - data={ - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", - }, - ) - entry.add_to_hass(hass) + mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"} + MockConfigEntry(domain=HMIPC_DOMAIN, data=mock_config).add_to_hass(hass) - with patch.object(hmipc, "HomematicipHAP") as mock_hap: + with patch("homeassistant.components.homematicip_cloud.HomematicipHAP") as mock_hap: instance = mock_hap.return_value - instance.async_setup.return_value = mock_coro(True) + instance.async_setup = CoroutineMock(return_value=True) instance.home.id = "1" instance.home.modelType = "mock-type" instance.home.name = "mock-name" instance.home.currentAPVersion = "mock-ap-version" + instance.async_reset = CoroutineMock(return_value=True) - assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True + assert await async_setup_component(hass, HMIPC_DOMAIN, {}) is True - assert len(mock_hap.return_value.mock_calls) >= 1 + assert mock_hap.return_value.mock_calls[0][0] == "async_setup" - mock_hap.return_value.async_reset.return_value = mock_coro(True) - assert await hmipc.async_unload_entry(hass, entry) - assert len(mock_hap.return_value.async_reset.mock_calls) == 1 - assert hass.data[hmipc.DOMAIN] == {} + assert hass.data[HMIPC_DOMAIN]["ABC123"] + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) + assert len(config_entries) == 1 + assert config_entries[0].state == ENTRY_STATE_LOADED + await hass.config_entries.async_unload(config_entries[0].entry_id) + assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert mock_hap.return_value.mock_calls[3][0] == "async_reset" + # entry is unloaded + assert hass.data[HMIPC_DOMAIN] == {} async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 212ae7499ab..58e6d8824dd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" +from ipaddress import ip_network import logging import unittest from unittest.mock import patch @@ -240,3 +241,20 @@ async def test_cors_defaults(hass): assert len(mock_setup.mock_calls) == 1 assert mock_setup.mock_calls[0][1][1] == ["https://cast.home-assistant.io"] + + +async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): + """Test that we store last working config.""" + config = { + http.CONF_SERVER_PORT: aiohttp_unused_port(), + "use_x_forwarded_for": True, + "trusted_proxies": ["192.168.1.100"], + } + + assert await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) + + await hass.async_start() + restored = await hass.components.http.async_get_last_config() + restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) + + assert restored == http.HTTP_SCHEMA(config) diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 29127ed964b..86de1ad8bd1 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -6,11 +6,16 @@ import pytest from requests.exceptions import ConnectionError from requests_mock import ANY -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler from homeassistant.components.huawei_lte.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_RECIPIENT, + CONF_URL, + CONF_USERNAME, +) from tests.common import MockConfigEntry @@ -20,59 +25,62 @@ FIXTURE_USER_INPUT = { CONF_PASSWORD: "secret", } - -@pytest.fixture -def flow(hass): - """Get flow to test.""" - flow = ConfigFlowHandler() - flow.hass = hass - flow.context = {} - return flow +FIXTURE_USER_INPUT_OPTIONS = { + CONF_NAME: DOMAIN, + CONF_RECIPIENT: "+15555551234", +} -async def test_show_set_form(flow): +async def test_show_set_form(hass): """Test that the setup form is served.""" - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" -async def test_urlize_plain_host(flow, requests_mock): +async def test_urlize_plain_host(hass, requests_mock): """Test that plain host or IP gets converted to a URL.""" requests_mock.request(ANY, ANY, exc=ConnectionError()) host = "192.168.100.1" user_input = {**FIXTURE_USER_INPUT, CONF_URL: host} - result = await flow.async_step_user(user_input=user_input) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" -async def test_already_configured(flow): +async def test_already_configured(hass): """Test we reject already configured devices.""" MockConfigEntry( domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" - ).add_to_hass(flow.hass) + ).add_to_hass(hass) - # Tweak URL a bit to check that doesn't fail duplicate detection - result = await flow.async_step_user( - user_input={ + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ **FIXTURE_USER_INPUT, + # Tweak URL a bit to check that doesn't fail duplicate detection CONF_URL: FIXTURE_USER_INPUT[CONF_URL].replace("http", "HTTP"), - } + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" -async def test_connection_error(flow, requests_mock): +async def test_connection_error(hass, requests_mock): """Test we show user form on connection error.""" - requests_mock.request(ANY, ANY, exc=ConnectionError()) - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -109,28 +117,32 @@ def login_requests_mock(requests_mock): (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), ), ) -async def test_login_error(flow, login_requests_mock, code, errors): +async def test_login_error(hass, login_requests_mock, code, errors): """Test we show user form with appropriate error on response failure.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"{code}", ) - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == errors -async def test_success(flow, login_requests_mock): +async def test_success(hass, login_requests_mock): """Test successful flow provides entry creation data.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"OK", ) - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] @@ -138,11 +150,14 @@ async def test_success(flow, login_requests_mock): assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] -async def test_ssdp(flow): +async def test_ssdp(hass): """Test SSDP discovery initiates config properly.""" url = "http://192.168.100.1/" - result = await flow.async_step_ssdp( - discovery_info={ + context = {"source": config_entries.SOURCE_SSDP} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context=context, + data={ ssdp.ATTR_SSDP_LOCATION: "http://192.168.100.1:60957/rootDesc.xml", ssdp.ATTR_SSDP_ST: "upnp:rootdevice", ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", @@ -154,9 +169,29 @@ async def test_ssdp(flow): ssdp.ATTR_UPNP_PRESENTATION_URL: url, ssdp.ATTR_UPNP_SERIAL: "00000000", ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - } + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - assert flow.context[CONF_URL] == url + assert context[CONF_URL] == url + + +async def test_options(hass): + """Test options produce expected data.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, data=FIXTURE_USER_INPUT, options=FIXTURE_USER_INPUT_OPTIONS + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + recipient = "+15555550000" + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_RECIPIENT: recipient} + ) + assert result["data"][CONF_NAME] == DOMAIN + assert result["data"][CONF_RECIPIENT] == [recipient] diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py new file mode 100644 index 00000000000..49cd953a697 --- /dev/null +++ b/tests/components/hue/conftest.py @@ -0,0 +1,11 @@ +"""Test helpers for Hue.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def no_request_delay(): + """Make the request refresh delay 0 for instant tests.""" + with patch("homeassistant.components.hue.light.REQUEST_REFRESH_DELAY", 0): + yield diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 5193d57ea6d..b1f6785b0a7 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,48 +1,76 @@ """Tests for Philips Hue config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import Mock import aiohue +from asynctest import CoroutineMock, patch import pytest import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry + + +@pytest.fixture(name="hue_setup", autouse=True) +def hue_setup_fixture(): + """Mock hue entry setup.""" + with patch("homeassistant.components.hue.async_setup_entry", return_value=True): + yield + + +def get_mock_bridge( + bridge_id="aabbccddeeff", host="1.2.3.4", mock_create_user=None, username=None +): + """Return a mock bridge.""" + mock_bridge = Mock() + mock_bridge.host = host + mock_bridge.username = username + mock_bridge.config.name = "Mock Bridge" + mock_bridge.id = bridge_id + + if not mock_create_user: + + async def create_user(username): + mock_bridge.username = username + + mock_create_user = create_user + + mock_bridge.create_user = mock_create_user + mock_bridge.initialize = CoroutineMock() + + return mock_bridge async def test_flow_works(hass): """Test config flow .""" - mock_bridge = Mock() - mock_bridge.host = "1.2.3.4" - mock_bridge.username = None - mock_bridge.config.name = "Mock Bridge" - mock_bridge.id = "aabbccddeeff" - - async def mock_create_user(username): - mock_bridge.username = username - - mock_bridge.create_user = mock_create_user - mock_bridge.initialize.return_value = mock_coro() - - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} + mock_bridge = get_mock_bridge() with patch( "homeassistant.components.hue.config_flow.discover_nupnp", - return_value=mock_coro([mock_bridge]), + return_value=[mock_bridge], ): - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "form" assert result["step_id"] == "link" - assert flow.context["unique_id"] == "aabbccddeeff" + flow = next( + ( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + ) + assert flow["context"]["unique_id"] == "aabbccddeeff" - result = await flow.async_step_link(user_input={}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" @@ -57,11 +85,12 @@ async def test_flow_works(hass): async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" aioclient_mock.get(const.API_NUPNP, json=[]) - flow = config_flow.HueFlowHandler() - flow.hass = hass - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "abort" + assert result["reason"] == "no_bridges" async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): @@ -72,12 +101,12 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): MockConfigEntry( domain="hue", unique_id="bla", data={"host": "1.2.3.4"} ).add_to_hass(hass) - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "abort" + assert result["reason"] == "all_configured" async def test_flow_one_bridge_discovered(hass, aioclient_mock): @@ -85,11 +114,10 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): aioclient_mock.get( const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] ) - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -108,10 +136,10 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): {"internalipaddress": "5.6.7.8", "id": "beer"}, ], ) - flow = config_flow.HueFlowHandler() - flow.hass = hass - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "form" assert result["step_id"] == "init" @@ -134,38 +162,72 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): MockConfigEntry( domain="hue", unique_id="bla", data={"host": "1.2.3.4"} ).add_to_hass(hass) - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "form" assert result["step_id"] == "link" - assert flow.bridge.host == "5.6.7.8" + flow = next( + ( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + ) + assert flow["context"]["unique_id"] == "beer" async def test_flow_timeout_discovery(hass): """Test config flow .""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - with patch( "homeassistant.components.hue.config_flow.discover_nupnp", side_effect=asyncio.TimeoutError, ): - result = await flow.async_step_init() + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) assert result["type"] == "abort" + assert result["reason"] == "discover_timeout" async def test_flow_link_timeout(hass): - """Test config flow .""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.bridge = Mock() + """Test config flow.""" + mock_bridge = get_mock_bridge( + mock_create_user=CoroutineMock(side_effect=asyncio.TimeoutError), + ) + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[mock_bridge], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) - with patch("aiohue.Bridge.create_user", side_effect=asyncio.TimeoutError): - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {"base": "linking"} + + +async def test_flow_link_unknown_error(hass): + """Test if a unknown error happend during the linking processes.""" + mock_bridge = get_mock_bridge(mock_create_user=CoroutineMock(side_effect=OSError),) + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[mock_bridge], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -174,13 +236,20 @@ async def test_flow_link_timeout(hass): async def test_flow_link_button_not_pressed(hass): """Test config flow .""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.bridge = Mock( - username=None, create_user=Mock(side_effect=aiohue.LinkButtonNotPressed) + mock_bridge = get_mock_bridge( + mock_create_user=CoroutineMock(side_effect=aiohue.LinkButtonNotPressed), ) + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[mock_bridge], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -189,12 +258,20 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.bridge = Mock() + mock_bridge = get_mock_bridge( + mock_create_user=CoroutineMock(side_effect=aiohue.RequestError), + ) + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=[mock_bridge], + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"} + ) - with patch("aiohue.Bridge.create_user", side_effect=aiohue.RequestError): - result = await flow.async_step_link({}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -203,16 +280,14 @@ async def test_flow_link_unknown_host(hass): async def test_bridge_ssdp(hass): """Test a bridge being discovered.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_ssdp( - { + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, ssdp.ATTR_UPNP_SERIAL: "1234", - } + }, ) assert result["type"] == "form" @@ -221,29 +296,57 @@ async def test_bridge_ssdp(hass): async def test_bridge_ssdp_discover_other_bridge(hass): """Test that discovery ignores other bridges.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_ssdp( - {ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"} + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, ) assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" async def test_bridge_ssdp_emulated_hue(hass): """Test if discovery info is from an emulated hue instance.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_ssdp( - { + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, ssdp.ATTR_UPNP_SERIAL: "1234", - } + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" + + +async def test_bridge_ssdp_missing_location(hass): + """Test if discovery info is missing a location attribute.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" + + +async def test_bridge_ssdp_missing_serial(hass): + """Test if discovery info is a serial attribute.""" + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + }, ) assert result["type"] == "abort" @@ -252,17 +355,15 @@ async def test_bridge_ssdp_emulated_hue(hass): async def test_bridge_ssdp_espalexa(hass): """Test if discovery info is from an Espalexa based device.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_ssdp( - { + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, ssdp.ATTR_UPNP_SERIAL: "1234", - } + }, ) assert result["type"] == "abort" @@ -275,27 +376,25 @@ async def test_bridge_ssdp_already_configured(hass): domain="hue", unique_id="1234", data={"host": "0.0.0.0"} ).add_to_hass(hass) - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", + }, + ) - with pytest.raises(data_entry_flow.AbortFlow): - await flow.async_step_ssdp( - { - ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, - ssdp.ATTR_UPNP_SERIAL: "1234", - } - ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_import_with_no_config(hass): """Test importing a host without an existing config file.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_import({"host": "0.0.0.0"}) + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "import"}, data={"host": "0.0.0.0"}, + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -319,11 +418,9 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): assert len(hass.config_entries.async_entries("hue")) == 2 - bridge = Mock() - bridge.username = "username-abc" - bridge.config.name = "Mock Bridge" - bridge.host = "0.0.0.0" - bridge.id = "id-1234" + bridge = get_mock_bridge( + bridge_id="id-1234", host="2.2.2.2", username="username-abc" + ) with patch( "aiohue.Bridge", return_value=bridge, @@ -335,19 +432,15 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): assert result["type"] == "form" assert result["step_id"] == "link" - with patch( - "homeassistant.components.hue.config_flow.authenticate_bridge", - return_value=mock_coro(), - ), patch( - "homeassistant.components.hue.async_setup_entry", - side_effect=lambda _, _2: mock_coro(True), + with patch("homeassistant.components.hue.config_flow.authenticate_bridge"), patch( + "homeassistant.components.hue.async_unload_entry", return_value=True ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" assert result["data"] == { - "host": "0.0.0.0", + "host": "2.2.2.2", "username": "username-abc", } entries = hass.config_entries.async_entries("hue") @@ -359,34 +452,88 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): async def test_bridge_homekit(hass): """Test a bridge being discovered via HomeKit.""" - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_homekit( - { + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "homekit"}, + data={ "host": "0.0.0.0", "serial": "1234", "manufacturerURL": config_flow.HUE_MANUFACTURERURL, "properties": {"id": "aa:bb:cc:dd:ee:ff"}, - } + }, ) assert result["type"] == "form" assert result["step_id"] == "link" +async def test_bridge_import_already_configured(hass): + """Test if a import flow aborts if host is already configured.""" + MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "import"}, + data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + async def test_bridge_homekit_already_configured(hass): """Test if a HomeKit discovered bridge has already been configured.""" MockConfigEntry( domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} ).add_to_hass(hass) - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "homekit"}, + data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) - with pytest.raises(data_entry_flow.AbortFlow): - await flow.async_step_homekit( - {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}} - ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_ssdp_discovery_update_configuration(hass): + """Test if a discovered bridge is configured and updated with new host.""" + entry = MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "ssdp"}, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://1.1.1.1/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "1.1.1.1" + + +async def test_homekit_discovery_update_configuration(hass): + """Test if a discovered bridge is configured and updated with new host.""" + entry = MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": "homekit"}, + data={"host": "1.1.1.1", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "1.1.1.1" diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 0f3e197b979..df3fe5f8998 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,11 +179,13 @@ LIGHT_GAMUT_TYPE = "A" def mock_bridge(hass): """Mock a Hue bridge.""" bridge = Mock( + hass=hass, available=True, authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), + reset_jobs=[], spec=hue.HueBridge, ) bridge.mock_requests = [] @@ -218,7 +220,6 @@ def mock_bridge(hass): async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) - hass.data[hue.DOMAIN] = {"mock-host": mock_bridge} config_entry = config_entries.ConfigEntry( 1, hue.DOMAIN, @@ -228,6 +229,8 @@ async def setup_bridge(hass, mock_bridge): config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) + mock_bridge.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group await hass.async_block_till_done() @@ -363,8 +366,8 @@ async def test_new_group_discovered(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 3 new_group = hass.states.get("light.group_3") @@ -443,8 +446,8 @@ async def test_group_removed(hass, mock_bridge): "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 1 group = hass.states.get("light.group_1") @@ -524,8 +527,8 @@ async def test_other_group_update(hass, mock_bridge): await hass.services.async_call( "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True ) - # 2x group update, 2x light update, 1 turn on request - assert len(mock_bridge.mock_requests) == 5 + # 2x group update, 1x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 2 group_2 = hass.states.get("light.group_2") @@ -599,7 +602,6 @@ async def test_update_timeout(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False async def test_update_unauthorized(hass, mock_bridge): @@ -701,7 +703,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=False), is_group=False, ) @@ -715,7 +717,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=True), is_group=False, ) @@ -729,7 +731,7 @@ def test_available(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(allow_unreachable=False), is_group=True, ) @@ -746,7 +748,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) @@ -760,7 +762,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) @@ -774,7 +776,7 @@ def test_hs_color(): colorgamuttype=LIGHT_GAMUT_TYPE, colorgamut=LIGHT_GAMUT, ), - request_bridge_update=None, + coordinator=Mock(failed_last_update=False), bridge=Mock(), is_group=False, ) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ad927767c30..78255116831 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,7 +1,6 @@ """Philips Hue sensors platform tests.""" import asyncio from collections import deque -import datetime import logging from unittest.mock import Mock @@ -252,16 +251,19 @@ SENSOR_RESPONSE = { } -def create_mock_bridge(): +def create_mock_bridge(hass): """Create a mock Hue bridge.""" bridge = Mock( + hass=hass, available=True, authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), + reset_jobs=[], spec=hue.HueBridge, ) + bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) bridge.mock_requests = [] # We're using a deque so we can schedule multiple responses # and also means that `popleft()` will blow up if we get more updates @@ -289,13 +291,7 @@ def create_mock_bridge(): @pytest.fixture def mock_bridge(hass): """Mock a Hue bridge.""" - return create_mock_bridge() - - -@pytest.fixture -def increase_scan_interval(hass): - """Increase the SCAN_INTERVAL to prevent unexpected scans during tests.""" - hue_sensor_base.SensorManager.SCAN_INTERVAL = datetime.timedelta(days=365) + return create_mock_bridge(hass) async def setup_bridge(hass, mock_bridge, hostname=None): @@ -303,7 +299,6 @@ async def setup_bridge(hass, mock_bridge, hostname=None): if hostname is None: hostname = "mock-host" hass.config.components.add(hue.DOMAIN) - hass.data[hue.DOMAIN] = {hostname: mock_bridge} config_entry = config_entries.ConfigEntry( 1, hue.DOMAIN, @@ -313,6 +308,8 @@ async def setup_bridge(hass, mock_bridge, hostname=None): config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) + mock_bridge.config_entry = config_entry + hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") # and make sure it completes before going further @@ -330,7 +327,7 @@ async def test_no_sensors(hass, mock_bridge): async def test_sensors_with_multiple_bridges(hass, mock_bridge): """Test the update_items function with some sensors.""" - mock_bridge_2 = create_mock_bridge() + mock_bridge_2 = create_mock_bridge(hass) mock_bridge_2.mock_sensor_responses.append( { "1": PRESENCE_SENSOR_3_PRESENT, @@ -412,11 +409,7 @@ async def test_new_sensor_discovered(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") - sm = hass.data[hue.DOMAIN][sm_key] - await sm.async_update_items() - - # To flush out the service call to update the group + await mock_bridge.sensor_manager.coordinator.async_refresh() await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 2 @@ -443,9 +436,7 @@ async def test_sensor_removed(hass, mock_bridge): mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) # Force updates to run again - sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") - sm = hass.data[hue.DOMAIN][sm_key] - await sm.async_update_items() + await mock_bridge.sensor_manager.coordinator.async_refresh() # To flush out the service call to update the group await hass.async_block_till_done() @@ -466,7 +457,6 @@ async def test_update_timeout(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False async def test_update_unauthorized(hass, mock_bridge): diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 5555150befc..6091d1cf1da 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -5,28 +5,26 @@ from pyicloud.exceptions import PyiCloudFailedLoginException import pytest from homeassistant import data_entry_flow -from homeassistant.components.icloud import config_flow from homeassistant.components.icloud.config_flow import ( CONF_TRUSTED_DEVICE, CONF_VERIFICATION_CODE, ) from homeassistant.components.icloud.const import ( - CONF_ACCOUNT_NAME, CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, DEFAULT_GPS_ACCURACY_THRESHOLD, DEFAULT_MAX_INTERVAL, DOMAIN, ) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry USERNAME = "username@me.com" +USERNAME_2 = "second_username@icloud.com" PASSWORD = "password" -ACCOUNT_NAME = "Account name 1 2 3" -ACCOUNT_NAME_FROM_USERNAME = None MAX_INTERVAL = 15 GPS_ACCURACY_THRESHOLD = 250 @@ -41,7 +39,10 @@ def mock_controller_service(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: - service_mock.return_value.requires_2fa = True + service_mock.return_value.requires_2sa = True + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=True) yield service_mock @@ -51,7 +52,7 @@ def mock_controller_service_with_cookie(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: - service_mock.return_value.requires_2fa = False + service_mock.return_value.requires_2sa = False service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) service_mock.return_value.validate_verification_code = Mock(return_value=True) @@ -64,7 +65,7 @@ def mock_controller_service_send_verification_code_failed(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: - service_mock.return_value.requires_2fa = False + service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=False) yield service_mock @@ -76,31 +77,26 @@ def mock_controller_service_validate_verification_code_failed(): with patch( "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: - service_mock.return_value.requires_2fa = False + service_mock.return_value.requires_2sa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=True) service_mock.return_value.validate_verification_code = Mock(return_value=False) yield service_mock -def init_config_flow(hass: HomeAssistantType): - """Init a configuration flow.""" - flow = config_flow.IcloudFlowHandler() - flow.hass = hass - return flow - - async def test_user(hass: HomeAssistantType, service: MagicMock): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" # test with all provided - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -110,41 +106,42 @@ async def test_user_with_cookie( hass: HomeAssistantType, service_with_cookie: MagicMock ): """Test user config with presence of a cookie.""" - flow = init_config_flow(hass) - # test with all provided - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD async def test_import(hass: HomeAssistantType, service: MagicMock): """Test import step.""" - flow = init_config_flow(hass) - # import with username and password - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "trusted_device" # import with all - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: USERNAME_2, CONF_PASSWORD: PASSWORD, - CONF_ACCOUNT_NAME: ACCOUNT_NAME, CONF_MAX_INTERVAL: MAX_INTERVAL, CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - } + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "trusted_device" @@ -154,82 +151,102 @@ async def test_import_with_cookie( hass: HomeAssistantType, service_with_cookie: MagicMock ): """Test import step with presence of a cookie.""" - flow = init_config_flow(hass) - # import with username and password - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD # import with all - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: USERNAME_2, CONF_PASSWORD: PASSWORD, - CONF_ACCOUNT_NAME: ACCOUNT_NAME, CONF_MAX_INTERVAL: MAX_INTERVAL, CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, - } + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME + assert result["result"].unique_id == USERNAME_2 + assert result["title"] == USERNAME_2 + assert result["data"][CONF_USERNAME] == USERNAME_2 assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME assert result["data"][CONF_MAX_INTERVAL] == MAX_INTERVAL assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == GPS_ACCURACY_THRESHOLD +async def test_two_accounts_setup( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test to setup two accounts.""" + MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + unique_id=USERNAME, + ).add_to_hass(hass) + + # import with username and password + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME_2, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME_2 + assert result["title"] == USERNAME_2 + assert result["data"][CONF_USERNAME] == USERNAME_2 + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + async def test_abort_if_already_setup(hass: HomeAssistantType): """Test we abort if the account is already setup.""" - flow = init_config_flow(hass) MockConfigEntry( - domain=DOMAIN, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + domain=DOMAIN, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + unique_id=USERNAME, ).add_to_hass(hass) # Should fail, same USERNAME (import) - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "username_exists" - - # Should fail, same ACCOUNT_NAME (import) - result = await flow.async_step_import( - { - CONF_USERNAME: "other_username@icloud.com", - CONF_PASSWORD: PASSWORD, - CONF_ACCOUNT_NAME: ACCOUNT_NAME_FROM_USERNAME, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "username_exists" + assert result["reason"] == "already_configured" # Should fail, same USERNAME (flow) - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_USERNAME: "username_exists"} + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_login_failed(hass: HomeAssistantType): """Test when we have errors during login.""" - flow = init_config_flow(hass) - with patch( "pyicloud.base.PyiCloudService.authenticate", side_effect=PyiCloudFailedLoginException(), ): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_USERNAME: "login"} @@ -237,20 +254,28 @@ async def test_login_failed(hass: HomeAssistantType): async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): """Test trusted_device step.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) - result = await flow.async_step_trusted_device() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_TRUSTED_DEVICE async def test_trusted_device_success(hass: HomeAssistantType, service: MagicMock): """Test trusted_device step success.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) - result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_TRUSTED_DEVICE: 0} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -259,38 +284,56 @@ async def test_send_verification_code_failed( hass: HomeAssistantType, service_send_verification_code_failed: MagicMock ): """Test when we have errors during send_verification_code.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) - result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_TRUSTED_DEVICE: 0} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} -async def test_verification_code(hass: HomeAssistantType): +async def test_verification_code(hass: HomeAssistantType, service: MagicMock): """Test verification_code step.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_TRUSTED_DEVICE: 0} + ) - result = await flow.async_step_verification_code() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_VERIFICATION_CODE -async def test_verification_code_success( - hass: HomeAssistantType, service_with_cookie: MagicMock -): +async def test_verification_code_success(hass: HomeAssistantType, service: MagicMock): """Test verification_code step success.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_TRUSTED_DEVICE: 0} + ) + service.return_value.requires_2sa = False - result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_ACCOUNT_NAME] is None assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD @@ -299,10 +342,18 @@ async def test_validate_verification_code_failed( hass: HomeAssistantType, service_validate_verification_code_failed: MagicMock ): """Test when we have errors during validate_verification_code.""" - flow = init_config_flow(hass) - await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_TRUSTED_DEVICE: 0} + ) - result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_VERIFICATION_CODE: "0"} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 3503fcfb9a2..39cbb8d583e 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -77,8 +77,6 @@ class TestImageProcessing: ) def test_get_image_from_camera(self, mock_camera): """Grab an image from camera entity.""" - self.hass.start() - common.scan(self.hass, entity_id="image_processing.test") self.hass.block_till_done() diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 1ab7e9a3d13..c0bdad3eacd 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -240,14 +240,13 @@ async def test_reload(hass, hass_admin_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_admin_user.id), - ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) @@ -334,3 +333,22 @@ async def test_ws_delete(hass, hass_ws_client, storage_setup): state = hass.states.get(input_entity_id) assert state is None assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 6908c4fc5f1..67a23a61d8b 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -9,18 +9,64 @@ import voluptuous as vol from homeassistant.components.input_datetime import ( ATTR_DATE, ATTR_DATETIME, + ATTR_EDITABLE, ATTR_TIME, + CONF_HAS_DATE, + CONF_HAS_TIME, + CONF_ID, + CONF_INITIAL, + CONF_NAME, + DEFAULT_TIME, DOMAIN, SERVICE_RELOAD, SERVICE_SET_DATETIME, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME from homeassistant.core import Context, CoreState, State from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache +INITIAL_DATE = "2020-01-10" +INITIAL_TIME = "23:45:56" +INITIAL_DATETIME = f"{INITIAL_DATE} {INITIAL_TIME}" + + +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + CONF_ID: "from_storage", + CONF_NAME: "datetime from storage", + CONF_INITIAL: INITIAL_DATETIME, + CONF_HAS_DATE: True, + CONF_HAS_TIME: True, + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + async def async_set_date_and_time(hass, entity_id, dt_value): """Set date and / or time of input_datetime.""" @@ -314,6 +360,7 @@ async def test_input_datetime_context(hass, hass_admin_user): async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) assert await async_setup_component( hass, @@ -321,19 +368,25 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): { DOMAIN: { "dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}, + "dt3": {CONF_HAS_TIME: True, CONF_HAS_DATE: True}, } }, ) - assert count_start + 1 == len(hass.states.async_entity_ids()) + assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") + state_3 = hass.states.get("input_datetime.dt3") dt_obj = datetime.datetime(2019, 1, 1, 0, 0) assert state_1 is not None assert state_2 is None + assert state_3 is not None assert str(dt_obj.date()) == state_1.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") == f"{DOMAIN}.dt3" with patch( "homeassistant.config.load_yaml_config_file", @@ -345,29 +398,195 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") + state_3 = hass.states.get("input_datetime.dt3") - dt_obj = datetime.datetime(2019, 1, 1, 23, 32) assert state_1 is not None assert state_2 is not None - assert str(dt_obj.time()) == state_1.state + assert state_3 is None + assert str(DEFAULT_TIME) == state_1.state assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state + + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt3") is None + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.datetime_from_storage") + assert state.state == INITIAL_DATETIME + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_HAS_DATE: True, + CONF_HAS_TIME: True, + CONF_NAME: "yaml datetime", + CONF_INITIAL: "2001-01-02 12:34:56", + } + } + } + ) + + state = hass.states.get(f"{DOMAIN}.datetime_from_storage") + assert state.state == INITIAL_DATETIME + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.state == "2001-01-02 12:34:56" + assert not state.attributes[ATTR_EDITABLE] + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": {CONF_HAS_DATE: True}}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "datetime from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.datetime_from_storage" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) == input_entity_id + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_update(hass, hass_ws_client, storage_setup): + """Test updating min/max updates the state.""" + + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.datetime_from_storage" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_FRIENDLY_NAME] == "datetime from storage" + assert state.state == INITIAL_DATETIME + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) == input_entity_id + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + ATTR_NAME: "even newer name", + CONF_HAS_DATE: False, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == INITIAL_TIME + assert state.attributes[ATTR_FRIENDLY_NAME] == "even newer name" + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_datetime" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + CONF_NAME: "New DateTime", + CONF_INITIAL: "1991-01-02 01:02:03", + CONF_HAS_DATE: True, + CONF_HAS_TIME: True, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "1991-01-02 01:02:03" + assert state.attributes[ATTR_FRIENDLY_NAME] == "New DateTime" + assert state.attributes[ATTR_EDITABLE] + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 6d032b639cf..8331e1374c8 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -12,15 +12,57 @@ from homeassistant.components.input_number import ( SERVICE_RELOAD, SERVICE_SET_VALUE, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, +) from homeassistant.core import Context, CoreState, State from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "initial": 10, + "name": "from storage", + "max": 100, + "min": 0, + "step": 1, + "mode": "slider", + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + @bind_hass def set_value(hass, entity_id, value): """Set input_number to value. @@ -258,19 +300,33 @@ async def test_input_number_context(hass, hass_admin_user): async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {"test_1": {"initial": 50, "min": 0, "max": 51}}} + hass, + DOMAIN, + { + DOMAIN: { + "test_1": {"initial": 50, "min": 0, "max": 51}, + "test_3": {"initial": 10, "min": 0, "max": 15}, + } + }, ) - assert count_start + 1 == len(hass.states.async_entity_ids()) + assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_number.test_1") state_2 = hass.states.get("input_number.test_2") + state_3 = hass.states.get("input_number.test_3") assert state_1 is not None assert state_2 is None + assert state_3 is not None assert 50 == float(state_1.state) + assert 10 == float(state_3.state) + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None with patch( "homeassistant.config.load_yaml_config_file", @@ -282,28 +338,227 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) state_1 = hass.states.get("input_number.test_1") state_2 = hass.states.get("input_number.test_2") + state_3 = hass.states.get("input_number.test_3") assert state_1 is not None assert state_2 is not None - assert 40 == float(state_1.state) + assert state_3 is None + assert 50 == float(state_1.state) assert 20 == float(state_2.state) + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.from_storage") + assert float(state.state) == 10 + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup( + config={ + DOMAIN: { + "from_yaml": { + "min": 1, + "max": 10, + "initial": 5, + "step": 1, + "mode": "slider", + } + } + } + ) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert float(state.state) == 10 + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert float(state.state) == 5 + assert not state.attributes.get(ATTR_EDITABLE) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup( + config={ + DOMAIN: { + "from_yaml": { + "min": 1, + "max": 10, + "initial": 5, + "step": 1, + "mode": "slider", + } + } + } + ) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_update_min_max(hass, hass_ws_client, storage_setup): + """Test updating min/max updates the state.""" + + items = [ + { + "id": "from_storage", + "name": "from storage", + "max": 100, + "min": 0, + "step": 1, + "mode": "slider", + } + ] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert state.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/update", f"{DOMAIN}_id": f"{input_id}", "min": 9} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert float(state.state) == 9 + + await client.send_json( + { + "id": 7, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + "max": 5, + "min": 0, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert float(state.state) == 5 + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + "max": 20, + "min": 0, + "initial": 10, + "step": 1, + "mode": "slider", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert float(state.state) == 10 + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 8fda80cd3d2..5c470ca5bfc 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.input_select import ( ATTR_OPTION, ATTR_OPTIONS, + CONF_INITIAL, DOMAIN, SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, @@ -14,19 +15,54 @@ from homeassistant.components.input_select import ( SERVICE_SET_OPTIONS, ) from homeassistant.const import ( + ATTR_EDITABLE, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + ATTR_NAME, SERVICE_RELOAD, ) from homeassistant.core import Context, State from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "name": "from storage", + "options": ["storage option 1", "storage option 2"], + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + @bind_hass def select_option(hass, entity_id, option): """Set value of input_select. @@ -329,6 +365,7 @@ async def test_input_select_context(hass, hass_admin_user): async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) assert await async_setup_component( hass, @@ -358,6 +395,9 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_3 is None assert "middle option" == state_1.state assert "an option" == state_2.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None with patch( "homeassistant.config.load_yaml_config_file", @@ -375,21 +415,20 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) @@ -400,5 +439,178 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is None assert state_2 is not None assert state_3 is not None - assert "reloaded option" == state_2.state + assert "an option" == state_2.state assert "newer option" == state_3.state + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "storage option 1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup( + config={DOMAIN: {"from_yaml": {"options": ["yaml option", "other option"]}}} + ) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "storage option 1" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.state == "yaml option" + assert not state.attributes.get(ATTR_EDITABLE) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup( + config={DOMAIN: {"from_yaml": {"options": ["yaml option"]}}} + ) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_update(hass, hass_ws_client, storage_setup): + """Test updating min/max updates the state.""" + + items = [ + { + "id": "from_storage", + "name": "from storage", + "options": ["yaml update 1", "yaml update 2"], + } + ] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_OPTIONS] == ["yaml update 1", "yaml update 2"] + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + "options": ["new option", "newer option"], + CONF_INITIAL: "newer option", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_OPTIONS] == ["new option", "newer option"] + + await client.send_json( + { + "id": 7, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + "options": ["new option", "no newer option"], + } + ) + resp = await client.receive_json() + assert not resp["success"] + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + "options": ["new option", "even newer option"], + "initial": "even newer option", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "even newer option" + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py index 469c258cb4b..ed1f9f45e43 100644 --- a/tests/components/input_select/test_reproduce_state.py +++ b/tests/components/input_select/test_reproduce_state.py @@ -68,5 +68,5 @@ async def test_reproducing_states(hass, caplog): ) # These should fail if options weren't changed to VALID_OPTION_SET2 - assert hass.states.get(ENTITY).attributes == {"options": VALID_OPTION_SET2} + assert hass.states.get(ENTITY).attributes["options"] == VALID_OPTION_SET2 assert hass.states.get(ENTITY).state == VALID_OPTION5 diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 8835128d672..304f7e09495 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -4,15 +4,71 @@ from unittest.mock import patch import pytest -from homeassistant.components.input_text import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD +from homeassistant.components.input_text import ( + ATTR_MAX, + ATTR_MIN, + ATTR_MODE, + ATTR_VALUE, + CONF_INITIAL, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + DOMAIN, + MODE_TEXT, + SERVICE_SET_VALUE, +) +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + SERVICE_RELOAD, +) from homeassistant.core import Context, CoreState, State from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache +TEST_VAL_MIN = 2 +TEST_VAL_MAX = 22 + + +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "name": "from storage", + "initial": "loaded from storage", + ATTR_MAX: TEST_VAL_MAX, + ATTR_MIN: TEST_VAL_MIN, + ATTR_MODE: MODE_TEXT, + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + @bind_hass def set_value(hass, entity_id, value): @@ -109,7 +165,7 @@ async def test_restore_state(hass): hass.state = CoreState.starting assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {"b1": None, "b2": {"min": 0, "max": 10}}}, + hass, DOMAIN, {DOMAIN: {"b1": None, "b2": {"min": 0, "max": 10}}} ) state = hass.states.get("input_text.b1") @@ -192,6 +248,11 @@ async def test_config_none(hass): assert state assert str(state.state) == "unknown" + # with empty config we still should have the defaults + assert state.attributes[ATTR_MODE] == MODE_TEXT + assert state.attributes[ATTR_MAX] == CONF_MAX_VALUE + assert state.attributes[ATTR_MIN] == CONF_MIN_VALUE + async def test_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" @@ -214,32 +275,33 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_3 is None assert "test 1" == state_1.state assert "test 2" == state_2.state + assert state_1.attributes[ATTR_MIN] == 0 + assert state_2.attributes[ATTR_MAX] == 100 with patch( "homeassistant.config.load_yaml_config_file", autospec=True, return_value={ DOMAIN: { - "test_2": {"initial": "test reloaded"}, - "test_3": {"initial": "test 3"}, + "test_2": {"initial": "test reloaded", ATTR_MIN: 12}, + "test_3": {"initial": "test 3", ATTR_MAX: 21}, } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) @@ -250,5 +312,193 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is None assert state_2 is not None assert state_3 is not None - assert "test reloaded" == state_2.state - assert "test 3" == state_3.state + assert state_2.attributes[ATTR_MIN] == 12 + assert state_3.attributes[ATTR_MAX] == 21 + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "loaded from storage" + assert state.attributes.get(ATTR_EDITABLE) + assert state.attributes[ATTR_MAX] == TEST_VAL_MAX + assert state.attributes[ATTR_MIN] == TEST_VAL_MIN + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup( + config={ + DOMAIN: { + "from_yaml": { + "initial": "yaml initial value", + ATTR_MODE: MODE_TEXT, + ATTR_MAX: 33, + ATTR_MIN: 3, + ATTR_NAME: "yaml friendly name", + } + } + } + ) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "loaded from storage" + assert state.attributes.get(ATTR_EDITABLE) + assert state.attributes[ATTR_MAX] == TEST_VAL_MAX + assert state.attributes[ATTR_MIN] == TEST_VAL_MIN + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state.state == "yaml initial value" + assert not state.attributes[ATTR_EDITABLE] + assert state.attributes[ATTR_MAX] == 33 + assert state.attributes[ATTR_MIN] == 3 + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup( + config={ + DOMAIN: { + "from_yaml": { + "initial": "yaml initial value", + ATTR_MODE: MODE_TEXT, + ATTR_MAX: 33, + ATTR_MIN: 3, + ATTR_NAME: "yaml friendly name", + } + } + } + ) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_update(hass, hass_ws_client, storage_setup): + """Test updating min/max updates the state.""" + + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_MODE] == MODE_TEXT + assert state.state == "loaded from storage" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + ATTR_NAME: "even newer name", + CONF_INITIAL: "newer option", + ATTR_MIN: 6, + ATTR_MODE: "password", + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "loaded from storage" + assert state.attributes[ATTR_FRIENDLY_NAME] == "even newer name" + assert state.attributes[ATTR_MODE] == "password" + assert state.attributes[ATTR_MIN] == 6 + assert state.attributes[ATTR_MAX] == TEST_VAL_MAX + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + "initial": "even newer option", + ATTR_MAX: 44, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "even newer option" + assert state.attributes[ATTR_FRIENDLY_NAME] == "New Input" + assert state.attributes[ATTR_EDITABLE] + assert state.attributes[ATTR_MAX] == 44 + assert state.attributes[ATTR_MIN] == 0 + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index de13d3c94b2..ead4654cba2 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -18,73 +18,99 @@ from tests.common import MockConfigEntry, mock_coro TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} -class MockStation: - """Mock Station from pyipma.""" +class MockLocation: + """Mock Location from pyipma.""" - async def observation(self): + async def observation(self, api): """Mock Observation.""" Observation = namedtuple( "Observation", [ - "temperature", + "accumulated_precipitation", "humidity", - "windspeed", - "winddirection", - "precipitation", "pressure", - "description", + "radiation", + "temperature", + "wind_direction", + "wind_intensity_km", ], ) - return Observation(18, 71.0, 3.94, "NW", 0, 1000.0, "---") + return Observation(0.0, 71.0, 1000.0, 0.0, 18.0, "NW", 3.94) - async def forecast(self): + async def forecast(self, api): """Mock Forecast.""" Forecast = namedtuple( "Forecast", [ - "precipitaProb", - "tMin", - "tMax", - "predWindDir", - "idWeatherType", - "classWindSpeed", - "longitude", - "forecastDate", - "classPrecInt", - "latitude", - "description", + "feels_like_temperature", + "forecast_date", + "forecasted_hours", + "humidity", + "max_temperature", + "min_temperature", + "precipitation_probability", + "temperature", + "update_date", + "weather_type", + "wind_direction", + "wind_strength", ], ) return [ Forecast( - 73.0, - 13.7, - 18.7, - "NW", - 6, - 2, - -8.64, - "2018-05-31", - 2, - 40.61, - "Aguaceiros, com vento Moderado de Noroeste", - ) + None, + "2020-01-15T00:00:00", + 24, + None, + 16.2, + 10.6, + "100.0", + 13.4, + "2020-01-15T07:51:00", + 9, + "S", + None, + ), + Forecast( + "7.7", + "2020-01-15T02:00:00", + 1, + "86.9", + None, + None, + "-99.0", + 10.6, + "2020-01-15T07:51:00", + 10, + "S", + "32.7", + ), ] @property - def local(self): + def name(self): """Mock location.""" return "HomeTown" @property - def latitude(self): + def station_latitude(self): """Mock latitude.""" return 0 @property - def longitude(self): + def global_id_local(self): + """Mock global identifier of the location.""" + return 1130600 + + @property + def id_station(self): + """Mock identifier of the station.""" + return 1200545 + + @property + def station_longitude(self): """Mock longitude.""" return 0 @@ -92,8 +118,8 @@ class MockStation: async def test_setup_configuration(hass): """Test for successfully setting up the IPMA platform.""" with patch( - "homeassistant.components.ipma.weather.async_get_station", - return_value=mock_coro(MockStation()), + "homeassistant.components.ipma.weather.async_get_location", + return_value=mock_coro(MockLocation()), ): assert await async_setup_component( hass, weather.DOMAIN, {"weather": {"name": "HomeTown", "platform": "ipma"}} @@ -115,8 +141,8 @@ async def test_setup_configuration(hass): async def test_setup_config_flow(hass): """Test for successfully setting up the IPMA platform.""" with patch( - "homeassistant.components.ipma.weather.async_get_station", - return_value=mock_coro(MockStation()), + "homeassistant.components.ipma.weather.async_get_location", + return_value=mock_coro(MockLocation()), ): entry = MockConfigEntry(domain="ipma", data=TEST_CONFIG) await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index a3cf57a7dbe..a737396cca8 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -30,7 +30,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 7a560dd781d..24645a32611 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -35,7 +35,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index dd8320c166e..969b4278aeb 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -35,7 +35,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py index 2b90c778a8f..8278a77d4d0 100644 --- a/tests/components/linky/test_config_flow.py +++ b/tests/components/linky/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Linky config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from pylinky.exceptions import ( PyLinkyAccessException, @@ -10,13 +10,15 @@ from pylinky.exceptions import ( import pytest from homeassistant import data_entry_flow -from homeassistant.components.linky import config_flow from homeassistant.components.linky.const import DEFAULT_TIMEOUT, DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry -USERNAME = "username" +USERNAME = "username@hotmail.fr" +USERNAME_2 = "username@free.fr" PASSWORD = "password" TIMEOUT = 20 @@ -24,145 +26,158 @@ TIMEOUT = 20 @pytest.fixture(name="login") def mock_controller_login(): """Mock a successful login.""" - with patch("pylinky.client.LinkyClient.login", return_value=True): - yield + with patch( + "homeassistant.components.linky.config_flow.LinkyClient" + ) as service_mock: + service_mock.return_value.login = Mock(return_value=True) + service_mock.return_value.close_session = Mock(return_value=None) + yield service_mock @pytest.fixture(name="fetch_data") def mock_controller_fetch_data(): """Mock a successful get data.""" - with patch("pylinky.client.LinkyClient.fetch_data", return_value={}): - yield + with patch( + "homeassistant.components.linky.config_flow.LinkyClient" + ) as service_mock: + service_mock.return_value.fetch_data = Mock(return_value={}) + service_mock.return_value.close_session = Mock(return_value=None) + yield service_mock -@pytest.fixture(name="close_session") -def mock_controller_close_session(): - """Mock a successful closing session.""" - with patch("pylinky.client.LinkyClient.close_session", return_value=None): - yield - - -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.LinkyFlowHandler() - flow.hass = hass - return flow - - -async def test_user(hass, login, fetch_data, close_session): +async def test_user(hass: HomeAssistantType, login, fetch_data): """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" # test with all provided - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT -async def test_import(hass, login, fetch_data, close_session): +async def test_import(hass: HomeAssistantType, login, fetch_data): """Test import step.""" - flow = init_config_flow(hass) - # import with username and password - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT # import with all - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_TIMEOUT: TIMEOUT} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: USERNAME_2, + CONF_PASSWORD: PASSWORD, + CONF_TIMEOUT: TIMEOUT, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME + assert result["result"].unique_id == USERNAME_2 + assert result["title"] == USERNAME_2 + assert result["data"][CONF_USERNAME] == USERNAME_2 assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_TIMEOUT] == TIMEOUT -async def test_abort_if_already_setup(hass, login, fetch_data, close_session): +async def test_abort_if_already_setup(hass: HomeAssistantType, login, fetch_data): """Test we abort if Linky is already setup.""" - flow = init_config_flow(hass) MockConfigEntry( - domain=DOMAIN, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + domain=DOMAIN, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + unique_id=USERNAME, ).add_to_hass(hass) # Should fail, same USERNAME (import) - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "username_exists" + assert result["reason"] == "already_configured" # Should fail, same USERNAME (flow) - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_login_failed(hass: HomeAssistantType, login): + """Test when we have errors during login.""" + login.return_value.login.side_effect = PyLinkyAccessException() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_USERNAME: "username_exists"} + assert result["errors"] == {"base": "access"} + hass.config_entries.flow.async_abort(result["flow_id"]) + + login.return_value.login.side_effect = PyLinkyWrongLoginException() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "wrong_login"} + hass.config_entries.flow.async_abort(result["flow_id"]) -async def test_abort_on_login_failed(hass, close_session): - """Test when we have errors during login.""" - flow = init_config_flow(hass) - - with patch( - "pylinky.client.LinkyClient.login", side_effect=PyLinkyAccessException() - ): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "access"} - - with patch( - "pylinky.client.LinkyClient.login", side_effect=PyLinkyWrongLoginException() - ): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "wrong_login"} - - -async def test_abort_on_fetch_failed(hass, login, close_session): +async def test_fetch_failed(hass: HomeAssistantType, login): """Test when we have errors during fetch.""" - flow = init_config_flow(hass) + login.return_value.fetch_data.side_effect = PyLinkyAccessException() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "access"} + hass.config_entries.flow.async_abort(result["flow_id"]) - with patch( - "pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyAccessException() - ): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "access"} + login.return_value.fetch_data.side_effect = PyLinkyEnedisException() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "enedis"} + hass.config_entries.flow.async_abort(result["flow_id"]) - with patch( - "pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyEnedisException() - ): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "enedis"} - - with patch("pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyException()): - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + login.return_value.fetch_data.side_effect = PyLinkyException() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + hass.config_entries.flow.async_abort(result["flow_id"]) diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index e4ca1c2106e..304487ca502 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -50,7 +50,9 @@ class TestLiteJetLight(unittest.TestCase): self.mock_lj.on_load_deactivated.side_effect = on_load_deactivated assert setup.setup_component( - self.hass, litejet.DOMAIN, {"litejet": {"port": "/tmp/this_will_be_mocked"}} + self.hass, + litejet.DOMAIN, + {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}, ) self.hass.block_till_done() diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index 0f42ac40cdf..48f5559799e 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -37,7 +37,9 @@ class TestLiteJetScene(unittest.TestCase): self.mock_lj.get_scene_name.side_effect = get_scene_name assert setup.setup_component( - self.hass, litejet.DOMAIN, {"litejet": {"port": "/tmp/this_will_be_mocked"}} + self.hass, + litejet.DOMAIN, + {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}}, ) self.hass.block_till_done() diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index a9cf54dc1f6..1244d8d9a25 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -48,7 +48,7 @@ class TestLiteJetSwitch(unittest.TestCase): self.mock_lj.on_switch_pressed.side_effect = on_switch_pressed self.mock_lj.on_switch_released.side_effect = on_switch_released - config = {"litejet": {"port": "/tmp/this_will_be_mocked"}} + config = {"litejet": {"port": "/dev/serial/by-id/mock-litejet"}} if method == self.test_include_switches_False: config["litejet"]["include_switches"] = False elif method != self.test_include_switches_unspecified: diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 638a7edf5d7..c2db984f16f 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 781ed03307b..006df742c6d 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -31,7 +31,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 1b48f301529..70e769a54f2 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -48,7 +48,6 @@ class TestComponentLogbook(unittest.TestCase): self.hass = get_test_home_assistant() init_recorder_component(self.hass) # Force an in memory DB assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG) - self.hass.start() def tearDown(self): """Stop everything that was started.""" @@ -90,7 +89,7 @@ class TestComponentLogbook(unittest.TestCase): dt_util.utcnow() + timedelta(hours=1), ) ) - assert len(events) == 2 + assert len(events) == 1 assert 1 == len(calls) last_call = calls[-1] diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 91e97685588..9567391e273 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -269,9 +269,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = "alarm_control_panel.test" - self.hass.start() - self.hass.block_till_done() - assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state common.alarm_arm_home(self.hass, "abc") @@ -1471,9 +1468,6 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase): entity_id = "alarm_control_panel.test" - self.hass.start() - self.hass.block_till_done() - assert STATE_ALARM_DISARMED == self.hass.states.get(entity_id).state common.alarm_arm_home(self.hass, "def") diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 65d4ab7e39c..810998ec0b8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -1,9 +1,12 @@ """The tests for the MaryTTS speech platform.""" -import asyncio import os import shutil +from urllib.parse import urlencode + +from mock import Mock, patch from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) @@ -11,7 +14,6 @@ import homeassistant.components.tts as tts from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSMaryTTSPlatform: @@ -21,14 +23,15 @@ class TestTTSMaryTTSPlatform: """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.url = "http://localhost:59125/process?" - self.url_param = { + self.host = "localhost" + self.port = 59125 + self.params = { "INPUT_TEXT": "HomeAssistant", "INPUT_TYPE": "TEXT", - "AUDIO": "WAVE", - "VOICE": "cmu-slt-hsmm", "OUTPUT_TYPE": "AUDIO", "LOCALE": "en_US", + "AUDIO": "WAVE_FILE", + "VOICE": "cmu-slt-hsmm", } def teardown_method(self): @@ -46,60 +49,83 @@ class TestTTSMaryTTSPlatform: with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - def test_service_say(self, aioclient_mock): + def test_service_say(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 200 + response.read.return_value = b"audio" config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} - ) + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 1 assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + conn.request.assert_called_with("POST", "/process", urlencode(self.params)) - def test_service_say_timeout(self, aioclient_mock): + def test_service_say_with_effect(self): + """Test service call say with effects.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 200 + response.read.return_value = b"audio" + + config = { + tts.DOMAIN: {"platform": "marytts", "effect": {"Volume": "amount:2.0;"}} + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) + self.hass.block_till_done() + + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + + self.params.update( + {"effect_Volume_selected": "on", "effect_Volume_parameters": "amount:2.0;"} + ) + conn.request.assert_called_with("POST", "/process", urlencode(self.params)) + + def test_service_say_http_error(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.get( - self.url, params=self.url_param, status=200, exc=asyncio.TimeoutError() - ) + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 500 + response.reason = "test" + response.readline.return_value = "content" config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} - ) - self.hass.block_till_done() - - assert len(calls) == 0 - assert len(aioclient_mock.mock_calls) == 1 - - def test_service_say_http_error(self, aioclient_mock): - """Test service call say.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - aioclient_mock.get(self.url, params=self.url_param, status=403, content=b"test") - - config = {tts.DOMAIN: {"platform": "marytts"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} - ) + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) self.hass.block_till_done() assert len(calls) == 0 + conn.request.assert_called_with("POST", "/process", urlencode(self.params)) diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 333cc4a2b13..c52daa80320 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -37,7 +37,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 73c6c819817..8a81d137672 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,32 +1,24 @@ """Tests for Met.no config flow.""" -from unittest.mock import Mock, patch +from asynctest import patch +import pytest -from homeassistant.components.met import config_flow +from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry -async def test_show_config_form(): +@pytest.fixture(name="met_setup", autouse=True) +def met_setup_fixture(): + """Patch met setup entry.""" + with patch("homeassistant.components.met.async_setup_entry", return_value=True): + yield + + +async def test_show_config_form(hass): """Test show configuration form.""" - hass = Mock() - flow = config_flow.MetFlowHandler() - flow.hass = hass - - result = await flow._show_config_form() - - assert result["type"] == "form" - assert result["step_id"] == "user" - - -async def test_show_config_form_default_values(): - """Test show configuration form.""" - hass = Mock() - flow = config_flow.MetFlowHandler() - flow.hass = hass - - result = await flow._show_config_form( - name="test", latitude="0", longitude="0", elevation="0" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} ) assert result["type"] == "form" @@ -34,117 +26,81 @@ async def test_show_config_form_default_values(): async def test_flow_with_home_location(hass): - """Test config flow . + """Test config flow. - Tests the flow when a default location is configured - then it should return a form with default values + Test the flow when a default location is configured. + Then it should return a form with default values. """ - flow = config_flow.MetFlowHandler() - flow.hass = hass - - hass.config.location_name = "Home" hass.config.latitude = 1 - hass.config.longitude = 1 - hass.config.elevation = 1 + hass.config.longitude = 2 + hass.config.elevation = 3 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) - result = await flow.async_step_user() assert result["type"] == "form" assert result["step_id"] == "user" - -async def test_flow_show_form(): - """Test show form scenarios first time. - - Test when the form should show when no configurations exists - """ - hass = Mock() - flow = config_flow.MetFlowHandler() - flow.hass = hass - - with patch.object( - flow, "_show_config_form", return_value=mock_coro() - ) as config_form: - await flow.async_step_user() - assert len(config_form.mock_calls) == 1 + default_data = result["data_schema"]({}) + assert default_data["name"] == HOME_LOCATION_NAME + assert default_data["latitude"] == 1 + assert default_data["longitude"] == 2 + assert default_data["elevation"] == 3 -async def test_flow_entry_created_from_user_input(): - """Test that create data from user input. - - Test when the form should show when no configurations exists - """ - hass = Mock() - flow = config_flow.MetFlowHandler() - flow.hass = hass - +async def test_create_entry(hass): + """Test create entry from user input.""" test_data = { "name": "home", - CONF_LONGITUDE: "0", - CONF_LATITUDE: "0", - CONF_ELEVATION: "0", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + CONF_ELEVATION: 0, } - # Test that entry created when user_input name not exists - with patch.object( - flow, "_show_config_form", return_value=mock_coro() - ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value=mock_coro() - ) as config_entries: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) - result = await flow.async_step_user(user_input=test_data) - - assert result["type"] == "create_entry" - assert result["data"] == test_data - assert len(config_entries.mock_calls) == 1 - assert not config_form.mock_calls + assert result["type"] == "create_entry" + assert result["title"] == "home" + assert result["data"] == test_data -async def test_flow_entry_config_entry_already_exists(): - """Test that create data from user input and config_entry already exists. +async def test_flow_entry_already_exists(hass): + """Test user input for config_entry that already exists. Test when the form should show when user puts existing location - in the config gui. Then the form should show with error + in the config gui. Then the form should show with error. """ - hass = Mock() - - flow = config_flow.MetFlowHandler() - flow.hass = hass - first_entry = MockConfigEntry(domain="met") first_entry.data["name"] = "home" - first_entry.data[CONF_LONGITUDE] = "0" - first_entry.data[CONF_LATITUDE] = "0" + first_entry.data[CONF_LONGITUDE] = 0 + first_entry.data[CONF_LATITUDE] = 0 + first_entry.data[CONF_ELEVATION] = 0 first_entry.add_to_hass(hass) test_data = { "name": "home", - CONF_LONGITUDE: "0", - CONF_LATITUDE: "0", - CONF_ELEVATION: "0", + CONF_LONGITUDE: 0, + CONF_LATITUDE: 0, + CONF_ELEVATION: 0, } - with patch.object( - flow, "_show_config_form", return_value=mock_coro() - ) as config_form, patch.object( - flow.hass.config_entries, "async_entries", return_value=[first_entry] - ) as config_entries: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=test_data + ) - await flow.async_step_user(user_input=test_data) - - assert len(config_form.mock_calls) == 1 - assert len(config_entries.mock_calls) == 1 - assert len(flow._errors) == 1 + assert result["type"] == "form" + assert result["errors"]["name"] == "name_exists" -async def test_onboarding_step(hass, mock_weather): +async def test_onboarding_step(hass): """Test initializing via onboarding step.""" - hass = Mock() - - flow = config_flow.MetFlowHandler() - flow.hass = hass - - result = await flow.async_step_onboarding({}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "onboarding"}, data={} + ) assert result["type"] == "create_entry" - assert result["title"] == "Home" + assert result["title"] == HOME_LOCATION_NAME assert result["data"] == {"track_home": True} diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py new file mode 100644 index 00000000000..ae8013eff4b --- /dev/null +++ b/tests/components/mikrotik/__init__.py @@ -0,0 +1,133 @@ +"""Tests for the Mikrotik component.""" +from homeassistant.components import mikrotik + +MOCK_DATA = { + mikrotik.CONF_NAME: "Mikrotik", + mikrotik.CONF_HOST: "0.0.0.0", + mikrotik.CONF_USERNAME: "user", + mikrotik.CONF_PASSWORD: "pass", + mikrotik.CONF_PORT: 8278, + mikrotik.CONF_VERIFY_SSL: False, +} + +MOCK_OPTIONS = { + mikrotik.CONF_ARP_PING: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_DETECTION_TIME: mikrotik.DEFAULT_DETECTION_TIME, +} + +DEVICE_1_DHCP = { + ".id": "*1A", + "address": "0.0.0.1", + "mac-address": "00:00:00:00:00:01", + "active-address": "0.0.0.1", + "host-name": "Device_1", + "comment": "Mobile", +} +DEVICE_2_DHCP = { + ".id": "*1B", + "address": "0.0.0.2", + "mac-address": "00:00:00:00:00:02", + "active-address": "0.0.0.2", + "host-name": "Device_2", + "comment": "PC", +} +DEVICE_1_WIRELESS = { + ".id": "*264", + "interface": "wlan1", + "mac-address": "00:00:00:00:00:01", + "ap": False, + "wds": False, + "bridge": False, + "rx-rate": "72.2Mbps-20MHz/1S/SGI", + "tx-rate": "72.2Mbps-20MHz/1S/SGI", + "packets": "59542,17464", + "bytes": "17536671,2966351", + "frames": "59542,17472", + "frame-bytes": "17655785,2862445", + "hw-frames": "78935,38395", + "hw-frame-bytes": "25636019,4063445", + "tx-frames-timed-out": 0, + "uptime": "5h49m36s", + "last-activity": "170ms", + "signal-strength": "-62@1Mbps", + "signal-to-noise": 52, + "signal-strength-ch0": -63, + "signal-strength-ch1": -69, + "strength-at-rates": "-62@1Mbps 16s330ms,-64@6Mbps 13s560ms,-65@HT20-3 52m6s30ms,-66@HT20-4 52m4s350ms,-66@HT20-5 51m58s580ms,-65@HT20-6 51m24s780ms,-65@HT20-7 5s680ms", + "tx-ccq": 93, + "p-throughput": 54928, + "last-ip": "0.0.0.1", + "802.1x-port-enabled": True, + "authentication-type": "wpa2-psk", + "encryption": "aes-ccm", + "group-encryption": "aes-ccm", + "management-protection": False, + "wmm-enabled": True, + "tx-rate-set": "OFDM:6-54 BW:1x SGI:1x HT:0-7", +} + +DEVICE_2_WIRELESS = { + ".id": "*265", + "interface": "wlan1", + "mac-address": "00:00:00:00:00:02", + "ap": False, + "wds": False, + "bridge": False, + "rx-rate": "72.2Mbps-20MHz/1S/SGI", + "tx-rate": "72.2Mbps-20MHz/1S/SGI", + "packets": "59542,17464", + "bytes": "17536671,2966351", + "frames": "59542,17472", + "frame-bytes": "17655785,2862445", + "hw-frames": "78935,38395", + "hw-frame-bytes": "25636019,4063445", + "tx-frames-timed-out": 0, + "uptime": "5h49m36s", + "last-activity": "170ms", + "signal-strength": "-62@1Mbps", + "signal-to-noise": 52, + "signal-strength-ch0": -63, + "signal-strength-ch1": -69, + "strength-at-rates": "-62@1Mbps 16s330ms,-64@6Mbps 13s560ms,-65@HT20-3 52m6s30ms,-66@HT20-4 52m4s350ms,-66@HT20-5 51m58s580ms,-65@HT20-6 51m24s780ms,-65@HT20-7 5s680ms", + "tx-ccq": 93, + "p-throughput": 54928, + "last-ip": "0.0.0.2", + "802.1x-port-enabled": True, + "authentication-type": "wpa2-psk", + "encryption": "aes-ccm", + "group-encryption": "aes-ccm", + "management-protection": False, + "wmm-enabled": True, + "tx-rate-set": "OFDM:6-54 BW:1x SGI:1x HT:0-7", +} +DHCP_DATA = [DEVICE_1_DHCP, DEVICE_2_DHCP] + +WIRELESS_DATA = [DEVICE_1_WIRELESS] + +ARP_DATA = [ + { + ".id": "*1", + "address": "0.0.0.1", + "mac-address": "00:00:00:00:00:01", + "interface": "bridge", + "published": False, + "invalid": False, + "DHCP": True, + "dynamic": True, + "complete": True, + "disabled": False, + }, + { + ".id": "*2", + "address": "0.0.0.2", + "mac-address": "00:00:00:00:00:02", + "interface": "bridge", + "published": False, + "invalid": False, + "DHCP": True, + "dynamic": True, + "complete": True, + "disabled": False, + }, +] diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py new file mode 100644 index 00000000000..25f541e9287 --- /dev/null +++ b/tests/components/mikrotik/test_config_flow.py @@ -0,0 +1,208 @@ +"""Test Mikrotik setup process.""" +from datetime import timedelta +from unittest.mock import patch + +import librouteros +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import mikrotik +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + +DEMO_USER_INPUT = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, +} + +DEMO_CONFIG = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: timedelta(seconds=30), +} + +DEMO_CONFIG_ENTRY = { + CONF_NAME: "Home router", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: 30, +} + + +@pytest.fixture(name="api") +def mock_mikrotik_api(): + """Mock an api.""" + with patch("librouteros.connect"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "librouteros.connect", + side_effect=librouteros.exceptions.TrapError("invalid user name or password"), + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed + ): + yield + + +async def test_import(hass, api): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "import"}, data=DEMO_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Home router" + assert result["data"][CONF_NAME] == "Home router" + assert result["data"][CONF_HOST] == "0.0.0.0" + assert result["data"][CONF_USERNAME] == "username" + assert result["data"][CONF_PASSWORD] == "password" + assert result["data"][CONF_PORT] == 8278 + assert result["data"][CONF_VERIFY_SSL] is False + + +async def test_flow_works(hass, api): + """Test config flow.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Home router" + assert result["data"][CONF_NAME] == "Home router" + assert result["data"][CONF_HOST] == "0.0.0.0" + assert result["data"][CONF_USERNAME] == "username" + assert result["data"][CONF_PASSWORD] == "password" + assert result["data"][CONF_PORT] == 8278 + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "device_tracker" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mikrotik.CONF_DETECTION_TIME: 30, + mikrotik.CONF_ARP_PING: True, + mikrotik.const.CONF_FORCE_DHCP: False, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + mikrotik.CONF_DETECTION_TIME: 30, + mikrotik.CONF_ARP_PING: True, + mikrotik.const.CONF_FORCE_DHCP: False, + } + + +async def test_host_already_configured(hass, auth_error): + """Test host already configured.""" + + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_name_exists(hass, api): + """Test name already configured.""" + + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry.add_to_hass(hass) + user_input = DEMO_USER_INPUT.copy() + user_input[CONF_HOST] = "0.0.0.1" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + + assert result["type"] == "form" + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_connection_error(hass, conn_error): + """Test error when connection is unsuccesful.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_wrong_credentials(hass, auth_error): + """Test error when credentials are wrong.""" + + result = await hass.config_entries.flow.async_init( + mikrotik.DOMAIN, context={"source": "user"} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=DEMO_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == { + CONF_USERNAME: "wrong_credentials", + CONF_PASSWORD: "wrong_credentials", + } diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py new file mode 100644 index 00000000000..643f94a5ad5 --- /dev/null +++ b/tests/components/mikrotik/test_device_tracker.py @@ -0,0 +1,118 @@ +"""The tests for the Mikrotik device tracker platform.""" +from datetime import timedelta + +from homeassistant.components import mikrotik +import homeassistant.components.device_tracker as device_tracker +from homeassistant.helpers import entity_registry +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import DEVICE_2_WIRELESS, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA +from .test_hub import setup_mikrotik_entry + +from tests.common import MockConfigEntry, patch + +DEFAULT_DETECTION_TIME = timedelta(seconds=300) + + +def mock_command(self, cmd, params=None): + """Mock the Mikrotik command method.""" + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return True + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return DHCP_DATA + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return WIRELESS_DATA + return {} + + +async def test_platform_manually_configured(hass): + """Test that nothing happens when configuring mikrotik through device tracker platform.""" + assert ( + await async_setup_component( + hass, + device_tracker.DOMAIN, + {device_tracker.DOMAIN: {"platform": "mikrotik"}}, + ) + is False + ) + assert mikrotik.DOMAIN not in hass.data + + +async def test_device_trackers(hass): + """Test device_trackers created by mikrotik.""" + + # test devices are added from wireless list only + hub = await setup_mikrotik_entry(hass) + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is None + + with patch.object(mikrotik.hub.MikrotikData, "command", new=mock_command): + # test device_2 is added after connecting to wireless network + WIRELESS_DATA.append(DEVICE_2_WIRELESS) + + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is not None + assert device_2.state == "home" + + # test state remains home if last_seen consider_home_interval + del WIRELESS_DATA[1] # device 2 is removed from wireless list + hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( + minutes=4 + ) + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state != "not_home" + + # test state changes to away if last_seen > consider_home_interval + hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( + minutes=5 + ) + await hub.async_update() + await hass.async_block_till_done() + + device_2 = hass.states.get("device_tracker.device_2") + assert device_2.state == "not_home" + + +async def test_restoring_devices(hass): + """Test restoring existing device_tracker entities if not detected on startup.""" + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + registry = await entity_registry.async_get_registry(hass) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:01", + suggested_object_id="device_1", + config_entry=config_entry, + ) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:02", + suggested_object_id="device_2", + config_entry=config_entry, + ) + + await setup_mikrotik_entry(hass) + + # test device_2 which is not in wireless list is restored + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is not None + assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 is not None + assert device_2.state == "not_home" diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py new file mode 100644 index 00000000000..fc37c9113ae --- /dev/null +++ b/tests/components/mikrotik/test_hub.py @@ -0,0 +1,179 @@ +"""Test Mikrotik hub.""" +from asynctest import patch +import librouteros + +from homeassistant import config_entries +from homeassistant.components import mikrotik + +from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA + +from tests.common import MockConfigEntry + + +async def setup_mikrotik_entry(hass, **kwargs): + """Set up Mikrotik intergation successfully.""" + support_wireless = kwargs.get("support_wireless", True) + dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) + wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) + + def mock_command(self, cmd, params=None): + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return support_wireless + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return dhcp_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return wireless_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: + return ARP_DATA + return {} + + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + if "force_dhcp" in kwargs: + config_entry.options["force_dhcp"] = True + + if "arp_ping" in kwargs: + config_entry.options["arp_ping"] = True + + with patch("librouteros.connect"), patch.object( + mikrotik.hub.MikrotikData, "command", new=mock_command + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return hass.data[mikrotik.DOMAIN][config_entry.entry_id] + + +async def test_hub_setup_successful(hass): + """Successful setup of Mikrotik hub.""" + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + hub = await setup_mikrotik_entry(hass) + + assert hub.config_entry.data == { + mikrotik.CONF_NAME: "Mikrotik", + mikrotik.CONF_HOST: "0.0.0.0", + mikrotik.CONF_USERNAME: "user", + mikrotik.CONF_PASSWORD: "pass", + mikrotik.CONF_PORT: 8278, + mikrotik.CONF_VERIFY_SSL: False, + } + assert hub.config_entry.options == { + mikrotik.hub.CONF_FORCE_DHCP: False, + mikrotik.CONF_ARP_PING: False, + mikrotik.CONF_DETECTION_TIME: 300, + } + + assert hub.api.available is True + assert hub.signal_update == "mikrotik-update-0.0.0.0" + assert forward_entry_setup.mock_calls[0][1] == (hub.config_entry, "device_tracker") + + +async def test_hub_setup_failed(hass): + """Failed setup of Mikrotik hub.""" + + config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) + config_entry.add_to_hass(hass) + # error when connection fails + with patch( + "librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed + ): + + await hass.config_entries.async_setup(config_entry.entry_id) + + assert config_entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + + # error when username or password is invalid + config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" + ) as forward_entry_setup, patch( + "librouteros.connect", + side_effect=librouteros.exceptions.TrapError("invalid user name or password"), + ): + + result = await hass.config_entries.async_setup(config_entry.entry_id) + + assert result is False + assert len(forward_entry_setup.mock_calls) == 0 + + +async def test_update_failed(hass): + """Test failing to connect during update.""" + + hub = await setup_mikrotik_entry(hass) + + with patch.object( + mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect + ): + await hub.async_update() + + assert hub.api.available is False + + +async def test_hub_not_support_wireless(hass): + """Test updating hub devices when hub doesn't support wireless interfaces.""" + + # test that the devices are constructed from dhcp data + + hub = await setup_mikrotik_entry(hass, support_wireless=False) + + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params is None + assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] + assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None + + +async def test_hub_support_wireless(hass): + """Test updating hub devices when hub support wireless interfaces.""" + + # test that the device list is from wireless data list + + hub = await setup_mikrotik_entry(hass) + + assert hub.api.support_wireless is True + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] + + # devices not in wireless list will not be added + assert "00:00:00:00:00:02" not in hub.api.devices + + +async def test_force_dhcp(hass): + """Test updating hub devices with forced dhcp method.""" + + # test that the devices are constructed from dhcp data + + hub = await setup_mikrotik_entry(hass, force_dhcp=True) + + assert hub.api.support_wireless is True + assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] + assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] + + # devices not in wireless list are added from dhcp + assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] + assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None + + +async def test_arp_ping(hass): + """Test arp ping devices to confirm they are connected.""" + + # test device show as home if arp ping returns value + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): + hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None + assert hub.api.devices["00:00:00:00:00:02"].last_seen is not None + + # test device show as away if arp ping times out + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): + hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None + # this device is not wireless so it will show as away + assert hub.api.devices["00:00:00:00:00:02"].last_seen is None diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py new file mode 100644 index 00000000000..bf2b19c735c --- /dev/null +++ b/tests/components/mikrotik/test_init.py @@ -0,0 +1,83 @@ +"""Test Mikrotik setup process.""" +from unittest.mock import Mock, patch + +from homeassistant.components import mikrotik +from homeassistant.setup import async_setup_component + +from . import MOCK_DATA + +from tests.common import MockConfigEntry, mock_coro + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a hub.""" + assert await async_setup_component(hass, mikrotik.DOMAIN, {}) is True + assert mikrotik.DOMAIN not in hass.data + + +async def test_successful_config_entry(hass): + """Test config entry successfull setup.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + mock_registry = Mock() + + with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(mock_registry), + ): + mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.serial_num = "12345678" + mock_hub.return_value.model = "RB750" + mock_hub.return_value.hostname = "mikrotik" + mock_hub.return_value.firmware = "3.65" + assert await mikrotik.async_setup_entry(hass, entry) is True + + assert len(mock_hub.mock_calls) == 2 + p_hass, p_entry = mock_hub.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + + assert len(mock_registry.mock_calls) == 1 + assert mock_registry.mock_calls[0][2] == { + "config_entry_id": entry.entry_id, + "connections": {("mikrotik", "12345678")}, + "manufacturer": mikrotik.ATTR_MANUFACTURER, + "model": "RB750", + "name": "mikrotik", + "sw_version": "3.65", + } + + +async def test_hub_fail_setup(hass): + """Test that a failed setup will not store the hub.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + + with patch.object(mikrotik, "MikrotikHub") as mock_hub: + mock_hub.return_value.async_setup.return_value = mock_coro(False) + assert await mikrotik.async_setup_entry(hass, entry) is False + + assert mikrotik.DOMAIN not in hass.data + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA,) + entry.add_to_hass(hass) + + with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(Mock()), + ): + mock_hub.return_value.async_setup.return_value = mock_coro(True) + mock_hub.return_value.serial_num = "12345678" + mock_hub.return_value.model = "RB750" + mock_hub.return_value.hostname = "mikrotik" + mock_hub.return_value.firmware = "3.65" + assert await mikrotik.async_setup_entry(hass, entry) is True + + assert len(mock_hub.return_value.mock_calls) == 1 + + assert await mikrotik.async_unload_entry(hass, entry) + assert entry.entry_id not in hass.data[mikrotik.DOMAIN] diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 3fed30d907b..4397f446a19 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -55,7 +55,7 @@ def minio_client_event_fixture(): async def test_minio_services(hass, caplog, minio_client): """Test Minio services.""" - hass.config.whitelist_external_dirs = set("/tmp") + hass.config.whitelist_external_dirs = set("/test") await async_setup_component( hass, @@ -80,22 +80,22 @@ async def test_minio_services(hass, caplog, minio_client): await hass.services.async_call( DOMAIN, "put", - {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + {"file_path": "/test/some_file", "key": "some_key", "bucket": "some_bucket"}, blocking=True, ) assert minio_client.fput_object.call_args == call( - "some_bucket", "some_key", "/tmp/some_file" + "some_bucket", "some_key", "/test/some_file" ) minio_client.reset_mock() await hass.services.async_call( DOMAIN, "get", - {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + {"file_path": "/test/some_file", "key": "some_key", "bucket": "some_bucket"}, blocking=True, ) assert minio_client.fget_object.call_args == call( - "some_bucket", "some_key", "/tmp/some_file" + "some_bucket", "some_key", "/test/some_file" ) minio_client.reset_mock() diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index cd819a9891c..e15c5732ac4 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -19,6 +19,8 @@ def registry(hass): @pytest.fixture async def create_registrations(hass, authed_api_client): """Return two new registrations.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + enc_reg = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER ) @@ -39,11 +41,13 @@ async def create_registrations(hass, authed_api_client): @pytest.fixture -async def webhook_client(hass, aiohttp_client): +async def webhook_client(hass, authed_api_client, aiohttp_client): """mobile_app mock client.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - return await aiohttp_client(hass.http.app) + # We pass in the authed_api_client server instance because + # it is used inside create_registrations and just passing in + # the app instance would cause the server to start twice, + # which caused deprecation warnings to be printed. + return await aiohttp_client(authed_api_client.server) @pytest.fixture diff --git a/tests/components/mobile_app/test_init.py b/tests/components/mobile_app/test_init.py new file mode 100644 index 00000000000..fe956796a96 --- /dev/null +++ b/tests/components/mobile_app/test_init.py @@ -0,0 +1,37 @@ +"""Tests for the mobile app integration.""" +from homeassistant.components.mobile_app.const import DATA_DELETED_IDS, DOMAIN + +from .const import CALL_SERVICE + +from tests.common import async_mock_service + + +async def test_unload_unloads(hass, create_registrations, webhook_client): + """Test we clean up when we unload.""" + # Second config entry is the one without encryption + config_entry = hass.config_entries.async_entries("mobile_app")[1] + webhook_id = config_entry.data["webhook_id"] + calls = async_mock_service(hass, "test", "mobile_app") + + # Test it works + await webhook_client.post(f"/api/webhook/{webhook_id}", json=CALL_SERVICE) + assert len(calls) == 1 + + await hass.config_entries.async_unload(config_entry.entry_id) + + # Test it no longer works + await webhook_client.post(f"/api/webhook/{webhook_id}", json=CALL_SERVICE) + assert len(calls) == 1 + + +async def test_remove_entry(hass, create_registrations): + """Test we clean up when we remove entry.""" + for config_entry in hass.config_entries.async_entries("mobile_app"): + await hass.config_entries.async_remove(config_entry.entry_id) + assert config_entry.data["webhook_id"] in hass.data[DOMAIN][DATA_DELETED_IDS] + + dev_reg = await hass.helpers.device_registry.async_get_registry() + assert len(dev_reg.devices) == 0 + + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 0 diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 6a41b5f054d..3df71c34781 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -67,9 +67,8 @@ async def test_webhook_handle_fire_event(hass, create_registrations, webhook_cli assert events[0].data["hello"] == "yo world" -async def test_webhook_update_registration(webhook_client, hass_client): +async def test_webhook_update_registration(webhook_client, authed_api_client): """Test that a we can update an existing registration via webhook.""" - authed_api_client = await hass_client() register_resp = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER_CLEARTEXT ) diff --git a/tests/components/mobile_app/test_websocket_api.py b/tests/components/mobile_app/test_websocket_api.py deleted file mode 100644 index bad956bf2db..00000000000 --- a/tests/components/mobile_app/test_websocket_api.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Test the mobile_app websocket API.""" -# pylint: disable=redefined-outer-name,unused-import -from homeassistant.components.mobile_app.const import CONF_SECRET, DOMAIN -from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.setup import async_setup_component - -from .const import CALL_SERVICE, REGISTER - - -async def test_webocket_get_user_registrations( - hass, aiohttp_client, hass_ws_client, hass_read_only_access_token -): - """Test get_user_registrations websocket command from admin perspective.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - user_api_client = await aiohttp_client( - hass.http.app, - headers={"Authorization": "Bearer {}".format(hass_read_only_access_token)}, - ) - - # First a read only user registers. - register_resp = await user_api_client.post( - "/api/mobile_app/registrations", json=REGISTER - ) - - assert register_resp.status == 201 - register_json = await register_resp.json() - assert CONF_WEBHOOK_ID in register_json - assert CONF_SECRET in register_json - - # Then the admin user attempts to access it. - client = await hass_ws_client(hass) - await client.send_json({"id": 5, "type": "mobile_app/get_user_registrations"}) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert len(msg["result"]) == 1 - - -async def test_webocket_delete_registration( - hass, hass_client, hass_ws_client, webhook_client -): - """Test delete_registration websocket command.""" - authed_api_client = await hass_client() # noqa: F811 - register_resp = await authed_api_client.post( - "/api/mobile_app/registrations", json=REGISTER - ) - - assert register_resp.status == 201 - register_json = await register_resp.json() - assert CONF_WEBHOOK_ID in register_json - assert CONF_SECRET in register_json - - webhook_id = register_json[CONF_WEBHOOK_ID] - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "mobile_app/delete_registration", CONF_WEBHOOK_ID: webhook_id} - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"] == "ok" - - ensure_four_ten_gone = await webhook_client.post( - "/api/webhook/{}".format(webhook_id), json=CALL_SERVICE - ) - - assert ensure_four_ten_gone.status == 410 diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 682aacdb746..dc79cb8a2e7 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,4 +1,5 @@ """The tests for the MQTT component.""" +from datetime import timedelta import ssl import unittest from unittest import mock @@ -16,10 +17,12 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from tests.common import ( MockConfigEntry, async_fire_mqtt_message, + async_fire_time_changed, async_mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, @@ -803,3 +806,25 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client): await client.send_json({"id": 8, "type": "unsubscribe_events", "subscription": 5}) response = await client.receive_json() assert response["success"] + + +async def test_dump_service(hass): + """Test that we can dump a topic.""" + await async_mock_mqtt_component(hass) + + mock_open = mock.mock_open() + + await hass.services.async_call( + "mqtt", "dump", {"topic": "bla/#", "duration": 3}, blocking=True + ) + async_fire_mqtt_message(hass, "bla/1", "test1") + async_fire_mqtt_message(hass, "bla/2", "test2") + + with mock.patch("homeassistant.components.mqtt.open", mock_open): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + writes = mock_open.return_value.write.mock_calls + assert len(writes) == 2 + assert writes[0][1][0] == "bla/1,test1\n" + assert writes[1][1][0] == "bla/2,test2\n" diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 3627c95040e..95f31b67826 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -1,5 +1,7 @@ """The tests for the MQTT component embedded server.""" -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock + +from asynctest import CoroutineMock, patch import homeassistant.components.mqtt as mqtt from homeassistant.const import CONF_PASSWORD @@ -21,7 +23,7 @@ class TestMQTT: @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -43,7 +45,7 @@ class TestMQTT: @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock(start=CoroutineMock()))) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): diff --git a/tests/components/netatmo/__init__.py b/tests/components/netatmo/__init__.py new file mode 100644 index 00000000000..26920894756 --- /dev/null +++ b/tests/components/netatmo/__init__.py @@ -0,0 +1 @@ +"""The tests for Netatmo platforms.""" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py new file mode 100644 index 00000000000..24aac6dc878 --- /dev/null +++ b/tests/components/netatmo/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the Netatmo config flow.""" +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.netatmo import config_flow +from homeassistant.components.netatmo.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + flow = config_flow.NetatmoFlowHandler() + flow.hass = hass + + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await hass.config_entries.flow.async_init( + "netatmo", + context={"source": "homekit"}, + data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "netatmo", + { + "netatmo": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + scope = "+".join( + [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", + ] + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={scope}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/nws/__init__.py b/tests/components/nws/__init__.py new file mode 100644 index 00000000000..bc3424f71c8 --- /dev/null +++ b/tests/components/nws/__init__.py @@ -0,0 +1 @@ +"""Tests for NWS.""" diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py new file mode 100644 index 00000000000..2a9bf060b73 --- /dev/null +++ b/tests/components/nws/const.py @@ -0,0 +1,114 @@ +"""Helpers for interacting with pynws.""" +from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) +from homeassistant.const import ( + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_PA, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.temperature import convert as convert_temperature + +DEFAULT_STATIONS = ["ABC", "XYZ"] + +DEFAULT_OBSERVATION = { + "temperature": 10, + "seaLevelPressure": 100000, + "relativeHumidity": 10, + "windSpeed": 10, + "windDirection": 180, + "visibility": 10000, + "textDescription": "A long description", + "station": "ABC", + "timestamp": "2019-08-12T23:53:00+00:00", + "iconTime": "day", + "iconWeather": (("Fair/clear", None),), +} + +EXPECTED_OBSERVATION_IMPERIAL = { + ATTR_WEATHER_TEMPERATURE: round( + convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ), + ATTR_WEATHER_WIND_BEARING: 180, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(10, LENGTH_METERS, LENGTH_MILES) * 3600 + ), + ATTR_WEATHER_PRESSURE: round( + convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 + ), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(10000, LENGTH_METERS, LENGTH_MILES) + ), + ATTR_WEATHER_HUMIDITY: 10, +} + +EXPECTED_OBSERVATION_METRIC = { + ATTR_WEATHER_TEMPERATURE: 10, + ATTR_WEATHER_WIND_BEARING: 180, + ATTR_WEATHER_WIND_SPEED: round( + convert_distance(10, LENGTH_METERS, LENGTH_KILOMETERS) * 3600 + ), + ATTR_WEATHER_PRESSURE: round(convert_pressure(100000, PRESSURE_PA, PRESSURE_HPA)), + ATTR_WEATHER_VISIBILITY: round( + convert_distance(10000, LENGTH_METERS, LENGTH_KILOMETERS) + ), + ATTR_WEATHER_HUMIDITY: 10, +} + +NONE_OBSERVATION = {key: None for key in DEFAULT_OBSERVATION} + +DEFAULT_FORECAST = [ + { + "number": 1, + "name": "Tonight", + "startTime": "2019-08-12T20:00:00-04:00", + "isDaytime": False, + "temperature": 10, + "windSpeedAvg": 10, + "windBearing": 180, + "detailedForecast": "A detailed forecast.", + "timestamp": "2019-08-12T23:53:00+00:00", + "iconTime": "night", + "iconWeather": (("lightning-rainy", 40), ("lightning-rainy", 90)), + }, +] + +EXPECTED_FORECAST_IMPERIAL = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: 10, + ATTR_FORECAST_WIND_SPEED: 10, + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + +EXPECTED_FORECAST_METRIC = { + ATTR_FORECAST_CONDITION: "lightning-rainy", + ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", + ATTR_FORECAST_TEMP: round(convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS)), + ATTR_FORECAST_WIND_SPEED: round( + convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) + ), + ATTR_FORECAST_WIND_BEARING: 180, + ATTR_FORECAST_PRECIP_PROB: 90, +} + +NONE_FORECAST = [{key: None for key in DEFAULT_FORECAST[0]}] diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index adda88a789d..f2b390a2235 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,87 +1,25 @@ """Tests for the NWS weather component.""" -from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB -from homeassistant.components.weather import ( - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, -) -from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_PA, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from unittest.mock import patch + +import aiohttp +import pytest + +from homeassistant.components.weather import ATTR_FORECAST from homeassistant.setup import async_setup_component -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import assert_setup_component, load_fixture - -EXP_OBS_IMP = { - ATTR_WEATHER_TEMPERATURE: round( - convert_temperature(26.7, TEMP_CELSIUS, TEMP_FAHRENHEIT) - ), - ATTR_WEATHER_WIND_BEARING: 190, - ATTR_WEATHER_WIND_SPEED: round( - convert_distance(2.6, LENGTH_METERS, LENGTH_MILES) * 3600 - ), - ATTR_WEATHER_PRESSURE: round( - convert_pressure(101040, PRESSURE_PA, PRESSURE_INHG), 2 - ), - ATTR_WEATHER_VISIBILITY: round( - convert_distance(16090, LENGTH_METERS, LENGTH_MILES) - ), - ATTR_WEATHER_HUMIDITY: 64, -} - -EXP_OBS_METR = { - ATTR_WEATHER_TEMPERATURE: round(26.7), - ATTR_WEATHER_WIND_BEARING: 190, - ATTR_WEATHER_WIND_SPEED: round( - convert_distance(2.6, LENGTH_METERS, LENGTH_KILOMETERS) * 3600 - ), - ATTR_WEATHER_PRESSURE: round(convert_pressure(101040, PRESSURE_PA, PRESSURE_HPA)), - ATTR_WEATHER_VISIBILITY: round( - convert_distance(16090, LENGTH_METERS, LENGTH_KILOMETERS) - ), - ATTR_WEATHER_HUMIDITY: 64, -} - -EXP_FORE_IMP = { - ATTR_FORECAST_CONDITION: "lightning-rainy", - ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", - ATTR_FORECAST_TEMP: 70, - ATTR_FORECAST_WIND_SPEED: 10, - ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIP_PROB: 90, -} - -EXP_FORE_METR = { - ATTR_FORECAST_CONDITION: "lightning-rainy", - ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", - ATTR_FORECAST_TEMP: round(convert_temperature(70, TEMP_FAHRENHEIT, TEMP_CELSIUS)), - ATTR_FORECAST_WIND_SPEED: round( - convert_distance(10, LENGTH_MILES, LENGTH_KILOMETERS) - ), - ATTR_FORECAST_WIND_BEARING: 180, - ATTR_FORECAST_PRECIP_PROB: 90, -} +from .const import ( + DEFAULT_FORECAST, + DEFAULT_OBSERVATION, + EXPECTED_FORECAST_IMPERIAL, + EXPECTED_FORECAST_METRIC, + EXPECTED_OBSERVATION_IMPERIAL, + EXPECTED_OBSERVATION_METRIC, + NONE_FORECAST, + NONE_OBSERVATION, +) +from tests.common import mock_coro MINIMAL_CONFIG = { "weather": { @@ -92,169 +30,168 @@ MINIMAL_CONFIG = { } } -INVALID_CONFIG = { - "weather": {"platform": "nws", "api_key": "x@example.com", "latitude": 40.0} +HOURLY_CONFIG = { + "weather": { + "platform": "nws", + "api_key": "x@example.com", + "latitude": 40.0, + "longitude": -85.0, + "mode": "hourly", + } } -STAURL = "https://api.weather.gov/points/{},{}/stations" -OBSURL = "https://api.weather.gov/stations/{}/observations/" -FORCURL = "https://api.weather.gov/points/{},{}/forecast" +@pytest.mark.parametrize( + "units,result_observation,result_forecast", + [ + (IMPERIAL_SYSTEM, EXPECTED_OBSERVATION_IMPERIAL, EXPECTED_FORECAST_IMPERIAL), + (METRIC_SYSTEM, EXPECTED_OBSERVATION_METRIC, EXPECTED_FORECAST_METRIC), + ], +) +async def test_imperial_metric(hass, units, result_observation, result_forecast): + """Test with imperial and metric units.""" + hass.config.units = units + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.return_value = mock_coro() + instance.update_forecast.return_value = mock_coro() + instance.observation = DEFAULT_OBSERVATION + instance.forecast = DEFAULT_FORECAST -async def test_imperial(hass, aioclient_mock): - """Test with imperial units.""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") - ) - aioclient_mock.get( - OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") - ) - - hass.config.units = IMPERIAL_SYSTEM - - with assert_setup_component(1, "weather"): await async_setup_component(hass, "weather", MINIMAL_CONFIG) - state = hass.states.get("weather.kmie") + state = hass.states.get("weather.abc") assert state assert state.state == "sunny" data = state.attributes - for key, value in EXP_OBS_IMP.items(): + for key, value in result_observation.items(): assert data.get(key) == value - assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) - for key, value in EXP_FORE_IMP.items(): + for key, value in result_forecast.items(): assert forecast[0].get(key) == value -async def test_metric(hass, aioclient_mock): - """Test with metric units.""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") - ) - aioclient_mock.get( - OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") - ) +async def test_hourly(hass): + """Test with hourly option.""" + hass.config.units = IMPERIAL_SYSTEM - hass.config.units = METRIC_SYSTEM + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.return_value = mock_coro() + instance.update_forecast_hourly.return_value = mock_coro() + instance.observation = DEFAULT_OBSERVATION + instance.forecast_hourly = DEFAULT_FORECAST - with assert_setup_component(1, "weather"): - await async_setup_component(hass, "weather", MINIMAL_CONFIG) + await async_setup_component(hass, "weather", HOURLY_CONFIG) - state = hass.states.get("weather.kmie") + state = hass.states.get("weather.abc") assert state assert state.state == "sunny" data = state.attributes - for key, value in EXP_OBS_METR.items(): + for key, value in EXPECTED_OBSERVATION_IMPERIAL.items(): assert data.get(key) == value - assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) - for key, value in EXP_FORE_METR.items(): + for key, value in EXPECTED_FORECAST_IMPERIAL.items(): assert forecast[0].get(key) == value -async def test_none(hass, aioclient_mock): - """Test with imperial units.""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") - ) - aioclient_mock.get( - OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-null.json") - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json") - ) - - hass.config.units = IMPERIAL_SYSTEM - - with assert_setup_component(1, "weather"): +async def test_none_values(hass): + """Test with none values in observation and forecast dicts.""" + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.return_value = mock_coro() + instance.update_forecast.return_value = mock_coro() + instance.observation = NONE_OBSERVATION + instance.forecast = NONE_FORECAST await async_setup_component(hass, "weather", MINIMAL_CONFIG) - state = hass.states.get("weather.kmie") + state = hass.states.get("weather.abc") assert state assert state.state == "unknown" data = state.attributes - for key in EXP_OBS_IMP: + for key in EXPECTED_OBSERVATION_IMPERIAL: assert data.get(key) is None - assert state.attributes.get("friendly_name") == "KMIE" + forecast = data.get(ATTR_FORECAST) - for key in EXP_FORE_IMP: + for key in EXPECTED_FORECAST_IMPERIAL: assert forecast[0].get(key) is None -async def test_fail_obs(hass, aioclient_mock): - """Test failing observation/forecast update.""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") - ) - aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - status=400, - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), - text=load_fixture("nws-weather-fore-valid.json"), - status=400, - ) - - hass.config.units = IMPERIAL_SYSTEM - - with assert_setup_component(1, "weather"): +async def test_none(hass): + """Test with None as observation and forecast.""" + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.return_value = mock_coro() + instance.update_forecast.return_value = mock_coro() + instance.observation = None + instance.forecast = None await async_setup_component(hass, "weather", MINIMAL_CONFIG) - state = hass.states.get("weather.kmie") + state = hass.states.get("weather.abc") assert state + assert state.state == "unknown" + + data = state.attributes + for key in EXPECTED_OBSERVATION_IMPERIAL: + assert data.get(key) is None + + forecast = data.get(ATTR_FORECAST) + assert forecast is None -async def test_fail_stn(hass, aioclient_mock): - """Test failing station update.""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), - text=load_fixture("nws-weather-sta-valid.json"), - status=400, - ) - aioclient_mock.get( - OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") - ) - - hass.config.units = IMPERIAL_SYSTEM - - with assert_setup_component(1, "weather"): +async def test_error_station(hass): + """Test error in setting station.""" + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.side_effect = aiohttp.ClientError + instance.update_observation.return_value = mock_coro() + instance.update_forecast.return_value = mock_coro() + instance.observation = None + instance.forecast = None await async_setup_component(hass, "weather", MINIMAL_CONFIG) - state = hass.states.get("weather.kmie") - assert state is None + state = hass.states.get("weather.abc") + assert state is None -async def test_invalid_config(hass, aioclient_mock): - """Test invalid config..""" - aioclient_mock.get( - STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") - ) - aioclient_mock.get( - OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") - ) - aioclient_mock.get( - FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") - ) +async def test_error_observation(hass, caplog): + """Test error during update observation.""" + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.side_effect = aiohttp.ClientError + instance.update_forecast.return_value = mock_coro() + instance.observation = None + instance.forecast = None + await async_setup_component(hass, "weather", MINIMAL_CONFIG) - hass.config.units = IMPERIAL_SYSTEM + assert "Error updating observation from station ABC" in caplog.text - with assert_setup_component(0, "weather"): - await async_setup_component(hass, "weather", INVALID_CONFIG) - state = hass.states.get("weather.kmie") - assert state is None +async def test_error_forecast(hass, caplog): + """Test error during update forecast.""" + with patch("homeassistant.components.nws.weather.SimpleNWS") as mock_nws: + instance = mock_nws.return_value + instance.station = "ABC" + instance.set_station.return_value = mock_coro() + instance.update_observation.return_value = mock_coro() + instance.update_forecast.side_effect = aiohttp.ClientError + instance.observation = None + instance.forecast = None + await async_setup_component(hass, "weather", MINIMAL_CONFIG) + assert "Error updating forecast from station ABC" in caplog.text diff --git a/tests/components/opnsense/__init__.py b/tests/components/opnsense/__init__.py new file mode 100644 index 00000000000..b3c8985caaf --- /dev/null +++ b/tests/components/opnsense/__init__.py @@ -0,0 +1 @@ +"""Tests for the opnsense component.""" diff --git a/tests/components/opnsense/test_device_tracker.py b/tests/components/opnsense/test_device_tracker.py new file mode 100644 index 00000000000..738847e1898 --- /dev/null +++ b/tests/components/opnsense/test_device_tracker.py @@ -0,0 +1,64 @@ +"""The tests for the opnsense device tracker platform.""" + +from unittest import mock + +import pytest + +from homeassistant.components import opnsense +from homeassistant.components.opnsense import CONF_API_SECRET, DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL +from homeassistant.setup import async_setup_component + + +@pytest.fixture(name="mocked_opnsense") +def mocked_opnsense(): + """Mock for pyopnense.diagnostics.""" + with mock.patch.object(opnsense, "diagnostics") as mocked_opn: + yield mocked_opn + + +async def test_get_scanner(hass, mocked_opnsense, mock_device_tracker_conf): + """Test creating an opnsense scanner.""" + interface_client = mock.MagicMock() + mocked_opnsense.InterfaceClient.return_value = interface_client + interface_client.get_arp.return_value = [ + { + "hostname": "", + "intf": "igb1", + "intf_description": "LAN", + "ip": "192.168.0.123", + "mac": "ff:ff:ff:ff:ff:ff", + "manufacturer": "", + }, + { + "hostname": "Desktop", + "intf": "igb1", + "intf_description": "LAN", + "ip": "192.168.0.167", + "mac": "ff:ff:ff:ff:ff:fe", + "manufacturer": "OEM", + }, + ] + network_insight_client = mock.MagicMock() + mocked_opnsense.NetworkInsightClient.return_value = network_insight_client + network_insight_client.get_interfaces.return_value = {"igb0": "WAN", "igb1": "LAN"} + + result = await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_URL: "https://fake_host_fun/api", + CONF_API_KEY: "fake_key", + CONF_API_SECRET: "fake_secret", + CONF_VERIFY_SSL: False, + } + }, + ) + await hass.async_block_till_done() + assert result + device_1 = hass.states.get("device_tracker.desktop") + assert device_1 is not None + assert device_1.state == "home" + device_2 = hass.states.get("device_tracker.ff_ff_ff_ff_ff_ff") + assert device_2.state == "home" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 699fb58a539..e5a414d95ad 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -753,7 +753,7 @@ async def test_reload(hass, hass_admin_user): {"name": "Person 3", "id": "id-3"}, ] }, - ), patch("homeassistant.config.find_config_file", return_value=""): + ): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 206d7477509..5c6189a811e 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -46,6 +46,13 @@ async def prometheus_client(loop, hass, hass_client): sensor4.entity_id = "sensor.wind_direction" await sensor4.async_update_ha_state() + sensor5 = DemoSensor( + None, "SPS30 PM <1µm Weight concentration", 3.7069, None, "µg/m³", None + ) + sensor5.hass = hass + sensor5.entity_id = "sensor.sps30_pm_1um_weight_concentration" + await sensor5.async_update_ha_state() + return await hass_client() @@ -113,3 +120,9 @@ async def test_view(prometheus_client): # pylint: disable=redefined-outer-name 'entity="sensor.wind_direction",' 'friendly_name="Wind Direction"} 25.0' in body ) + + assert ( + 'sensor_unit_u0xb5g_per_mu0xb3{domain="sensor",' + 'entity="sensor.sps30_pm_1um_weight_concentration",' + 'friendly_name="SPS30 PM <1µm Weight concentration"} 3.7069' in body + ) diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index a01d625d8c4..826a73bece2 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -14,6 +14,7 @@ class TestProximity(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config.components.add("zone") self.hass.states.set( "zone.home", "zoning", @@ -211,7 +212,7 @@ class TestProximity(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("proximity.home") assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "towards" + assert state.attributes.get("dir_of_travel") == "away_from" def test_device_tracker_test1_awaycloser(self): """Test for tracker state away closer.""" @@ -245,7 +246,7 @@ class TestProximity(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("proximity.home") assert state.attributes.get("nearest") == "test1" - assert state.attributes.get("dir_of_travel") == "away_from" + assert state.attributes.get("dir_of_travel") == "towards" def test_all_device_trackers_in_ignored_zone(self): """Test for tracker in ignored zone.""" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 81f81093a67..7c021199952 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -5,7 +5,12 @@ from pyps4_2ndscreen.errors import CredentialTimeout from homeassistant import data_entry_flow from homeassistant.components import ps4 -from homeassistant.components.ps4.const import DEFAULT_NAME, DEFAULT_REGION +from homeassistant.components.ps4.const import ( + DEFAULT_ALIAS, + DEFAULT_NAME, + DEFAULT_REGION, + DOMAIN, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -16,10 +21,12 @@ from homeassistant.const import ( ) from homeassistant.util import location -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro MOCK_TITLE = "PlayStation 4" -MOCK_CODE = "12345678" +MOCK_CODE = 12345678 +MOCK_CODE_LEAD_0 = 1234567 +MOCK_CODE_LEAD_0_STR = "01234567" MOCK_CREDS = "000aa000" MOCK_HOST = "192.0.0.0" MOCK_HOST_ADDITIONAL = "192.0.0.1" @@ -293,6 +300,42 @@ async def test_additional_device(hass): assert len(manager.async_entries()) == 2 +async def test_0_pin(hass): + """Test Pin with leading '0' is passed correctly.""" + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "creds"}, data={}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "mode" + + with patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + ), patch( + "homeassistant.components.ps4.config_flow.location.async_detect_location_info", + return_value=mock_coro(MOCK_LOCATION), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_AUTO + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + mock_config = MOCK_CONFIG + mock_config[CONF_CODE] = MOCK_CODE_LEAD_0 + with patch( + "pyps4_2ndscreen.Helper.link", return_value=(True, True) + ) as mock_call, patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], mock_config + ) + mock_call.assert_called_once_with( + MOCK_HOST, MOCK_CREDS, MOCK_CODE_LEAD_0_STR, DEFAULT_ALIAS + ) + + async def test_no_devices_found_abort(hass): """Test that failure to find devices aborts flow.""" flow = ps4.PlayStation4FlowHandler() diff --git a/tests/components/remote/test_reproduce_state.py b/tests/components/remote/test_reproduce_state.py new file mode 100644 index 00000000000..ee1574d1741 --- /dev/null +++ b/tests/components/remote/test_reproduce_state.py @@ -0,0 +1,52 @@ +"""Test reproduce state for Remote.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Remote states.""" + hass.states.async_set("remote.entity_off", "off", {}) + hass.states.async_set("remote.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "remote", "turn_on") + turn_off_calls = async_mock_service(hass, "remote", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("remote.entity_off", "off"), State("remote.entity_on", "on")], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("remote.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("remote.entity_on", "off"), + State("remote.entity_off", "on", {}), + # Should not raise + State("remote.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "remote" + assert turn_on_calls[0].data == { + "entity_id": "remote.entity_off", + } + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "remote" + assert turn_off_calls[0].data == {"entity_id": "remote.entity_on"} diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index b22730a3310..970c532f22e 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -4,7 +4,6 @@ Test setup of RFLink lights component/platform. State tracking and control of RFLink switch devices. """ - from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( @@ -267,15 +266,11 @@ async def test_signal_repetitions_alternation(hass, monkeypatch): # setup mocking rflink module _, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} - ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} ) - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test1"} - ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test1"} ) await hass.async_block_till_done() @@ -299,10 +294,8 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch): # setup mocking rflink module _, _, protocol, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch) - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} - ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DOMAIN + ".test"} ) hass.async_create_task( diff --git a/tests/components/safe_mode/__init__.py b/tests/components/safe_mode/__init__.py new file mode 100644 index 00000000000..3732fef17cb --- /dev/null +++ b/tests/components/safe_mode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Safe Mode integration.""" diff --git a/tests/components/safe_mode/test_init.py b/tests/components/safe_mode/test_init.py new file mode 100644 index 00000000000..a069ce90b17 --- /dev/null +++ b/tests/components/safe_mode/test_init.py @@ -0,0 +1,9 @@ +"""Tests for safe mode integration.""" +from homeassistant.setup import async_setup_component + + +async def test_works(hass): + """Test safe mode works.""" + assert await async_setup_component(hass, "safe_mode", {}) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids()) == 1 diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py new file mode 100644 index 00000000000..9c8ec3a9a09 --- /dev/null +++ b/tests/components/samsungtv/test_config_flow.py @@ -0,0 +1,388 @@ +"""Tests for Samsung TV config flow.""" +from unittest.mock import call, patch + +from asynctest import mock +import pytest +from samsungctl.exceptions import AccessDenied, UnhandledResponse + +from homeassistant.components.samsungtv.const import ( + CONF_MANUFACTURER, + CONF_MODEL, + DOMAIN, +) +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_MANUFACTURER, + ATTR_UPNP_MODEL_NAME, + ATTR_UPNP_UDN, +) +from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME + +MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} +MOCK_SSDP_DATA = { + ATTR_SSDP_LOCATION: "https://fake_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "[TV]fake_name", + ATTR_UPNP_MANUFACTURER: "fake_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake_model", + ATTR_UPNP_UDN: "uuid:fake_uuid", +} +MOCK_SSDP_DATA_NOPREFIX = { + ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "fake2_name", + ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MODEL_NAME: "fake2_model", + ATTR_UPNP_UDN: "fake2_uuid", +} + +AUTODETECT_WEBSOCKET = { + "name": "HomeAssistant", + "description": "HomeAssistant", + "id": "ha.component.samsung", + "method": "websocket", + "port": None, + "host": "fake_host", + "timeout": 31, +} +AUTODETECT_LEGACY = { + "name": "HomeAssistant", + "description": "HomeAssistant", + "id": "ha.component.samsung", + "method": "legacy", + "port": None, + "host": "fake_host", + "timeout": 31, +} + + +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch("samsungctl.Remote") as remote_class, patch( + "homeassistant.components.samsungtv.config_flow.socket" + ) as socket_class: + remote = mock.Mock() + remote.__enter__ = mock.Mock() + remote.__exit__ = mock.Mock() + remote_class.return_value = remote + socket = mock.Mock() + socket_class.return_value = socket + yield remote + + +async def test_user(hass, remote): + """Test starting a flow by user.""" + + # show form + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_name" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_MANUFACTURER] is None + assert result["data"][CONF_MODEL] is None + assert result["data"][CONF_ID] is None + + +async def test_user_missing_auth(hass): + """Test starting a flow by user with authentication.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=AccessDenied("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # missing authentication + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "auth_missing" + + +async def test_user_not_supported(hass): + """Test starting a flow by user for not supported device.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=UnhandledResponse("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # device not supported + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_user_not_successful(hass): + """Test starting a flow by user but no connection found.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=OSError("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # device not connectable + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_successful" + + +async def test_user_already_configured(hass, remote): + """Test starting a flow by user when already configured.""" + + # entry was added + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + + # failed as already configured + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_ssdp(hass, remote): + """Test starting a flow from discovery.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake_model" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Samsung fake_model" + assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["data"][CONF_ID] == "fake_uuid" + + +async def test_ssdp_noprefix(hass, remote): + """Test starting a flow from discovery without prefixes.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA_NOPREFIX + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "fake2_model" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "Samsung fake2_model" + assert result["data"][CONF_MANUFACTURER] == "fake2_manufacturer" + assert result["data"][CONF_MODEL] == "fake2_model" + assert result["data"][CONF_ID] == "fake2_uuid" + + +async def test_ssdp_missing_auth(hass): + """Test starting a flow from discovery with authentication.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=AccessDenied("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # missing authentication + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "abort" + assert result["reason"] == "auth_missing" + + +async def test_ssdp_not_supported(hass): + """Test starting a flow from discovery for not supported device.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=UnhandledResponse("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # device not supported + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_ssdp_not_successful(hass): + """Test starting a flow from discovery but no device found.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=OSError("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # device not found + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "abort" + assert result["reason"] == "not_successful" + + +async def test_ssdp_already_in_progress(hass, remote): + """Test starting a flow from discovery twice.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # failed as already in progress + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" + + +async def test_ssdp_already_configured(hass, remote): + """Test starting a flow from discovery when already configured.""" + + # entry was added + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_MANUFACTURER] is None + assert result["data"][CONF_MODEL] is None + assert result["data"][CONF_ID] is None + + # failed as already configured + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA + ) + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + # check updated device info + assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" + assert result["data"][CONF_MODEL] == "fake_model" + assert result["data"][CONF_ID] == "fake_uuid" + + +async def test_autodetect_websocket(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch("homeassistant.components.samsungtv.config_flow.Remote") as remote: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_METHOD] == "websocket" + assert remote.call_count == 1 + assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] + + +async def test_autodetect_auth_missing(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=[AccessDenied("Boom")], + ) as remote: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "auth_missing" + assert remote.call_count == 1 + assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] + + +async def test_autodetect_not_supported(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=[UnhandledResponse("Boom")], + ) as remote: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + assert remote.call_count == 1 + assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] + + +async def test_autodetect_legacy(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=[OSError("Boom"), mock.DEFAULT], + ) as remote: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_METHOD] == "legacy" + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] + + +async def test_autodetect_none(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=OSError("Boom"), + ) as remote: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_successful" + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py new file mode 100644 index 00000000000..cd31434e6b0 --- /dev/null +++ b/tests/components/samsungtv/test_init.py @@ -0,0 +1,99 @@ +"""Tests for the Samsung TV Integration.""" +from unittest.mock import call, patch + +import pytest + +from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON +from homeassistant.components.samsungtv.const import ( + CONF_ON_ACTION, + DOMAIN as SAMSUNGTV_DOMAIN, +) +from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_HOST, + CONF_NAME, + CONF_PORT, + SERVICE_VOLUME_UP, +) +from homeassistant.setup import async_setup_component + +ENTITY_ID = f"{DOMAIN}.fake_name" +MOCK_CONFIG = { + SAMSUNGTV_DOMAIN: [ + { + CONF_HOST: "fake_host", + CONF_NAME: "fake_name", + CONF_PORT: 1234, + CONF_ON_ACTION: [{"delay": "00:00:01"}], + } + ] +} +REMOTE_CALL = { + "name": "HomeAssistant", + "description": "HomeAssistant", + "id": "ha.component.samsung", + "method": "websocket", + "port": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_PORT], + "host": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_HOST], + "timeout": 1, +} + + +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch("homeassistant.components.samsungtv.socket") as socket1, patch( + "homeassistant.components.samsungtv.config_flow.socket" + ) as socket2, patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote: + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" + yield remote + + +async def test_setup(hass, remote): + """Test Samsung TV integration is setup.""" + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID) + + # test name and turn_on + assert state + assert state.name == "fake_name" + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + ) + + # test host and port + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert remote.mock_calls[0] == call(REMOTE_CALL) + + +async def test_setup_duplicate_config(hass, remote, caplog): + """Test duplicate setup of platform.""" + DUPLICATE = { + SAMSUNGTV_DOMAIN: [ + MOCK_CONFIG[SAMSUNGTV_DOMAIN][0], + MOCK_CONFIG[SAMSUNGTV_DOMAIN][0], + ] + } + await async_setup_component(hass, SAMSUNGTV_DOMAIN, DUPLICATE) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID) is None + assert len(hass.states.async_all()) == 0 + assert "duplicate host entries found" in caplog.text + + +async def test_setup_duplicate_entries(hass, remote, caplog): + """Test duplicate setup of platform.""" + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID) + assert len(hass.states.async_all()) == 1 + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index bb40dc28445..ba245ce7d6f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,9 +2,9 @@ import asyncio from datetime import timedelta import logging -from unittest.mock import call, patch from asynctest import mock +from asynctest.mock import call, patch import pytest from samsungctl import exceptions from websocket import WebSocketException @@ -22,21 +22,18 @@ from homeassistant.components.media_player.const import ( SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, ) -from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN -from homeassistant.components.samsungtv.media_player import ( - CONF_TIMEOUT, - SUPPORT_SAMSUNGTV, +from homeassistant.components.samsungtv.const import ( + CONF_ON_ACTION, + DOMAIN as SAMSUNGTV_DOMAIN, ) +from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, - CONF_BROADCAST_ADDRESS, CONF_HOST, - CONF_MAC, CONF_NAME, - CONF_PLATFORM, CONF_PORT, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -49,9 +46,7 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, STATE_OFF, STATE_ON, - STATE_UNKNOWN, ) -from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -59,107 +54,48 @@ from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { - DOMAIN: { - CONF_PLATFORM: SAMSUNGTV_DOMAIN, - CONF_HOST: "fake", - CONF_NAME: "fake", - CONF_PORT: 8001, - CONF_TIMEOUT: 10, - CONF_MAC: "38:f9:d3:82:b4:f1", - } + SAMSUNGTV_DOMAIN: [ + { + CONF_HOST: "fake", + CONF_NAME: "fake", + CONF_PORT: 8001, + CONF_ON_ACTION: [{"delay": "00:00:01"}], + } + ] } -ENTITY_ID_BROADCAST = f"{DOMAIN}.fake_broadcast" -MOCK_CONFIG_BROADCAST = { - DOMAIN: { - CONF_PLATFORM: SAMSUNGTV_DOMAIN, - CONF_HOST: "fake_broadcast", - CONF_NAME: "fake_broadcast", - CONF_PORT: 8001, - CONF_TIMEOUT: 10, - CONF_MAC: "38:f9:d3:82:b4:f1", - CONF_BROADCAST_ADDRESS: "192.168.5.255", - } -} - -ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac" -MOCK_CONFIG_NOMAC = { - DOMAIN: { - CONF_PLATFORM: SAMSUNGTV_DOMAIN, - CONF_HOST: "fake_nomac", - CONF_NAME: "fake_nomac", - CONF_PORT: 55000, - CONF_TIMEOUT: 10, - } -} - -ENTITY_ID_AUTO = f"{DOMAIN}.fake_auto" -MOCK_CONFIG_AUTO = { - DOMAIN: { - CONF_PLATFORM: SAMSUNGTV_DOMAIN, - CONF_HOST: "fake_auto", - CONF_NAME: "fake_auto", - } -} - -ENTITY_ID_DISCOVERY = f"{DOMAIN}.fake_discovery_fake_model" -MOCK_CONFIG_DISCOVERY = { - "name": "fake_discovery", - "model_name": "fake_model", - "host": "fake_host", - "udn": "fake_uuid", -} - -ENTITY_ID_DISCOVERY_PREFIX = f"{DOMAIN}.fake_discovery_prefix_fake_model_prefix" -MOCK_CONFIG_DISCOVERY_PREFIX = { - "name": "[TV]fake_discovery_prefix", - "model_name": "fake_model_prefix", - "host": "fake_host_prefix", - "udn": "uuid:fake_uuid_prefix", -} - -AUTODETECT_WEBSOCKET = { - "name": "HomeAssistant", - "description": "fake_auto", - "id": "ha.component.samsung", - "method": "websocket", - "port": None, - "host": "fake_auto", - "timeout": 1, -} -AUTODETECT_LEGACY = { - "name": "HomeAssistant", - "description": "fake_auto", - "id": "ha.component.samsung", - "method": "legacy", - "port": None, - "host": "fake_auto", - "timeout": 1, +ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" +MOCK_CONFIG_NOTURNON = { + SAMSUNGTV_DOMAIN: [ + {CONF_HOST: "fake_noturnon", CONF_NAME: "fake_noturnon", CONF_PORT: 55000} + ] } @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch( + with patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + "homeassistant.components.samsungtv.config_flow.socket" + ) as socket1, patch( "homeassistant.components.samsungtv.media_player.SamsungRemote" ) as remote_class, patch( - "homeassistant.components.samsungtv.media_player.socket" - ) as socket_class: + "homeassistant.components.samsungtv.socket" + ) as socket2: remote = mock.Mock() remote_class.return_value = remote - socket = mock.Mock() - socket_class.return_value = socket + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote -@pytest.fixture(name="wakeonlan") -def wakeonlan_fixture(): - """Patch the wakeonlan Remote.""" +@pytest.fixture(name="delay") +def delay_fixture(): + """Patch the delay script function.""" with patch( - "homeassistant.components.samsungtv.media_player.wakeonlan" - ) as wakeonlan_module: - yield wakeonlan_module + "homeassistant.components.samsungtv.media_player.Script.async_run" + ) as delay: + yield delay @pytest.fixture @@ -170,61 +106,20 @@ def mock_now(): async def setup_samsungtv(hass, config): """Set up mock Samsung TV.""" - await async_setup_component(hass, "media_player", config) + await async_setup_component(hass, SAMSUNGTV_DOMAIN, config) await hass.async_block_till_done() -async def test_setup_with_mac(hass, remote): +async def test_setup_with_turnon(hass, remote): """Test setup of platform.""" await setup_samsungtv(hass, MOCK_CONFIG) assert hass.states.get(ENTITY_ID) -async def test_setup_duplicate(hass, remote, caplog): - """Test duplicate setup of platform.""" - DUPLICATE = {DOMAIN: [MOCK_CONFIG[DOMAIN], MOCK_CONFIG[DOMAIN]]} - await setup_samsungtv(hass, DUPLICATE) - assert "Ignoring duplicate Samsung TV fake" in caplog.text - - -async def test_setup_without_mac(hass, remote): +async def test_setup_without_turnon(hass, remote): """Test setup of platform.""" - await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) - assert hass.states.get(ENTITY_ID_NOMAC) - - -async def test_setup_discovery(hass, remote): - """Test setup of platform with discovery.""" - hass.async_create_task( - async_load_platform( - hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY, {DOMAIN: {}} - ) - ) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID_DISCOVERY) - assert state - assert state.name == "fake_discovery (fake_model)" - entity_registry = await hass.helpers.entity_registry.async_get_registry() - entry = entity_registry.async_get(ENTITY_ID_DISCOVERY) - assert entry - assert entry.unique_id == "fake_uuid" - - -async def test_setup_discovery_prefix(hass, remote): - """Test setup of platform with discovery.""" - hass.async_create_task( - async_load_platform( - hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY_PREFIX, {DOMAIN: {}} - ) - ) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID_DISCOVERY_PREFIX) - assert state - assert state.name == "fake_discovery_prefix (fake_model_prefix)" - entity_registry = await hass.helpers.entity_registry.async_get_registry() - entry = entity_registry.async_get(ENTITY_ID_DISCOVERY_PREFIX) - assert entry - assert entry.unique_id == "fake_uuid_prefix" + await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) + assert hass.states.get(ENTITY_ID_NOTURNON) async def test_update_on(hass, remote, mock_now): @@ -243,18 +138,61 @@ async def test_update_on(hass, remote, mock_now): async def test_update_off(hass, remote, mock_now): """Testing update tv off.""" await setup_samsungtv(hass, MOCK_CONFIG) - remote.control = mock.Mock(side_effect=OSError("Boom")) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[OSError("Boom"), mock.DEFAULT], + ): - state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF -async def test_send_key(hass, remote, wakeonlan): +async def test_update_access_denied(hass, remote, mock_now): + """Testing update tv unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=exceptions.AccessDenied("Boom"), + ): + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + + +async def test_update_unhandled_response(hass, remote, mock_now): + """Testing update tv unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[exceptions.UnhandledResponse("Boom"), mock.DEFAULT], + ): + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key(hass, remote): """Test for send key.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -262,90 +200,13 @@ async def test_send_key(hass, remote, wakeonlan): ) state = hass.states.get(ENTITY_ID) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_VOLUP")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] assert state.state == STATE_ON -async def test_send_key_autodetect_websocket(hass, remote): - """Test for send key with autodetection of protocol.""" - with patch( - "homeassistant.components.samsungtv.media_player.SamsungRemote" - ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): - await setup_samsungtv(hass, MOCK_CONFIG_AUTO) - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True - ) - state = hass.states.get(ENTITY_ID_AUTO) - assert remote.call_count == 1 - assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] - assert state.state == STATE_ON - - -async def test_send_key_autodetect_websocket_exception(hass, caplog): - """Test for send key with autodetection of protocol.""" - caplog.set_level(logging.DEBUG) - with patch( - "homeassistant.components.samsungtv.media_player.SamsungRemote", - side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT], - ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): - await setup_samsungtv(hass, MOCK_CONFIG_AUTO) - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True - ) - state = hass.states.get(ENTITY_ID_AUTO) - # called 2 times because of the exception and the send key - assert remote.call_count == 2 - assert remote.call_args_list == [ - call(AUTODETECT_WEBSOCKET), - call(AUTODETECT_WEBSOCKET), - ] - assert state.state == STATE_ON - assert "Found working config without connection: " in caplog.text - assert "Failing config: " not in caplog.text - - -async def test_send_key_autodetect_legacy(hass, remote): - """Test for send key with autodetection of protocol.""" - with patch( - "homeassistant.components.samsungtv.media_player.SamsungRemote", - side_effect=[OSError("Boom"), mock.DEFAULT], - ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): - await setup_samsungtv(hass, MOCK_CONFIG_AUTO) - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True - ) - state = hass.states.get(ENTITY_ID_AUTO) - assert remote.call_count == 2 - assert remote.call_args_list == [ - call(AUTODETECT_WEBSOCKET), - call(AUTODETECT_LEGACY), - ] - assert state.state == STATE_ON - - -async def test_send_key_autodetect_none(hass, remote): - """Test for send key with autodetection of protocol.""" - with patch( - "homeassistant.components.samsungtv.media_player.SamsungRemote", - side_effect=OSError("Boom"), - ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): - await setup_samsungtv(hass, MOCK_CONFIG_AUTO) - assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True - ) - state = hass.states.get(ENTITY_ID_AUTO) - # 4 calls because of retry - assert remote.call_count == 4 - assert remote.call_args_list == [ - call(AUTODETECT_WEBSOCKET), - call(AUTODETECT_LEGACY), - call(AUTODETECT_WEBSOCKET), - call(AUTODETECT_LEGACY), - ] - assert state.state == STATE_UNKNOWN - - async def test_send_key_broken_pipe(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) @@ -368,12 +229,13 @@ async def test_send_key_connection_closed_retry_succeed(hass, remote): ) state = hass.states.get(ENTITY_ID) # key because of retry two times and update called - assert remote.control.call_count == 3 + assert remote.control.call_count == 2 assert remote.control.call_args_list == [ call("KEY_VOLUP"), call("KEY_VOLUP"), - call("KEY"), ] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] assert state.state == STATE_ON @@ -407,7 +269,7 @@ async def test_send_key_os_error(hass, remote): DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) state = hass.states.get(ENTITY_ID) - assert state.state == STATE_OFF + assert state.state == STATE_ON async def test_name(hass, remote): @@ -417,7 +279,7 @@ async def test_name(hass, remote): assert state.attributes[ATTR_FRIENDLY_NAME] == "fake" -async def test_state_with_mac(hass, remote, wakeonlan): +async def test_state_with_turnon(hass, remote, delay): """Test for state property.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -425,6 +287,8 @@ async def test_state_with_mac(hass, remote, wakeonlan): ) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON + assert delay.call_count == 1 + assert await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True ) @@ -432,22 +296,22 @@ async def test_state_with_mac(hass, remote, wakeonlan): assert state.state == STATE_OFF -async def test_state_without_mac(hass, remote): +async def test_state_without_turnon(hass, remote): """Test for state property.""" - await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert await hass.services.async_call( - DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) - state = hass.states.get(ENTITY_ID_NOMAC) + state = hass.states.get(ENTITY_ID_NOTURNON) assert state.state == STATE_ON assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) - state = hass.states.get(ENTITY_ID_NOMAC) + state = hass.states.get(ENTITY_ID_NOTURNON) assert state.state == STATE_OFF -async def test_supported_features_with_mac(hass, remote): +async def test_supported_features_with_turnon(hass, remote): """Test for supported_features property.""" await setup_samsungtv(hass, MOCK_CONFIG) state = hass.states.get(ENTITY_ID) @@ -456,10 +320,10 @@ async def test_supported_features_with_mac(hass, remote): ) -async def test_supported_features_without_mac(hass, remote): +async def test_supported_features_without_turnon(hass, remote): """Test for supported_features property.""" - await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) - state = hass.states.get(ENTITY_ID_NOMAC) + await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) + state = hass.states.get(ENTITY_ID_NOTURNON) assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV @@ -481,15 +345,25 @@ async def test_turn_off_websocket(hass, remote): assert remote.control.call_args_list == [call("KEY_POWER")] -async def test_turn_off_legacy(hass, remote): +async def test_turn_off_legacy(hass): """Test for turn_off.""" - await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True - ) - # key called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY_POWEROFF")] + with patch("homeassistant.components.samsungtv.config_flow.socket"), patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=[OSError("Boom"), mock.DEFAULT], + ), patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote_class, patch( + "homeassistant.components.samsungtv.socket" + ): + remote = mock.Mock() + remote_class.return_value = remote + await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True + ) + # key called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_POWEROFF")] async def test_turn_off_os_error(hass, remote, caplog): @@ -510,8 +384,10 @@ async def test_volume_up(hass, remote): DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_VOLUP")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_volume_down(hass, remote): @@ -521,8 +397,10 @@ async def test_volume_down(hass, remote): DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_VOLDOWN"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_VOLDOWN")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_mute_volume(hass, remote): @@ -535,8 +413,10 @@ async def test_mute_volume(hass, remote): True, ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_MUTE"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_MUTE")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_media_play(hass, remote): @@ -546,8 +426,10 @@ async def test_media_play(hass, remote): DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_PLAY")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_media_pause(hass, remote): @@ -557,8 +439,10 @@ async def test_media_pause(hass, remote): DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_PAUSE")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_media_next_track(hass, remote): @@ -568,8 +452,10 @@ async def test_media_next_track(hass, remote): DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_CHUP"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_CHUP")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_media_previous_track(hass, remote): @@ -579,41 +465,26 @@ async def test_media_previous_track(hass, remote): DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_CHDOWN")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] -async def test_turn_on_with_mac(hass, remote, wakeonlan): +async def test_turn_on_with_turnon(hass, remote, delay): """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True ) - # key and update called - assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [ - call("38:f9:d3:82:b4:f1", ip_address="255.255.255.255") - ] + assert delay.call_count == 1 -async def test_turn_on_with_mac_and_broadcast(hass, remote, wakeonlan): +async def test_turn_on_without_turnon(hass, remote): """Test turn on.""" - await setup_samsungtv(hass, MOCK_CONFIG_BROADCAST) + await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_BROADCAST}, True - ) - # key and update called - assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [ - call("38:f9:d3:82:b4:f1", ip_address="192.168.5.255") - ] - - -async def test_turn_on_without_mac(hass, remote): - """Test turn on.""" - await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) - assert await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) # nothing called as not supported feature assert remote.control.call_count == 0 @@ -641,71 +512,84 @@ async def test_play_media(hass, remote): True, ) # keys and update called - assert remote.control.call_count == 5 + assert remote.control.call_count == 4 assert remote.control.call_args_list == [ call("KEY_5"), call("KEY_7"), call("KEY_6"), call("KEY_ENTER"), - call("KEY"), ] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] assert len(sleeps) == 3 async def test_play_media_invalid_type(hass, remote): """Test for play_media with invalid media type.""" - url = "https://example.com" - await setup_samsungtv(hass, MOCK_CONFIG) - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, - ATTR_MEDIA_CONTENT_ID: url, - }, - True, - ) - # only update called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY")] + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.config_flow.socket"): + url = "https://example.com" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 0 + assert remote.close.call_count == 0 + assert remote.call_count == 1 async def test_play_media_channel_as_string(hass, remote): """Test for play_media with invalid channel as string.""" - url = "https://example.com" - await setup_samsungtv(hass, MOCK_CONFIG) - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, - ATTR_MEDIA_CONTENT_ID: url, - }, - True, - ) - # only update called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY")] + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.config_flow.socket"): + url = "https://example.com" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 0 + assert remote.close.call_count == 0 + assert remote.call_count == 1 async def test_play_media_channel_as_non_positive(hass, remote): """Test for play_media with invalid channel as non positive integer.""" - await setup_samsungtv(hass, MOCK_CONFIG) - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: ENTITY_ID, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, - ATTR_MEDIA_CONTENT_ID: "-4", - }, - True, - ) - # only update called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY")] + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.config_flow.socket"): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: "-4", + }, + True, + ) + # only update called + assert remote.control.call_count == 0 + assert remote.close.call_count == 0 + assert remote.call_count == 1 async def test_select_source(hass, remote): @@ -718,19 +602,25 @@ async def test_select_source(hass, remote): True, ) # key and update called - assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_HDMI"), call("KEY")] + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_HDMI")] + assert remote.close.call_count == 1 + assert remote.close.call_args_list == [call()] async def test_select_source_invalid_source(hass, remote): """Test for select_source with invalid source.""" - await setup_samsungtv(hass, MOCK_CONFIG) - assert await hass.services.async_call( - DOMAIN, - SERVICE_SELECT_SOURCE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, - True, - ) - # only update called - assert remote.control.call_count == 1 - assert remote.control.call_args_list == [call("KEY")] + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.config_flow.socket"): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, + True, + ) + # only update called + assert remote.control.call_count == 0 + assert remote.close.call_count == 0 + assert remote.call_count == 1 diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index f26189eec6c..8211ff10857 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -3,7 +3,7 @@ import io import unittest from homeassistant.components import light, scene -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_home_assistant @@ -128,3 +128,11 @@ class TestScene(unittest.TestCase): assert self.light_1.is_on assert self.light_2.is_on assert 100 == self.light_2.last_call("turn_on")[1].get("brightness") + + +async def test_services_registered(hass): + """Test we register services with empty config.""" + assert await async_setup_component(hass, "scene", {}) + assert hass.services.has_service("scene", "reload") + assert hass.services.has_service("scene", "turn_on") + assert hass.services.has_service("scene", "apply") diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index e008984f47c..9d64f5298f4 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -229,9 +229,8 @@ class TestScriptComponent(unittest.TestCase): "script": {"test2": {"sequence": [{"delay": {"seconds": 5}}]}} }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - reload(self.hass) - self.hass.block_till_done() + reload(self.hass) + self.hass.block_till_done() assert self.hass.states.get(ENTITY_ID) is None assert not self.hass.services.has_service(script.DOMAIN, "test") @@ -262,7 +261,6 @@ async def test_service_descriptions(hass): assert not descriptions[DOMAIN]["test"]["fields"] # Test 2: has "fields" but no "description" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) with patch( "homeassistant.config.load_yaml_config_file", return_value={ @@ -279,8 +277,7 @@ async def test_service_descriptions(hass): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) descriptions = await async_get_all_descriptions(hass) @@ -361,9 +358,8 @@ async def test_turning_no_scripts_off(hass): async def test_async_get_descriptions_script(hass): """Test async_set_service_schema for the script integration.""" - script = hass.components.script script_config = { - script.DOMAIN: { + DOMAIN: { "test1": {"sequence": [{"service": "homeassistant.restart"}]}, "test2": { "description": "test2", @@ -378,18 +374,75 @@ async def test_async_get_descriptions_script(hass): } } - await async_setup_component(hass, script.DOMAIN, script_config) + await async_setup_component(hass, DOMAIN, script_config) descriptions = await hass.helpers.service.async_get_all_descriptions() - assert descriptions[script.DOMAIN]["test1"]["description"] == "" - assert not descriptions[script.DOMAIN]["test1"]["fields"] + assert descriptions[DOMAIN]["test1"]["description"] == "" + assert not descriptions[DOMAIN]["test1"]["fields"] - assert descriptions[script.DOMAIN]["test2"]["description"] == "test2" + assert descriptions[DOMAIN]["test2"]["description"] == "test2" assert ( - descriptions[script.DOMAIN]["test2"]["fields"]["param"]["description"] + descriptions[DOMAIN]["test2"]["fields"]["param"]["description"] == "param_description" ) assert ( - descriptions[script.DOMAIN]["test2"]["fields"]["param"]["example"] - == "param_example" + descriptions[DOMAIN]["test2"]["fields"]["param"]["example"] == "param_example" ) + + +async def test_extraction_functions(hass): + """Test extraction functions.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test1": { + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": "light.in_both"}, + }, + { + "service": "test.script", + "data": {"entity_id": "light.in_first"}, + }, + {"domain": "light", "device_id": "device-in-both"}, + ] + }, + "test2": { + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": "light.in_both"}, + }, + { + "condition": "state", + "entity_id": "sensor.condition", + "state": "100", + }, + {"scene": "scene.hello"}, + {"domain": "light", "device_id": "device-in-both"}, + {"domain": "light", "device_id": "device-in-last"}, + ], + }, + } + }, + ) + + assert set(script.scripts_with_entity(hass, "light.in_both")) == { + "script.test1", + "script.test2", + } + assert set(script.entities_in_script(hass, "script.test1")) == { + "light.in_both", + "light.in_first", + } + assert set(script.scripts_with_device(hass, "device-in-both")) == { + "script.test1", + "script.test2", + } + assert set(script.devices_in_script(hass, "script.test2")) == { + "device-in-both", + "device-in-last", + } diff --git a/tests/components/search/__init__.py b/tests/components/search/__init__.py new file mode 100644 index 00000000000..5f8e27ceff2 --- /dev/null +++ b/tests/components/search/__init__.py @@ -0,0 +1 @@ +"""Tests for the Search integration.""" diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py new file mode 100644 index 00000000000..a379b91f82a --- /dev/null +++ b/tests/components/search/test_init.py @@ -0,0 +1,308 @@ +"""Tests for Search integration.""" +from homeassistant.components import search +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_search(hass): + """Test that search works.""" + area_reg = await hass.helpers.area_registry.async_get_registry() + device_reg = await hass.helpers.device_registry.async_get_registry() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + + living_room_area = area_reg.async_create("Living Room") + + # Light strip with 2 lights. + wled_config_entry = MockConfigEntry(domain="wled") + wled_config_entry.add_to_hass(hass) + + wled_device = device_reg.async_get_or_create( + config_entry_id=wled_config_entry.entry_id, + name="Light Strip", + identifiers=({"wled", "wled-1"}), + ) + + device_reg.async_update_device(wled_device.id, area_id=living_room_area.id) + + wled_segment_1_entity = entity_reg.async_get_or_create( + "light", + "wled", + "wled-1-seg-1", + suggested_object_id="wled segment 1", + config_entry=wled_config_entry, + device_id=wled_device.id, + ) + wled_segment_2_entity = entity_reg.async_get_or_create( + "light", + "wled", + "wled-1-seg-2", + suggested_object_id="wled segment 2", + config_entry=wled_config_entry, + device_id=wled_device.id, + ) + + # Non related info. + kitchen_area = area_reg.async_create("Kitchen") + + hue_config_entry = MockConfigEntry(domain="hue") + hue_config_entry.add_to_hass(hass) + + hue_device = device_reg.async_get_or_create( + config_entry_id=hue_config_entry.entry_id, + name="Light Strip", + identifiers=({"hue", "hue-1"}), + ) + + device_reg.async_update_device(hue_device.id, area_id=kitchen_area.id) + + hue_segment_1_entity = entity_reg.async_get_or_create( + "light", + "hue", + "hue-1-seg-1", + suggested_object_id="hue segment 1", + config_entry=hue_config_entry, + device_id=hue_device.id, + ) + hue_segment_2_entity = entity_reg.async_get_or_create( + "light", + "hue", + "hue-1-seg-2", + suggested_object_id="hue segment 2", + config_entry=hue_config_entry, + device_id=hue_device.id, + ) + + await async_setup_component( + hass, + "group", + { + "group": { + "wled": { + "name": "wled", + "entities": [ + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + ], + }, + "hue": { + "name": "hue", + "entities": [ + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + ], + }, + "wled_hue": { + "name": "wled and hue", + "entities": [ + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + ], + }, + } + }, + ) + + await async_setup_component( + hass, + "scene", + { + "scene": [ + { + "name": "scene_wled_seg_1", + "entities": {wled_segment_1_entity.entity_id: "on"}, + }, + { + "name": "scene_hue_seg_1", + "entities": {hue_segment_1_entity.entity_id: "on"}, + }, + { + "name": "scene_wled_hue", + "entities": { + wled_segment_1_entity.entity_id: "on", + wled_segment_2_entity.entity_id: "on", + hue_segment_1_entity.entity_id: "on", + hue_segment_2_entity.entity_id: "on", + }, + }, + ] + }, + ) + + await async_setup_component( + hass, + "script", + { + "script": { + "wled": { + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": wled_segment_1_entity.entity_id}, + }, + ] + }, + "hue": { + "sequence": [ + { + "service": "test.script", + "data": {"entity_id": hue_segment_1_entity.entity_id}, + }, + ] + }, + } + }, + ) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "alias": "wled_entity", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "service": "test.script", + "data": {"entity_id": wled_segment_1_entity.entity_id}, + }, + ], + }, + { + "alias": "wled_device", + "trigger": {"platform": "template", "value_template": "true"}, + "action": [ + { + "domain": "light", + "device_id": wled_device.id, + "entity_id": wled_segment_1_entity.entity_id, + "type": "turn_on", + }, + ], + }, + ] + }, + ) + + # Explore the graph from every node and make sure we find the same results + expected = { + "config_entry": {wled_config_entry.entry_id}, + "area": {living_room_area.id}, + "device": {wled_device.id}, + "entity": {wled_segment_1_entity.entity_id, wled_segment_2_entity.entity_id}, + "scene": {"scene.scene_wled_seg_1", "scene.scene_wled_hue"}, + "group": {"group.wled", "group.wled_hue"}, + "script": {"script.wled"}, + "automation": {"automation.wled_entity", "automation.wled_device"}, + } + + for search_type, search_id in ( + ("config_entry", wled_config_entry.entry_id), + ("area", living_room_area.id), + ("device", wled_device.id), + ("entity", wled_segment_1_entity.entity_id), + ("entity", wled_segment_2_entity.entity_id), + ("scene", "scene.scene_wled_seg_1"), + ("group", "group.wled"), + ("script", "script.wled"), + ("automation", "automation.wled_entity"), + ("automation", "automation.wled_device"), + ): + searcher = search.Searcher(hass, device_reg, entity_reg) + results = searcher.async_search(search_type, search_id) + # Add the item we searched for, it's omitted from results + results.setdefault(search_type, set()).add(search_id) + + assert ( + results == expected + ), f"Results for {search_type}/{search_id} do not match up" + + # For combined things, needs to return everything. + expected_combined = { + "config_entry": {wled_config_entry.entry_id, hue_config_entry.entry_id}, + "area": {living_room_area.id, kitchen_area.id}, + "device": {wled_device.id, hue_device.id}, + "entity": { + wled_segment_1_entity.entity_id, + wled_segment_2_entity.entity_id, + hue_segment_1_entity.entity_id, + hue_segment_2_entity.entity_id, + }, + "scene": { + "scene.scene_wled_seg_1", + "scene.scene_hue_seg_1", + "scene.scene_wled_hue", + }, + "group": {"group.wled", "group.hue", "group.wled_hue"}, + "script": {"script.wled", "script.hue"}, + "automation": {"automation.wled_entity", "automation.wled_device"}, + } + for search_type, search_id in ( + ("scene", "scene.scene_wled_hue"), + ("group", "group.wled_hue"), + ): + searcher = search.Searcher(hass, device_reg, entity_reg) + results = searcher.async_search(search_type, search_id) + # Add the item we searched for, it's omitted from results + results.setdefault(search_type, set()).add(search_id) + assert ( + results == expected_combined + ), f"Results for {search_type}/{search_id} do not match up" + + for search_type, search_id in ( + ("entity", "automation.non_existing"), + ("entity", "scene.non_existing"), + ("entity", "group.non_existing"), + ("entity", "script.non_existing"), + ("entity", "light.non_existing"), + ("area", "non_existing"), + ("config_entry", "non_existing"), + ("device", "non_existing"), + ("group", "group.non_existing"), + ("scene", "scene.non_existing"), + ("script", "script.non_existing"), + ("automation", "automation.non_existing"), + ): + searcher = search.Searcher(hass, device_reg, entity_reg) + assert searcher.async_search(search_type, search_id) == {} + + +async def test_ws_api(hass, hass_ws_client): + """Test WS API.""" + assert await async_setup_component(hass, "search", {}) + + area_reg = await hass.helpers.area_registry.async_get_registry() + device_reg = await hass.helpers.device_registry.async_get_registry() + + kitchen_area = area_reg.async_create("Kitchen") + + hue_config_entry = MockConfigEntry(domain="hue") + hue_config_entry.add_to_hass(hass) + + hue_device = device_reg.async_get_or_create( + config_entry_id=hue_config_entry.entry_id, + name="Light Strip", + identifiers=({"hue", "hue-1"}), + ) + + device_reg.async_update_device(hue_device.id, area_id=kitchen_area.id) + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "search/related", + "item_type": "device", + "item_id": hue_device.id, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "config_entry": [hue_config_entry.entry_id], + "area": [kitchen_area.id], + } diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index bd6a6ce4928..f9d8bb640c3 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -33,7 +33,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 7bb69388c1d..8e4b5d1792a 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -37,7 +37,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/sighthound/__init__.py b/tests/components/sighthound/__init__.py new file mode 100644 index 00000000000..96e0f549baf --- /dev/null +++ b/tests/components/sighthound/__init__.py @@ -0,0 +1 @@ +"""Tests for the Sighthound integration.""" diff --git a/tests/components/sighthound/test_image_processing.py b/tests/components/sighthound/test_image_processing.py new file mode 100644 index 00000000000..4548a3a6a35 --- /dev/null +++ b/tests/components/sighthound/test_image_processing.py @@ -0,0 +1,93 @@ +"""Tests for the Sighthound integration.""" +from unittest.mock import patch + +import pytest +import simplehound.core as hound + +import homeassistant.components.image_processing as ip +import homeassistant.components.sighthound.image_processing as sh +from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +VALID_CONFIG = { + ip.DOMAIN: { + "platform": "sighthound", + CONF_API_KEY: "abc123", + ip.CONF_SOURCE: {ip.CONF_ENTITY_ID: "camera.demo_camera"}, + }, + "camera": {"platform": "demo"}, +} + +VALID_ENTITY_ID = "image_processing.sighthound_demo_camera" + +MOCK_DETECTIONS = { + "image": {"width": 960, "height": 480, "orientation": 1}, + "objects": [ + { + "type": "person", + "boundingBox": {"x": 227, "y": 133, "height": 245, "width": 125}, + }, + { + "type": "person", + "boundingBox": {"x": 833, "y": 137, "height": 268, "width": 93}, + }, + ], + "requestId": "545cec700eac4d389743e2266264e84b", +} + + +@pytest.fixture +def mock_detections(): + """Return a mock detection.""" + with patch( + "simplehound.core.cloud.detect", return_value=MOCK_DETECTIONS + ) as detection: + yield detection + + +@pytest.fixture +def mock_image(): + """Return a mock camera image.""" + with patch( + "homeassistant.components.demo.camera.DemoCamera.camera_image", + return_value=b"Test", + ) as image: + yield image + + +async def test_bad_api_key(hass, caplog): + """Catch bad api key.""" + with patch("simplehound.core.cloud.detect", side_effect=hound.SimplehoundException): + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert "Sighthound error" in caplog.text + assert not hass.states.get(VALID_ENTITY_ID) + + +async def test_setup_platform(hass, mock_detections): + """Set up platform with one entity.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + +async def test_process_image(hass, mock_image, mock_detections): + """Process an image.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + person_events = [] + + @callback + def capture_person_event(event): + """Mock event.""" + person_events.append(event) + + hass.bus.async_listen(sh.EVENT_PERSON_DETECTED, capture_person_event) + + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, ip.SERVICE_SCAN, service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == "2" + assert len(person_events) == 2 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a7a21c577d6..2d40495215a 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,16 +1,10 @@ """Define tests for the SimpliSafe config flow.""" -from datetime import timedelta import json from unittest.mock import MagicMock, PropertyMock, mock_open, patch from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN, config_flow -from homeassistant.const import ( - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_TOKEN, - CONF_USERNAME, -) +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry, mock_coro @@ -85,7 +79,6 @@ async def test_step_import(hass): assert result["data"] == { CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345abc", - CONF_SCAN_INTERVAL: 30, } @@ -94,7 +87,6 @@ async def test_step_user(hass): conf = { CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", - CONF_SCAN_INTERVAL: timedelta(seconds=90), } flow = config_flow.SimpliSafeFlowHandler() @@ -116,5 +108,4 @@ async def test_step_user(hass): assert result["data"] == { CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345abc", - CONF_SCAN_INTERVAL: 90, } diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 58a519cae51..300e2ac4b46 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -32,11 +32,6 @@ async def test_mapping_integrity(): assert device_class in DEVICE_CLASSES, device_class -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await binary_sensor.async_setup_platform(None, None, None) - - async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the light types.""" device = device_factory( diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 79919a376cd..4229bd7cf94 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -198,11 +198,6 @@ def air_conditioner_fixture(device_factory): return device -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await climate.async_setup_platform(None, None, None) - - async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): """Tests the state attributes properly match the thermostat type.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[legacy_thermostat]) diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 26b68c0cb1f..9c5a80e27fb 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -18,7 +18,6 @@ from homeassistant.components.cover import ( STATE_OPEN, STATE_OPENING, ) -from homeassistant.components.smartthings import cover from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -26,11 +25,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await cover.async_setup_platform(None, None, None) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index af557ae83b1..6b8eb56d65c 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -16,7 +16,6 @@ from homeassistant.components.fan import ( SPEED_OFF, SUPPORT_SET_SPEED, ) -from homeassistant.components.smartthings import fan from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -24,11 +23,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await fan.async_setup_platform(None, None, None) - - async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the fan types.""" device = device_factory( diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 5f56138bb76..43a73113fec 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -18,7 +18,6 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, ) -from homeassistant.components.smartthings import light from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -68,11 +67,6 @@ def light_devices_fixture(device_factory): ] -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await light.async_setup_platform(None, None, None) - - async def test_entity_state(hass, light_devices): """Tests the state attributes properly match the light types.""" await setup_platform(hass, LIGHT_DOMAIN, devices=light_devices) diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index f76e42cdd46..65219852392 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -8,18 +8,12 @@ from pysmartthings import Attribute, Capability from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.components.smartthings import lock from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await lock.async_setup_platform(None, None, None) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 9d86520b5ab..a9e6443d2bf 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,17 +5,11 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN -from homeassistant.components.smartthings import scene as scene_platform from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from .conftest import setup_platform -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await scene_platform.async_setup_platform(None, None, None) - - async def test_entity_and_device_attributes(hass, scene): """Test the attributes of the entity are correct.""" # Arrange diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index f70c5bac57d..f285bc65d8d 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -31,11 +31,6 @@ async def test_mapping_integrity(): ), sensor_map.device_class -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await sensor.async_setup_platform(None, None, None) - - async def test_entity_state(hass, device_factory): """Tests the state attributes properly match the sensor types.""" device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 1c65550eb26..0b47739caf5 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -6,7 +6,6 @@ real HTTP calls are not initiated during testing. """ from pysmartthings import Attribute, Capability -from homeassistant.components.smartthings import switch from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE from homeassistant.components.switch import ( ATTR_CURRENT_POWER_W, @@ -18,11 +17,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .conftest import setup_platform -async def test_async_setup_platform(): - """Test setup platform does nothing (it uses config entries).""" - await switch.async_setup_platform(None, None, None) - - async def test_entity_and_device_attributes(hass, device_factory): """Test the attributes of the entity are correct.""" # Arrange diff --git a/tests/components/spotify/__init__.py b/tests/components/spotify/__init__.py new file mode 100644 index 00000000000..51e3404d3ad --- /dev/null +++ b/tests/components/spotify/__init__.py @@ -0,0 +1 @@ +"""Tests for the Spotify integration.""" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py new file mode 100644 index 00000000000..eabaa57d3a8 --- /dev/null +++ b/tests/components/spotify/test_config_flow.py @@ -0,0 +1,139 @@ +"""Tests for the Spotify config flow.""" +from unittest.mock import patch + +from spotipy import SpotifyException + +from homeassistant import data_entry_flow, setup +from homeassistant.components.spotify.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry + + +async def test_abort_if_no_configuration(hass): + """Check flow aborts when no configuration is present.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_zeroconf_abort_if_existing_entry(hass): + """Check zeroconf flow aborts when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check a full flow.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + "https://accounts.spotify.com/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=user-modify-playback-state,user-read-playback-state,user-read-private" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://accounts.spotify.com/api/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.spotify.config_flow.Spotify"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == DOMAIN + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_abort_if_spotify_error(hass, aiohttp_client, aioclient_mock): + """Check Spotify errors causes flow to abort.""" + await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + client = await aiohttp_client(hass.http.app) + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + aioclient_mock.post( + "https://accounts.spotify.com/api/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.spotify.config_flow.Spotify.current_user", + side_effect=SpotifyException(400, -1, "message"), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 6a38ea6c391..cec669da134 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -12,7 +12,11 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CE from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import ( + fire_time_changed, + get_test_home_assistant, + init_recorder_component, +) class TestStatisticsSensor(unittest.TestCase): @@ -211,6 +215,58 @@ class TestStatisticsSensor(unittest.TestCase): assert 6 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") + def test_max_age_without_sensor_change(self): + """Test value deprecation.""" + mock_data = {"return_time": datetime(2017, 8, 2, 12, 23, tzinfo=dt_util.UTC)} + + def mock_now(): + return mock_data["return_time"] + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "max_age": {"minutes": 3}, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + # insert the next value 30 seconds later + mock_data["return_time"] += timedelta(seconds=30) + + state = self.hass.states.get("sensor.test") + + assert 3.8 == state.attributes.get("min_value") + assert 15.2 == state.attributes.get("max_value") + + # wait for 3 minutes (max_age). + mock_data["return_time"] += timedelta(minutes=3) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + + assert state.attributes.get("min_value") == STATE_UNKNOWN + assert state.attributes.get("max_value") == STATE_UNKNOWN + assert state.attributes.get("count") == 0 + def test_change_rate(self): """Test min_age/max_age and change_rate.""" mock_data = { diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 06ad7323ead..fbd24fe2095 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -32,7 +32,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index d51a00ddf79..fe32fca9cb7 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -35,7 +35,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 19588ebfba0..73d12d0a729 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -35,7 +35,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py new file mode 100644 index 00000000000..36c639bc95b --- /dev/null +++ b/tests/components/template/test_alarm_control_panel.py @@ -0,0 +1,556 @@ +"""The tests for the Template alarm control panel platform.""" +import logging + +from homeassistant import setup +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, +) + +from tests.common import async_mock_service +from tests.components.alarm_control_panel import common + +_LOGGER = logging.getLogger(__name__) + + +async def test_template_state_text(hass): + """Test the state text of a template.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_ARMED_HOME + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_ARMED_AWAY + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_ARMED_NIGHT + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_DISARMED) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_DISARMED + + +async def test_optimistic_states(hass): + """Test the optimistic state.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == "unknown" + + await common.async_alarm_arm_away( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_AWAY + + await common.async_alarm_arm_home( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_HOME + + await common.async_alarm_arm_night( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_NIGHT + + await common.async_alarm_disarm( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_DISARMED + + +async def test_no_action_scripts(hass): + """Test no action scripts per state.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + + await common.async_alarm_arm_away( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_AWAY + + await common.async_alarm_arm_home( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_AWAY + + await common.async_alarm_arm_night( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_AWAY + + await common.async_alarm_disarm( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + state = hass.states.get("alarm_control_panel.test_template_panel") + await hass.async_block_till_done() + assert state.state == STATE_ALARM_ARMED_AWAY + + +async def test_template_syntax_error(hass, caplog): + """Test templating syntax error.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{% if blah %}", + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert ("invalid template") in caplog.text + + +async def test_invalid_name_does_not_create(hass, caplog): + """Test invalid name.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "bad name here": { + "value_template": "{{ disarmed }}", + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert ("invalid slug bad name") in caplog.text + + +async def test_invalid_panel_does_not_create(hass, caplog): + """Test invalid alarm control panel.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "wibble": {"test_panel": "Invalid"}, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert ("[wibble] is an invalid option") in caplog.text + + +async def test_no_panels_does_not_create(hass, caplog): + """Test if there are no panels -> no creation.""" + await setup.async_setup_component( + hass, "alarm_control_panel", {"alarm_control_panel": {"platform": "template"}}, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert ("required key not provided @ data['panels']") in caplog.text + + +async def test_name(hass): + """Test the accessibility of the name attribute.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "name": "Template Alarm Panel", + "value_template": "{{ disarmed }}", + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state is not None + + assert state.attributes.get("friendly_name") == "Template Alarm Panel" + + +async def test_arm_home_action(hass): + """Test arm home action.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + "arm_away": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": {"service": "test.automation"}, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + service_calls = async_mock_service(hass, "test", "automation") + + await common.async_alarm_arm_home( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + await hass.async_block_till_done() + + assert len(service_calls) == 1 + + +async def test_arm_away_action(hass): + """Test arm away action.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_away": {"service": "test.automation"}, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + service_calls = async_mock_service(hass, "test", "automation") + + await common.async_alarm_arm_away( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + await hass.async_block_till_done() + + assert len(service_calls) == 1 + + +async def test_arm_night_action(hass): + """Test arm night action.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": {"service": "test.automation"}, + "arm_away": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + service_calls = async_mock_service(hass, "test", "automation") + + await common.async_alarm_arm_night( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + await hass.async_block_till_done() + + assert len(service_calls) == 1 + + +async def test_disarm_action(hass): + """Test disarm action.""" + await setup.async_setup_component( + hass, + "alarm_control_panel", + { + "alarm_control_panel": { + "platform": "template", + "panels": { + "test_template_panel": { + "value_template": "{{ states('alarm_control_panel.test') }}", + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": {"service": "test.automation"}, + "arm_away": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + service_calls = async_mock_service(hass, "test", "automation") + + await common.async_alarm_disarm( + hass, entity_id="alarm_control_panel.test_template_panel" + ) + await hass.async_block_till_done() + + assert len(service_calls) == 1 diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index c3e1f2843fd..a0cccdcb18e 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -32,7 +32,7 @@ ENTITY_COVER = "cover.test_template_cover" @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 981b87ff43e..b6b0a87c9f2 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -38,7 +38,7 @@ _DIRECTION_INPUT_SELECT = "input_select.direction" @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 8da61ff3890..3e1ec207169 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,7 +4,7 @@ import logging import pytest from homeassistant import setup -from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.core import callback @@ -582,6 +582,98 @@ class TestTemplateLight: assert state is not None assert state.attributes.get("brightness") == expected_level + @pytest.mark.parametrize( + "expected_temp,template", + [(500, "{{500}}"), (None, "{{501}}"), (None, "{{x - 12}}")], + ) + def test_temperature_template(self, expected_temp, template): + """Test the template for the temperature.""" + with assert_setup_component(1, "light"): + assert setup.setup_component( + self.hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": template, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("color_temp") == expected_temp + + def test_temperature_action_no_template(self): + """Test setting temperature with optimistic template.""" + assert setup.setup_component( + self.hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{1 == 1}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "color_temp": "{{color_temp}}", + }, + }, + } + }, + } + }, + ) + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("light.test_template_light") + assert state.attributes.get("color_template") is None + + common.turn_on(self.hass, "light.test_template_light", **{ATTR_COLOR_TEMP: 345}) + self.hass.block_till_done() + assert len(self.calls) == 1 + assert self.calls[0].data["color_temp"] == "345" + + state = self.hass.states.get("light.test_template_light") + _LOGGER.info(str(state.attributes)) + assert state is not None + assert state.attributes.get("color_temp") == 345 + def test_friendly_name(self): """Test the accessibility of the friendly_name attribute.""" with assert_setup_component(1, "light"): diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 56675c9d893..dcf9c36474f 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -27,13 +27,17 @@ from homeassistant.components.timer import ( STATUS_PAUSED, ) from homeassistant.const import ( + ATTR_EDITABLE, ATTR_FRIENDLY_NAME, ATTR_ICON, + ATTR_ID, + ATTR_NAME, CONF_ENTITY_ID, SERVICE_RELOAD, ) from homeassistant.core import Context, CoreState from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -42,6 +46,38 @@ from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + ATTR_ID: "from_storage", + ATTR_NAME: "timer from storage", + ATTR_DURATION: 0, + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {DOMAIN: {}} + return await async_setup_component(hass, DOMAIN, config) + + return _storage + + async def test_config(hass): """Test config.""" invalid_configs = [None, 1, {}, {"name with space": None}] @@ -92,7 +128,9 @@ async def test_config_options(hass): assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) assert STATUS_IDLE == state_3.state - assert str(DEFAULT_DURATION) == state_3.attributes.get(CONF_DURATION) + assert str(cv.time_period(DEFAULT_DURATION)) == state_3.attributes.get( + CONF_DURATION + ) async def test_methods_and_events(hass): @@ -208,6 +246,7 @@ async def test_no_initial_state_and_no_restore_state(hass): async def test_config_reload(hass, hass_admin_user, hass_read_only_user): """Test reload service.""" count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) @@ -235,6 +274,9 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is not None assert state_2 is not None assert state_3 is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is None assert STATUS_IDLE == state_1.state assert ATTR_ICON not in state_1.attributes @@ -259,21 +301,20 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user): } }, ): - with patch("homeassistant.config.find_config_file", return_value=""): - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) + with pytest.raises(Unauthorized): await hass.services.async_call( DOMAIN, SERVICE_RELOAD, blocking=True, - context=Context(user_id=hass_admin_user.id), + context=Context(user_id=hass_read_only_user.id), ) - await hass.async_block_till_done() + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() assert count_start + 2 == len(hass.states.async_entity_ids()) @@ -284,6 +325,9 @@ async def test_config_reload(hass, hass_admin_user, hass_read_only_user): assert state_1 is None assert state_2 is not None assert state_3 is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_1") is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_2") is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "test_3") is not None assert STATUS_IDLE == state_2.state assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) @@ -360,3 +404,152 @@ async def test_timer_restarted_event(hass): assert results[-1].event_type == EVENT_TIMER_RESTARTED assert len(results) == 4 + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.timer_from_storage") + assert state.state == STATUS_IDLE + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "timer from storage" + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": None}}) + + state = hass.states.get(f"{DOMAIN}.{DOMAIN}_from_storage") + assert state.state == STATUS_IDLE + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "timer from storage" + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert not state.attributes.get(ATTR_EDITABLE) + assert state.state == STATUS_IDLE + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup(config={DOMAIN: {"from_yaml": None}}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "timer from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + timer_id = "from_storage" + timer_entity_id = f"{DOMAIN}.{DOMAIN}_{timer_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(timer_entity_id) + assert state is not None + from_reg = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) + assert from_reg == timer_entity_id + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{timer_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(timer_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) is None + + +async def test_update(hass, hass_ws_client, storage_setup): + """Test updating timer entity.""" + + assert await storage_setup() + + timer_id = "from_storage" + timer_entity_id = f"{DOMAIN}.{DOMAIN}_{timer_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(timer_entity_id) + assert state.attributes[ATTR_FRIENDLY_NAME] == "timer from storage" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{timer_id}", + CONF_DURATION: 33, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(timer_entity_id) + assert state.attributes[ATTR_DURATION] == str(cv.time_period(33)) + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + timer_id = "new_timer" + timer_entity_id = f"{DOMAIN}.{timer_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(timer_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + CONF_NAME: "New Timer", + CONF_DURATION: 42, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(timer_entity_id) + assert state.state == STATUS_IDLE + assert state.attributes[ATTR_DURATION] == str(cv.time_period(42)) + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id + + +async def test_setup_no_config(hass, hass_admin_user): + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index cbc8316f7c8..1c6bbff9588 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -137,7 +137,7 @@ def test_config_verify_ssl_but_no_ssl_enabled(hass, mock_session_send): CONF_HOST: "tomato-router", CONF_PORT: 1234, CONF_SSL: False, - CONF_VERIFY_SSL: "/tmp/tomato.crt", + CONF_VERIFY_SSL: "/test/tomato.crt", CONF_USERNAME: "foo", CONF_PASSWORD: "password", tomato.CONF_HTTP_ID: "1234567890", @@ -171,7 +171,7 @@ def test_config_valid_verify_ssl_path(hass, mock_session_send): CONF_HOST: "tomato-router", CONF_PORT: 1234, CONF_SSL: True, - CONF_VERIFY_SSL: "/tmp/tomato.crt", + CONF_VERIFY_SSL: "/test/tomato.crt", CONF_USERNAME: "bar", CONF_PASSWORD: "foo", tomato.CONF_HTTP_ID: "0987654321", @@ -189,7 +189,7 @@ def test_config_valid_verify_ssl_path(hass, mock_session_send): assert "exec=devlist" in result.req.body assert mock_session_send.call_count == 1 assert mock_session_send.mock_calls[0] == mock.call( - result.req, timeout=3, verify="/tmp/tomato.crt" + result.req, timeout=3, verify="/test/tomato.crt" ) diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 8d1d4d94738..8e5a2a775b9 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -1,9 +1,15 @@ """Tests for light platform.""" -from unittest.mock import patch +from typing import Callable, NamedTuple +from unittest.mock import Mock, patch -from pyHS100 import SmartBulb +from pyHS100 import SmartDeviceException +import pytest from homeassistant.components import tplink +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -20,9 +26,25 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +LightMockData = NamedTuple( + "LightMockData", + ( + ("sys_info", dict), + ("light_state", dict), + ("set_light_state", Callable[[dict], None]), + ("set_light_state_mock", Mock), + ("get_light_state_mock", Mock), + ("current_consumption_mock", Mock), + ("get_sysinfo_mock", Mock), + ("get_emeter_daily_mock", Mock), + ("get_emeter_monthly_mock", Mock), + ), +) -async def test_light(hass: HomeAssistant) -> None: - """Test function.""" + +@pytest.fixture(name="light_mock_data") +def light_mock_data_fixture() -> None: + """Create light mock data.""" sys_info = { "sw_ver": "1.2.3", "hw_ver": "2.3.4", @@ -44,22 +66,26 @@ async def test_light(hass: HomeAssistant) -> None: } light_state = { - "on_off": SmartBulb.BULB_STATE_ON, + "on_off": True, "dft_on_state": { "brightness": 12, "color_temp": 3200, - "hue": 100, - "saturation": 200, + "hue": 110, + "saturation": 90, }, "brightness": 13, "color_temp": 3300, "hue": 110, - "saturation": 210, + "saturation": 90, } - def set_light_state(state): + def set_light_state(state) -> None: nonlocal light_state + drt_on_state = light_state["dft_on_state"] + drt_on_state.update(state.get("dft_on_state", {})) + light_state.update(state) + light_state["dft_on_state"] = drt_on_state set_light_state_patch = patch( "homeassistant.components.tplink.common.SmartBulb.set_light_state", @@ -112,109 +138,209 @@ async def test_light(hass: HomeAssistant) -> None: }, ) - with set_light_state_patch, get_light_state_patch, current_consumption_patch, get_sysinfo_patch, get_emeter_daily_patch, get_emeter_monthly_patch: - await async_setup_component( - hass, - tplink.DOMAIN, - { - tplink.DOMAIN: { - CONF_DISCOVERY: False, - CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], - } - }, - ) - await hass.async_block_till_done() - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light1"}, - blocking=True, + with set_light_state_patch as set_light_state_mock, get_light_state_patch as get_light_state_mock, current_consumption_patch as current_consumption_mock, get_sysinfo_patch as get_sysinfo_mock, get_emeter_daily_patch as get_emeter_daily_mock, get_emeter_monthly_patch as get_emeter_monthly_mock: + yield LightMockData( + sys_info=sys_info, + light_state=light_state, + set_light_state=set_light_state, + set_light_state_mock=set_light_state_mock, + get_light_state_mock=get_light_state_mock, + current_consumption_mock=current_consumption_mock, + get_sysinfo_mock=get_sysinfo_mock, + get_emeter_daily_mock=get_emeter_daily_mock, + get_emeter_monthly_mock=get_emeter_monthly_mock, ) - assert hass.states.get("light.light1").state == "off" - assert light_state["on_off"] == 0 - await hass.async_block_till_done() +async def update_entity(hass: HomeAssistant, entity_id: str) -> None: + """Run an update action for an entity.""" + await hass.services.async_call( + HA_DOMAIN, SERVICE_UPDATE_ENTITY, {ATTR_ENTITY_ID: entity_id}, blocking=True, + ) + await hass.async_block_till_done() - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.light1", - ATTR_COLOR_TEMP: 312, - ATTR_BRIGHTNESS: 50, - }, - blocking=True, - ) - await hass.async_block_till_done() +async def test_light(hass: HomeAssistant, light_mock_data: LightMockData) -> None: + """Test function.""" + light_state = light_mock_data.light_state + set_light_state = light_mock_data.set_light_state - state = hass.states.get("light.light1") - assert state.state == "on" - assert state.attributes["brightness"] == 48.45 - assert state.attributes["hs_color"] == (110, 210) - assert state.attributes["color_temp"] == 312 - assert light_state["on_off"] == 1 + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.light1", - ATTR_BRIGHTNESS: 55, - ATTR_HS_COLOR: (23, 27), - }, - blocking=True, - ) + await async_setup_component( + hass, + tplink.DOMAIN, + { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], + } + }, + ) + await hass.async_block_till_done() - await hass.async_block_till_done() + assert hass.states.get("light.light1") - state = hass.states.get("light.light1") - assert state.state == "on" - assert state.attributes["brightness"] == 53.55 - assert state.attributes["hs_color"] == (23, 27) - assert state.attributes["color_temp"] == 312 - assert light_state["brightness"] == 21 - assert light_state["hue"] == 23 - assert light_state["saturation"] == 27 + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") - light_state["on_off"] = 0 - light_state["dft_on_state"]["on_off"] = 0 - light_state["brightness"] = 66 - light_state["dft_on_state"]["brightness"] = 66 - light_state["color_temp"] = 6400 - light_state["dft_on_state"]["color_temp"] = 123 - light_state["hue"] = 77 - light_state["dft_on_state"]["hue"] = 77 - light_state["saturation"] = 78 - light_state["dft_on_state"]["saturation"] = 78 + assert hass.states.get("light.light1").state == "off" + assert light_state["on_off"] == 0 - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light1"}, - blocking=True, - ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light1", ATTR_COLOR_TEMP: 222, ATTR_BRIGHTNESS: 50}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") - await hass.async_block_till_done() + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 48.45 + assert state.attributes["hs_color"] == (110, 90) + assert state.attributes["color_temp"] == 222 + assert light_state["on_off"] == 1 - state = hass.states.get("light.light1") - assert state.state == "off" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light1", ATTR_BRIGHTNESS: 55, ATTR_HS_COLOR: (23, 27)}, + blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.light1"}, - blocking=True, - ) + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 53.55 + assert state.attributes["hs_color"] == (23, 27) + assert light_state["brightness"] == 21 + assert light_state["hue"] == 23 + assert light_state["saturation"] == 27 - await hass.async_block_till_done() + light_state["on_off"] = 0 + light_state["dft_on_state"]["on_off"] = 0 + light_state["brightness"] = 66 + light_state["dft_on_state"]["brightness"] = 66 + light_state["color_temp"] = 6400 + light_state["dft_on_state"]["color_temp"] = 123 + light_state["hue"] = 77 + light_state["dft_on_state"]["hue"] = 77 + light_state["saturation"] = 78 + light_state["dft_on_state"]["saturation"] = 78 - state = hass.states.get("light.light1") - assert state.attributes["brightness"] == 168.3 - assert state.attributes["hs_color"] == (77, 78) - assert state.attributes["color_temp"] == 156 - assert light_state["brightness"] == 66 - assert light_state["hue"] == 77 - assert light_state["saturation"] == 78 + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") + + state = hass.states.get("light.light1") + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 168.3 + assert state.attributes["hs_color"] == (77, 78) + assert state.attributes["color_temp"] == 156 + assert light_state["brightness"] == 66 + assert light_state["hue"] == 77 + assert light_state["saturation"] == 78 + + set_light_state({"brightness": 91, "dft_on_state": {"brightness": 91}}) + await update_entity(hass, "light.light1") + + state = hass.states.get("light.light1") + assert state.attributes["brightness"] == 232.05 + + +async def test_get_light_state_retry( + hass: HomeAssistant, light_mock_data: LightMockData +) -> None: + """Test function.""" + # Setup test for retries for sysinfo. + get_sysinfo_call_count = 0 + + def get_sysinfo_side_effect(): + nonlocal get_sysinfo_call_count + get_sysinfo_call_count += 1 + + # Need to fail on the 2nd call because the first call is used to + # determine if the device is online during the light platform's + # setup hook. + if get_sysinfo_call_count == 2: + raise SmartDeviceException() + + return light_mock_data.sys_info + + light_mock_data.get_sysinfo_mock.side_effect = get_sysinfo_side_effect + + # Setup test for retries of getting state information. + get_state_call_count = 0 + + def get_light_state_side_effect(): + nonlocal get_state_call_count + get_state_call_count += 1 + + if get_state_call_count == 1: + raise SmartDeviceException() + + return light_mock_data.light_state + + light_mock_data.get_light_state_mock.side_effect = get_light_state_side_effect + + # Setup test for retries of setting state information. + set_state_call_count = 0 + + def set_light_state_side_effect(state_data: dict): + nonlocal set_state_call_count, light_mock_data + set_state_call_count += 1 + + if set_state_call_count == 1: + raise SmartDeviceException() + + light_mock_data.set_light_state(state_data) + + light_mock_data.set_light_state_mock.side_effect = set_light_state_side_effect + + # Setup component. + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + + await async_setup_component( + hass, + tplink.DOMAIN, + { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.light1"}, blocking=True, + ) + await hass.async_block_till_done() + await update_entity(hass, "light.light1") + + assert light_mock_data.get_sysinfo_mock.call_count > 1 + assert light_mock_data.get_light_state_mock.call_count > 1 + assert light_mock_data.set_light_state_mock.call_count > 1 + + assert light_mock_data.get_sysinfo_mock.call_count < 40 + assert light_mock_data.get_light_state_mock.call_count < 40 + assert light_mock_data.set_light_state_mock.call_count < 10 diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index f8dc11069d8..6aafe29901d 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -24,6 +24,7 @@ from tests.common import ( get_test_home_assistant, get_test_instance_port, mock_service, + mock_storage, ) @@ -45,6 +46,8 @@ class TestTTS: self.hass = get_test_home_assistant() self.demo_provider = DemoProvider("en") self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + self.mock_storage = mock_storage() + self.mock_storage.__enter__() setup_component( self.hass, @@ -55,6 +58,7 @@ class TestTTS: def teardown_method(self): """Stop everything that was started.""" self.hass.stop() + self.mock_storage.__exit__(None, None, None) if os.path.isdir(self.default_tts_cache): shutil.rmtree(self.default_tts_cache) diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 7be944305da..16715266b8c 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -35,7 +35,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 554de025e58..f3439700e33 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -30,7 +30,7 @@ def entity_reg(hass): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/vizio/__init__.py b/tests/components/vizio/__init__.py new file mode 100644 index 00000000000..f6cd65f56c1 --- /dev/null +++ b/tests/components/vizio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Vizio integration.""" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py new file mode 100644 index 00000000000..cf6cdb6afdb --- /dev/null +++ b/tests/components/vizio/test_config_flow.py @@ -0,0 +1,508 @@ +"""Tests for Vizio config flow.""" +import logging + +from asynctest import patch +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV +from homeassistant.components.vizio.const import ( + CONF_VOLUME_STEP, + DEFAULT_NAME, + DEFAULT_VOLUME_STEP, + DOMAIN, + VIZIO_SCHEMA, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + +NAME = "Vizio" +NAME2 = "Vizio2" +HOST = "192.168.1.1:9000" +HOST2 = "192.168.1.2:9000" +ACCESS_TOKEN = "deadbeef" +VOLUME_STEP = 2 +UNIQUE_ID = "testid" + +MOCK_USER_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, +} + +MOCK_IMPORT_VALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + CONF_VOLUME_STEP: VOLUME_STEP, +} + +MOCK_INVALID_TV_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, +} + +MOCK_SPEAKER_CONFIG = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, +} + +VIZIO_ZEROCONF_SERVICE_TYPE = "_viziocast._tcp.local." +ZEROCONF_NAME = f"{NAME}.{VIZIO_ZEROCONF_SERVICE_TYPE}" +ZEROCONF_HOST = HOST.split(":")[0] +ZEROCONF_PORT = HOST.split(":")[1] + +MOCK_ZEROCONF_ENTRY = { + CONF_TYPE: VIZIO_ZEROCONF_SERVICE_TYPE, + CONF_NAME: ZEROCONF_NAME, + CONF_HOST: ZEROCONF_HOST, + CONF_PORT: ZEROCONF_PORT, + "properties": {"name": "SB4031-D5"}, +} + + +@pytest.fixture(name="vizio_connect") +def vizio_connect_fixture(): + """Mock valid vizio device and entry setup.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=True, + ), patch( + "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", + return_value=UNIQUE_ID, + ): + yield + + +@pytest.fixture(name="vizio_bypass_setup") +def vizio_bypass_setup_fixture(): + """Mock component setup.""" + with patch("homeassistant.components.vizio.async_setup_entry", return_value=True): + yield + + +@pytest.fixture(name="vizio_bypass_update") +def vizio_bypass_update_fixture(): + """Mock component update.""" + with patch( + "homeassistant.components.vizio.media_player.VizioAsync.can_connect", + return_value=True, + ), patch("homeassistant.components.vizio.media_player.VizioDevice.async_update"): + yield + + +@pytest.fixture(name="vizio_guess_device_type") +def vizio_guess_device_type_fixture(): + """Mock vizio async_guess_device_type function.""" + with patch( + "homeassistant.components.vizio.config_flow.async_guess_device_type", + return_value="speaker", + ): + yield + + +@pytest.fixture(name="vizio_cant_connect") +def vizio_cant_connect_fixture(): + """Mock vizio device cant connect.""" + with patch( + "homeassistant.components.vizio.config_flow.VizioAsync.validate_ha_config", + return_value=False, + ): + yield + + +async def test_user_flow_minimum_fields( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test user config flow with minimum fields.""" + # test form shows + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + + +async def test_user_flow_all_fields( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test user config flow with all fields.""" + # test form shows + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN + + +async def test_options_flow(hass: HomeAssistantType) -> None: + """Test options config flow.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + + +async def test_user_host_already_configured( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test host is already configured during user setup.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} + ) + entry.add_to_hass(hass) + fail_entry = MOCK_SPEAKER_CONFIG.copy() + fail_entry[CONF_NAME] = "newtestname" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "host_exists"} + + +async def test_user_host_already_configured_no_port( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test host is already configured during user setup when existing entry has no port.""" + # Mock entry without port so we can test that the same entry WITH a port will fail + no_port_entry = MOCK_SPEAKER_CONFIG.copy() + no_port_entry[CONF_HOST] = no_port_entry[CONF_HOST].split(":")[0] + entry = MockConfigEntry( + domain=DOMAIN, data=no_port_entry, options={CONF_VOLUME_STEP: VOLUME_STEP} + ) + entry.add_to_hass(hass) + fail_entry = MOCK_SPEAKER_CONFIG.copy() + fail_entry[CONF_NAME] = "newtestname" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "host_exists"} + + +async def test_user_name_already_configured( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test name is already configured during user setup.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} + ) + entry.add_to_hass(hass) + + fail_entry = MOCK_SPEAKER_CONFIG.copy() + fail_entry[CONF_HOST] = "0.0.0.0" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_user_esn_already_exists( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test ESN is already configured with different host and name during user setup.""" + # Set up new entry + MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, unique_id=UNIQUE_ID + ).add_to_hass(hass) + + # Set up new entry with same unique_id but different host and name + fail_entry = MOCK_SPEAKER_CONFIG.copy() + fail_entry[CONF_HOST] = HOST2 + fail_entry[CONF_NAME] = NAME2 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup_with_diff_host_and_name" + + +async def test_user_error_on_could_not_connect( + hass: HomeAssistantType, vizio_cant_connect: pytest.fixture +) -> None: + """Test with could_not_connect during user_setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cant_connect"} + + +async def test_user_error_on_tv_needs_token( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test when config fails custom validation for non null access token when device_class = tv during user setup.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_INVALID_TV_CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "tv_needs_token"} + + +async def test_import_flow_minimum_fields( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test import config flow with minimum fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)( + {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER} + ), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP + + +async def test_import_flow_all_fields( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test import config flow with all fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV + assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + + +async def test_import_entity_already_configured( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test entity is already configured during import setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), + options={CONF_VOLUME_STEP: VOLUME_STEP}, + ) + entry.add_to_hass(hass) + fail_entry = vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG.copy()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import_flow_update_options( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: + """Test import config flow with updated options.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), + ) + await hass.async_block_till_done() + + assert result["result"].options == {CONF_VOLUME_STEP: VOLUME_STEP} + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry_id = result["result"].entry_id + + updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() + updated_config[CONF_VOLUME_STEP] = VOLUME_STEP + 1 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(updated_config), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + assert ( + hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] + == VOLUME_STEP + 1 + ) + + +async def test_import_flow_update_name( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: + """Test import config flow with updated name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), + ) + await hass.async_block_till_done() + + assert result["result"].data[CONF_NAME] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry_id = result["result"].entry_id + + updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() + updated_config[CONF_NAME] = NAME2 + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(updated_config), + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + assert hass.config_entries.async_get_entry(entry_id).data[CONF_NAME] == NAME2 + + +async def test_zeroconf_flow( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, + vizio_guess_device_type: pytest.fixture, +) -> None: + """Test zeroconf config flow.""" + discovery_info = MOCK_ZEROCONF_ENTRY.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + + # Form should always show even if all required properties are discovered + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # Apply discovery updates to entry to mimick when user hits submit without changing + # defaults which were set from discovery parameters + user_input = result["data_schema"](discovery_info) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER + + +async def test_zeroconf_flow_already_configured( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, +) -> None: + """Test entity is already configured during zeroconf setup.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} + ) + entry.add_to_hass(hass) + + # Try rediscovering same device + discovery_info = MOCK_ZEROCONF_ENTRY.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info + ) + + # Flow should abort because device is already setup + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index b0be238f971..e415734bec2 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -21,7 +21,6 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, SERVICE_VOLUME_MUTE, - STATE_ON, ) from homeassistant.setup import async_setup_component @@ -79,7 +78,6 @@ async def test_select_source_with_empty_source_list(hass, client): await hass.services.async_call(media_player.DOMAIN, SERVICE_SELECT_SOURCE, data) await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_ID, STATE_ON) client.launch_app.assert_not_called() client.set_input.assert_not_called() diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index acb69dddf4e..4a48dcee571 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -11,7 +11,6 @@ from homeassistant.components.withings.common import ( NotAuthenticatedError, WithingsDataManager, ) -from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt @@ -27,17 +26,6 @@ def withings_api_fixture() -> WithingsApi: return withings_api -@pytest.fixture -def mock_time_zone(): - """Provide an alternative time zone.""" - patch_time_zone = patch( - "homeassistant.util.dt.DEFAULT_TIME_ZONE", - new=dt.get_time_zone("America/Los_Angeles"), - ) - with patch_time_zone: - yield - - @pytest.fixture(name="data_manager") def data_manager_fixture(hass, withings_api: WithingsApi) -> WithingsDataManager: """Provide data manager.""" @@ -122,20 +110,26 @@ async def test_data_manager_call_throttle_disabled( async def test_data_manager_update_sleep_date_range( - hass: HomeAssistant, data_manager: WithingsDataManager, mock_time_zone + data_manager: WithingsDataManager, ) -> None: """Test method.""" - update_start_time = dt.now() - await data_manager.update_sleep() + patch_time_zone = patch( + "homeassistant.util.dt.DEFAULT_TIME_ZONE", + new=dt.get_time_zone("America/Los_Angeles"), + ) - call_args = data_manager.api.sleep_get.call_args_list[0][1] - startdate = call_args.get("startdate") - enddate = call_args.get("enddate") + with patch_time_zone: + update_start_time = dt.now() + await data_manager.update_sleep() - assert startdate.tzname() == "PST" + call_args = data_manager.api.sleep_get.call_args_list[0][1] + startdate = call_args.get("startdate") + enddate = call_args.get("enddate") - assert enddate.tzname() == "PST" - assert startdate.tzname() == "PST" - assert update_start_time < enddate - assert enddate < update_start_time + timedelta(seconds=1) - assert enddate > startdate + assert startdate.tzname() == "PST" + + assert enddate.tzname() == "PST" + assert startdate.tzname() == "PST" + assert update_start_time < enddate + assert enddate < update_start_time + timedelta(seconds=1) + assert enddate > startdate diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index a1247a8c373..779e39c67ce 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -3,11 +3,13 @@ from datetime import datetime from asynctest import patch +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.wled.const import ( ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_BYTES, + DOMAIN, ) from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant @@ -22,11 +24,31 @@ async def test_sensors( ) -> None: """Test the creation and values of the WLED sensors.""" + entry = await init_integration(hass, aioclient_mock, skip_setup=True) + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_uptime", + suggested_object_id="wled_rgb_light_uptime", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_free_heap", + suggested_object_id="wled_rgb_light_free_memory", + disabled_by=None, + ) + + # Setup test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): - await init_integration(hass, aioclient_mock) - - entity_registry = await hass.helpers.entity_registry.async_get_registry() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("sensor.wled_rgb_light_estimated_current") assert state @@ -36,7 +58,7 @@ async def test_sensors( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA assert state.state == "470" - entry = entity_registry.async_get("sensor.wled_rgb_light_estimated_current") + entry = registry.async_get("sensor.wled_rgb_light_estimated_current") assert entry assert entry.unique_id == "aabbccddeeff_estimated_current" @@ -46,7 +68,7 @@ async def test_sensors( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "2019-11-11T09:10:00+00:00" - entry = entity_registry.async_get("sensor.wled_rgb_light_uptime") + entry = registry.async_get("sensor.wled_rgb_light_uptime") assert entry assert entry.unique_id == "aabbccddeeff_uptime" @@ -56,6 +78,30 @@ async def test_sensors( assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES assert state.state == "14600" - entry = entity_registry.async_get("sensor.wled_rgb_light_free_memory") + entry = registry.async_get("sensor.wled_rgb_light_free_memory") assert entry assert entry.unique_id == "aabbccddeeff_free_heap" + + +async def test_disabled_by_default_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the disabled by default WLED sensors.""" + await init_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.wled_rgb_light_uptime") + assert state is None + + entry = registry.async_get("sensor.wled_rgb_light_uptime") + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + state = hass.states.get("sensor.wled_rgb_light_free_memory") + assert state is None + + entry = registry.async_get("sensor.wled_rgb_light_free_memory") + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index b0c8a48abbd..47c7a98023c 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -204,12 +204,12 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80" assert state.attributes.get(ATTR_CLEANING_TIME) == 155 assert state.attributes.get(ATTR_CLEANED_AREA) == 123 - assert state.attributes.get(ATTR_FAN_SPEED) == "Quiet" + assert state.attributes.get(ATTR_FAN_SPEED) == "Silent" assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [ - "Quiet", - "Balanced", + "Silent", + "Standard", + "Medium", "Turbo", - "Max", "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 @@ -273,7 +273,7 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, - {"entity_id": entity_id, "fan_speed": "turbo"}, + {"entity_id": entity_id, "fan_speed": "Medium"}, blocking=True, ) mock_mirobo_is_got_error.assert_has_calls( @@ -348,10 +348,10 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): assert state.attributes.get(ATTR_CLEANED_AREA) == 133 assert state.attributes.get(ATTR_FAN_SPEED) == 99 assert state.attributes.get(ATTR_FAN_SPEED_LIST) == [ - "Quiet", - "Balanced", + "Silent", + "Standard", + "Medium", "Turbo", - "Max", "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 34a500f1733..c5790dc718c 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -31,7 +31,7 @@ def get_service_info_mock(service_type, name): weight=0, priority=0, server="name.local.", - properties={b"macaddress": b"ABCDEF012345"}, + properties={b"macaddress": b"ABCDEF012345", b"non-utf8-value": b"ABCDEF\x8a"}, ) @@ -93,3 +93,16 @@ async def test_homekit_match_full(hass, mock_zeroconf): assert len(mock_service_browser.mock_calls) == 1 assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "hue" + + +async def test_info_from_service_non_utf8(hass): + """Test info_from_service handles non UTF-8 property values correctly.""" + service_type = "_test._tcp.local." + info = zeroconf.info_from_service( + get_service_info_mock(service_type, f"test.{service_type}") + ) + raw_info = info["properties"].pop("_raw", False) + assert raw_info + assert len(info["properties"]) <= len(raw_info) + assert "non-utf8-value" not in info["properties"] + assert raw_info["non-utf8-value"] is not None diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 06712e638f6..9b6a8b5b55f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -78,7 +78,7 @@ def patch_cluster(cluster): class FakeDevice: """Fake device for mocking zigpy.""" - def __init__(self, ieee, manufacturer, model): + def __init__(self, ieee, manufacturer, model, node_desc=None): """Init fake device.""" self._application = APPLICATION self.ieee = zigpy.types.EUI64.convert(ieee) @@ -95,6 +95,9 @@ class FakeDevice: self.node_desc = zigpy.zdo.types.NodeDescriptor() self.add_to_group = CoroutineMock() self.remove_from_group = CoroutineMock() + if node_desc is None: + node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" + self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] def make_device(endpoints, ieee, manufacturer, model): diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index d8abfb8f227..18344172d29 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -2,6 +2,7 @@ from unittest import mock from unittest.mock import patch +import asynctest import pytest import zigpy from zigpy.application import ControllerApplication @@ -12,7 +13,7 @@ from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.store import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from .common import async_setup_entry +from .common import FakeDevice, FakeEndpoint, async_setup_entry FIXTURE_GRP_ID = 0x1001 FIXTURE_GRP_NAME = "fixture group" @@ -49,9 +50,10 @@ async def zha_gateway_fixture(hass, config_entry): gateway.ha_device_registry = dev_reg gateway.application_controller = mock.MagicMock(spec_set=ControllerApplication) groups = zigpy.group.Groups(gateway.application_controller) - groups.listener_event = mock.MagicMock() + groups.add_listener(gateway) groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) - gateway.application_controller.groups = groups + gateway.application_controller.configure_mock(groups=groups) + gateway._initialize_groups() return gateway @@ -70,3 +72,52 @@ async def setup_zha(hass, config_entry): # init ZHA await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.async_block_till_done() + + +@pytest.fixture +def channel(): + """Channel mock factory fixture.""" + + def channel(name: str, cluster_id: int, endpoint_id: int = 1): + ch = mock.MagicMock() + ch.name = name + ch.generic_id = f"channel_0x{cluster_id:04x}" + ch.id = f"{endpoint_id}:0x{cluster_id:04x}" + ch.async_configure = asynctest.CoroutineMock() + ch.async_initialize = asynctest.CoroutineMock() + return ch + + return channel + + +@pytest.fixture +def zigpy_device_mock(): + """Make a fake device using the specified cluster classes.""" + + def _mock_dev( + endpoints, + ieee="00:0d:6f:00:0a:90:69:e7", + manufacturer="FakeManufacturer", + model="FakeModel", + node_desc=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", + ): + """Make a fake device using the specified cluster classes.""" + device = FakeDevice(ieee, manufacturer, model, node_desc) + for epid, ep in endpoints.items(): + endpoint = FakeEndpoint(manufacturer, model, epid) + endpoint.device = device + device.endpoints[epid] = endpoint + endpoint.device_type = ep["device_type"] + profile_id = ep.get("profile_id") + if profile_id: + endpoint.profile_id = profile_id + + for cluster_id in ep.get("in_clusters", []): + endpoint.add_input_cluster(cluster_id) + + for cluster_id in ep.get("out_clusters", []): + endpoint.add_output_cluster(cluster_id) + + return device + + return _mock_dev diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 557cc0f2c5c..c5ad4d3fbc0 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -6,8 +6,6 @@ import homeassistant.components.zha.core.channels as channels import homeassistant.components.zha.core.device as zha_device import homeassistant.components.zha.core.registries as registries -from .common import make_device - @pytest.fixture def ieee(): @@ -64,9 +62,11 @@ def nwk(): (0x1000, 1, {}), ], ) -async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, hass): +async def test_in_channel_config( + cluster_id, bind_count, attrs, zha_gateway, hass, zigpy_device_mock +): """Test ZHA core channel configuration for input clusters.""" - zigpy_dev = make_device( + zigpy_dev = zigpy_device_mock( {1: {"in_clusters": [cluster_id], "out_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", @@ -120,9 +120,11 @@ async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, has (0x1000, 1), ], ) -async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): +async def test_out_channel_config( + cluster_id, bind_count, zha_gateway, hass, zigpy_device_mock +): """Test ZHA core channel configuration for output clusters.""" - zigpy_dev = make_device( + zigpy_dev = zigpy_device_mock( {1: {"out_clusters": [cluster_id], "in_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py new file mode 100644 index 00000000000..9d1c019c718 --- /dev/null +++ b/tests/components/zha/test_cover.py @@ -0,0 +1,129 @@ +"""Test zha cover.""" +from unittest.mock import MagicMock, call, patch + +import zigpy.types +import zigpy.zcl.clusters.closures as closures +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE + +from .common import ( + async_enable_traffic, + async_init_zigpy_device, + async_test_device_join, + find_entity_id, + make_attribute, + make_zcl_header, +) + +from tests.common import mock_coro + + +async def test_cover(hass, config_entry, zha_gateway): + """Test zha cover platform.""" + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [closures.WindowCovering.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, + ) + + async def get_chan_attr(*args, **kwargs): + return 100 + + with patch( + "homeassistant.components.zha.core.channels.ZigbeeChannel.get_attribute_value", + new=MagicMock(side_effect=get_chan_attr), + ) as get_attr_mock: + # load up cover domain + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + assert get_attr_mock.call_count == 2 + assert get_attr_mock.call_args[0][0] == "current_position_lift_percentage" + + cluster = zigpy_device.endpoints.get(1).window_covering + zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + # test that the cover was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + await hass.async_block_till_done() + + attr = make_attribute(8, 100) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) + await hass.async_block_till_done() + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_CLOSED + + # test to see if it opens + attr = make_attribute(8, 0) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OPEN + + # close from UI + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x1, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, 0x1, (), expect_reply=True, manufacturer=None + ) + + # open from UI + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x0, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, 0x0, (), expect_reply=True, manufacturer=None + ) + + # set position UI + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x5, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, + "set_cover_position", + {"entity_id": entity_id, "position": 47}, + blocking=True, + ) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, 0x5, (zigpy.types.uint8_t,), 53, expect_reply=True, manufacturer=None + ) + + # stop from UI + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x2, zcl_f.Status.SUCCESS]) + ): + await hass.services.async_call( + DOMAIN, "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert cluster.request.call_count == 1 + assert cluster.request.call_args == call( + False, 0x2, (), expect_reply=True, manufacturer=None + ) + + await async_test_device_join( + hass, zha_gateway, closures.WindowCovering.cluster_id, entity_id + ) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 62884fe72ae..c3195559d20 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -26,7 +26,7 @@ COMMAND_SINGLE = "single" @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "zha", "warning_device_warn") diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 75e8538c5bf..973b6673671 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -38,7 +38,7 @@ def _same_lists(list_a, list_b): @pytest.fixture def calls(hass): - """Track calls to a mock serivce.""" + """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 91805acc448..9ed88c86e51 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,34 +1,50 @@ """Test zha device discovery.""" import asyncio +import re from unittest import mock import pytest -from homeassistant.components.zha.core.channels import EventRelayChannel import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.discovery as disc import homeassistant.components.zha.core.gateway as core_zha_gw +import homeassistant.helpers.entity_registry -from .common import make_device from .zha_devices_list import DEVICES +NO_TAIL_ID = re.compile("_\\d$") + @pytest.mark.parametrize("device", DEVICES) -async def test_devices(device, zha_gateway: core_zha_gw.ZHAGateway, hass, config_entry): +async def test_devices( + device, + zha_gateway: core_zha_gw.ZHAGateway, + hass, + config_entry, + zigpy_device_mock, + monkeypatch, +): """Test device discovery.""" - zigpy_device = make_device( + zigpy_device = zigpy_device_mock( device["endpoints"], "00:11:22:33:44:55:66:77", device["manufacturer"], device["model"], + node_desc=device["node_descriptor"], + ) + + _dispatch = mock.MagicMock(wraps=disc.async_dispatch_discovery_info) + monkeypatch.setattr(core_zha_gw, "async_dispatch_discovery_info", _dispatch) + entity_registry = await homeassistant.helpers.entity_registry.async_get_registry( + hass ) with mock.patch( "homeassistant.components.zha.core.discovery._async_create_cluster_channel", wraps=disc._async_create_cluster_channel, - ) as cr_ch: + ): await zha_gateway.async_device_restored(zigpy_device) await hass.async_block_till_done() tasks = [ @@ -45,11 +61,25 @@ async def test_devices(device, zha_gateway: core_zha_gw.ZHAGateway, hass, config ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS } - event_channels = { - arg[0].cluster_id - for arg, kwarg in cr_ch.call_args_list - if kwarg.get("channel_class") == EventRelayChannel + zha_dev = zha_gateway.get_device(zigpy_device.ieee) + event_channels = { # pylint: disable=protected-access + ch.id for ch in zha_dev._relay_channels.values() } assert zha_entities == set(device["entities"]) assert event_channels == set(device["event_channels"]) + + entity_map = device["entity_map"] + for calls in _dispatch.call_args_list: + discovery_info = calls[0][2] + unique_id = discovery_info["unique_id"] + channels = discovery_info["channels"] + component = discovery_info["component"] + key = (component, unique_id) + entity_id = entity_registry.async_get_entity_id(component, "zha", unique_id) + + assert key in entity_map + assert entity_id is not None + no_tail_id = NO_TAIL_ID.sub("", entity_map[key]["entity_id"]) + assert entity_id.startswith(no_tail_id) + assert set([ch.name for ch in channels]) == set(entity_map[key]["channels"]) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py new file mode 100644 index 00000000000..5c6e8ecfe7a --- /dev/null +++ b/tests/components/zha/test_gateway.py @@ -0,0 +1,29 @@ +"""Test ZHA Gateway.""" +import zigpy.zcl.clusters.general as general + +import homeassistant.components.zha.core.const as zha_const + +from .common import async_enable_traffic, async_init_zigpy_device + + +async def test_device_left(hass, config_entry, zha_gateway): + """Test zha fan platform.""" + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [general.Basic.cluster_id], [], None, zha_gateway + ) + + # load up fan domain + await hass.config_entries.async_forward_entry_setup(config_entry, zha_const.SENSOR) + await hass.async_block_till_done() + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + assert zha_device.available is False + + await async_enable_traffic(hass, zha_gateway, [zha_device]) + assert zha_device.available is True + + zha_gateway.device_left(zigpy_device) + assert zha_device.available is False diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index 9f77330dd55..383b61e6c66 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -19,16 +19,10 @@ def zha_device(): @pytest.fixture -def channels(): +def channels(channel): """Return a mock of channels.""" - def channel(name, chan_id): - ch = mock.MagicMock() - ch.name = name - ch.generic_id = chan_id - return ch - - return [channel("level", "channel_0x0008"), channel("on_off", "channel_0x0006")] + return [channel("level", 8), channel("on_off", 6)] @pytest.mark.parametrize( diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 3e02542a4fb..4c913e10034 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,6 @@ """Test zha sensor.""" +from unittest import mock + import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.homeautomation as homeautomation @@ -179,8 +181,26 @@ async def async_test_metering(hass, device_info): async def async_test_electrical_measurement(hass, device_info): """Test electrical measurement sensor.""" - await send_attribute_report(hass, device_info["cluster"], 1291, 100) - assert_state(hass, device_info, "100", "W") + with mock.patch( + ( + "homeassistant.components.zha.core.channels.homeautomation" + ".ElectricalMeasurementChannel.divisor" + ), + new_callable=mock.PropertyMock, + ) as divisor_mock: + divisor_mock.return_value = 1 + await send_attribute_report(hass, device_info["cluster"], 1291, 100) + assert_state(hass, device_info, "100", "W") + + await send_attribute_report(hass, device_info["cluster"], 1291, 99) + assert_state(hass, device_info, "99", "W") + + divisor_mock.return_value = 10 + await send_attribute_report(hass, device_info["cluster"], 1291, 1000) + assert_state(hass, device_info, "100", "W") + + await send_attribute_report(hass, device_info["cluster"], 1291, 99) + assert_state(hass, device_info, "9.9", "W") async def send_attribute_report(hass, cluster, attrid, value): diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index d5875edc9e2..a8c83406435 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -2,8 +2,9 @@ DEVICES = [ { + "device_no": 0, "endpoints": { - "1": { + 1: { "device_type": 2080, "endpoint_id": 1, "in_clusters": [0, 3, 4096, 64716], @@ -12,13 +13,17 @@ DEVICES = [ } }, "entities": [], - "event_channels": [6, 8], + "entity_map": {}, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "ADUROLIGHT", "model": "Adurolight_NCC", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", + "zha_quirks": "AdurolightNCC", }, { + "device_no": 1, "endpoints": { - "5": { + 5: { "device_type": 1026, "endpoint_id": 5, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], @@ -31,13 +36,32 @@ DEVICES = [ "sensor.bosch_isw_zpr1_wp13_77665544_power", "sensor.bosch_isw_zpr1_wp13_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-5-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.bosch_isw_zpr1_wp13_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-5-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Bosch", "model": "ISW-ZPR1-WP13", + "node_descriptor": b"\x02@\x08\x00\x00l\x00\x00\x00\x00\x00\x00\x00", }, { + "device_no": 2, "endpoints": { - "1": { + 1: { "device_type": 1, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 2821], @@ -45,17 +69,24 @@ DEVICES = [ "profile_id": 260, } }, - "entities": [ - "binary_sensor.centralite_3130_77665544_on_off", - "sensor.centralite_3130_77665544_power", - ], - "event_channels": [6, 8], + "entities": ["sensor.centralite_3130_77665544_power"], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_3130_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "CentraLite", "model": "3130", + "node_descriptor": b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLite3130", }, { + "device_no": 3, "endpoints": { - "1": { + 1: { "device_type": 81, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 1794, 2820, 2821, 64515], @@ -64,17 +95,36 @@ DEVICES = [ } }, "entities": [ - "sensor.centralite_3210_l_77665544_smartenergy_metering", "sensor.centralite_3210_l_77665544_electrical_measurement", + "sensor.centralite_3210_l_77665544_smartenergy_metering", "switch.centralite_3210_l_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.centralite_3210_l_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.centralite_3210_l_77665544_smartenergy_metering", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.centralite_3210_l_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "3210-L", + "node_descriptor": b"\x01@\x8eN\x10RR\x00\x00\x00R\x00\x00", }, { + "device_no": 4, "endpoints": { - "1": { + 1: { "device_type": 770, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 2821, 64581], @@ -83,24 +133,44 @@ DEVICES = [ } }, "entities": [ + "sensor.centralite_3310_s_77665544_manufacturer_specific", "sensor.centralite_3310_s_77665544_power", "sensor.centralite_3310_s_77665544_temperature", - "sensor.centralite_3310_s_77665544_manufacturer_specific", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_3310_s_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.centralite_3310_s_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { + "channels": ["manufacturer_specific"], + "entity_class": "Humidity", + "entity_id": "sensor.centralite_3310_s_77665544_manufacturer_specific", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "3310-S", + "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLite3310S", }, { + "device_no": 5, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], "out_clusters": [25], "profile_id": 260, }, - "2": { + 2: { "device_type": 12, "endpoint_id": 2, "in_clusters": [0, 3, 2821, 64527], @@ -110,23 +180,43 @@ DEVICES = [ }, "entities": [ "binary_sensor.centralite_3315_s_77665544_ias_zone", - "sensor.centralite_3315_s_77665544_temperature", "sensor.centralite_3315_s_77665544_power", + "sensor.centralite_3315_s_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_3315_s_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.centralite_3315_s_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.centralite_3315_s_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "3315-S", + "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLiteIASSensor", }, { + "device_no": 6, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], "out_clusters": [25], "profile_id": 260, }, - "2": { + 2: { "device_type": 12, "endpoint_id": 2, "in_clusters": [0, 3, 2821, 64527], @@ -136,23 +226,43 @@ DEVICES = [ }, "entities": [ "binary_sensor.centralite_3320_l_77665544_ias_zone", - "sensor.centralite_3320_l_77665544_temperature", "sensor.centralite_3320_l_77665544_power", + "sensor.centralite_3320_l_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_3320_l_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.centralite_3320_l_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.centralite_3320_l_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "3320-L", + "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLiteIASSensor", }, { + "device_no": 7, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], "out_clusters": [25], "profile_id": 260, }, - "2": { + 2: { "device_type": 263, "endpoint_id": 2, "in_clusters": [0, 3, 2821, 64582], @@ -162,23 +272,43 @@ DEVICES = [ }, "entities": [ "binary_sensor.centralite_3326_l_77665544_ias_zone", - "sensor.centralite_3326_l_77665544_temperature", "sensor.centralite_3326_l_77665544_power", + "sensor.centralite_3326_l_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_3326_l_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.centralite_3326_l_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.centralite_3326_l_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "3326-L", + "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLiteMotionSensor", }, { + "device_no": 8, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], "out_clusters": [25], "profile_id": 260, }, - "2": { + 2: { "device_type": 263, "endpoint_id": 2, "in_clusters": [0, 3, 1030, 2821], @@ -187,25 +317,50 @@ DEVICES = [ }, }, "entities": [ - "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - "sensor.centralite_motion_sensor_a_77665544_temperature", + "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", "sensor.centralite_motion_sensor_a_77665544_power", + "sensor.centralite_motion_sensor_a_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.centralite_motion_sensor_a_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.centralite_motion_sensor_a_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { + "channels": ["occupancy"], + "entity_class": "Occupancy", + "entity_id": "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + }, + }, "event_channels": [], "manufacturer": "CentraLite", "model": "Motion Sensor-A", + "node_descriptor": b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLite3305S", }, { + "device_no": 9, "endpoints": { - "1": { + 1: { "device_type": 81, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 1794], "out_clusters": [0], "profile_id": 260, }, - "4": { + 4: { "device_type": 9, "endpoint_id": 4, "in_clusters": [], @@ -217,13 +372,27 @@ DEVICES = [ "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + }, + }, "event_channels": [], "manufacturer": "ClimaxTechnology", "model": "PSMP5_00.00.02.02TC", + "node_descriptor": b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { + "device_no": 10, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 3, 1280, 1282], @@ -234,13 +403,22 @@ DEVICES = [ "entities": [ "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone" ], + "entity_map": { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", + } + }, "event_channels": [], "manufacturer": "ClimaxTechnology", "model": "SD8SC_00.00.03.12TC", + "node_descriptor": b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { + "device_no": 11, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 3, 1280], @@ -251,20 +429,29 @@ DEVICES = [ "entities": [ "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone" ], + "entity_map": { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", + } + }, "event_channels": [], "manufacturer": "ClimaxTechnology", "model": "WS15_00.00.03.03TC", + "node_descriptor": b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { + "device_no": 12, "endpoints": { - "11": { + 11: { "device_type": 528, "endpoint_id": 11, "in_clusters": [0, 3, 4, 5, 6, 8, 768], "out_clusters": [], "profile_id": 49246, }, - "13": { + 13: { "device_type": 57694, "endpoint_id": 13, "in_clusters": [4096], @@ -275,13 +462,78 @@ DEVICES = [ "entities": [ "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-11"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "Feibit Inc co.", "model": "FB56-ZCW08KU1.1", + "node_descriptor": b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", }, { + "device_no": 13, "endpoints": { - "1": { + 1: { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1280, 1282], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + "sensor.heiman_smokesensor_em_77665544_power", + ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.heiman_smokesensor_em_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + }, + }, + "event_channels": [], + "manufacturer": "HEIMAN", + "model": "SmokeSensor-EM", + "node_descriptor": b"\x02@\x80\x0b\x12RR\x00\x00\x00R\x00\x00", + }, + { + "device_no": 14, + "endpoints": { + 1: { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["binary_sensor.heiman_co_v16_77665544_ias_zone"], + "entity_map": { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.heiman_co_v16_77665544_ias_zone", + }, + }, + "event_channels": [], + "manufacturer": "Heiman", + "model": "CO_V16", + "node_descriptor": b"\x02@\x84\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", + }, + { + "device_no": 15, + "endpoints": { + 1: { "device_type": 1027, "endpoint_id": 1, "in_clusters": [0, 1, 3, 4, 9, 1280, 1282], @@ -289,17 +541,23 @@ DEVICES = [ "profile_id": 260, } }, - "entities": [ - "binary_sensor.heiman_warningdevice_77665544_ias_zone", - "sensor.heiman_warningdevice_77665544_power", - ], + "entities": ["binary_sensor.heiman_warningdevice_77665544_ias_zone"], + "entity_map": { + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.heiman_warningdevice_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Heiman", "model": "WarningDevice", + "node_descriptor": b"\x01@\x8e\x0b\x12RR\x00\x00\x00R\x00\x00", }, { + "device_no": 16, "endpoints": { - "6": { + 6: { "device_type": 1026, "endpoint_id": 6, "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280], @@ -308,25 +566,50 @@ DEVICES = [ } }, "entities": [ - "sensor.hivehome_com_mot003_77665544_temperature", - "sensor.hivehome_com_mot003_77665544_power", - "sensor.hivehome_com_mot003_77665544_illuminance", "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + "sensor.hivehome_com_mot003_77665544_illuminance", + "sensor.hivehome_com_mot003_77665544_power", + "sensor.hivehome_com_mot003_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-6-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.hivehome_com_mot003_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-6-1024"): { + "channels": ["illuminance"], + "entity_class": "Illuminance", + "entity_id": "sensor.hivehome_com_mot003_77665544_illuminance", + }, + ("sensor", "00:11:22:33:44:55:66:77-6-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.hivehome_com_mot003_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "HiveHome.com", "model": "MOT003", + "node_descriptor": b"\x02@\x809\x10PP\x00\x00\x00P\x00\x00", + "zha_quirks": "MOT003", }, { + "device_no": 17, "endpoints": { - "1": { + 1: { "device_type": 268, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 4096, 64636], "out_clusters": [5, 25, 32, 4096], "profile_id": 260, }, - "242": { + 242: { "device_type": 97, "endpoint_id": 242, "in_clusters": [33], @@ -337,13 +620,22 @@ DEVICES = [ "entities": [ "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E12 WS opal 600lm", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", }, { + "device_no": 18, "endpoints": { - "1": { + 1: { "device_type": 512, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], @@ -354,13 +646,22 @@ DEVICES = [ "entities": [ "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 CWS opal 600lm", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 19, "endpoints": { - "1": { + 1: { "device_type": 256, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], @@ -371,13 +672,22 @@ DEVICES = [ "entities": [ "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 W opal 1000lm", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 20, "endpoints": { - "1": { + 1: { "device_type": 544, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], @@ -388,13 +698,22 @@ DEVICES = [ "entities": [ "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 WS opal 980lm", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 21, "endpoints": { - "1": { + 1: { "device_type": 256, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], @@ -405,13 +724,22 @@ DEVICES = [ "entities": [ "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 opal 1000lm", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 22, "endpoints": { - "1": { + 1: { "device_type": 266, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 64636], @@ -420,13 +748,23 @@ DEVICES = [ } }, "entities": ["switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off"], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", + } + }, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI control outlet", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", + "zha_quirks": "TradfriPlug", }, { + "device_no": 23, "endpoints": { - "1": { + 1: { "device_type": 2128, "endpoint_id": 1, "in_clusters": [0, 1, 3, 9, 2821, 4096], @@ -438,13 +776,28 @@ DEVICES = [ "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", ], - "event_channels": [6], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Opening", + "entity_id": "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + }, + }, + "event_channels": ["1:0x0006"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI motion sensor", + "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", + "zha_quirks": "IkeaTradfriMotion", }, { + "device_no": 24, "endpoints": { - "1": { + 1: { "device_type": 2080, "endpoint_id": 1, "in_clusters": [0, 1, 3, 9, 32, 4096, 64636], @@ -453,13 +806,23 @@ DEVICES = [ } }, "entities": ["sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power"], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI on/off switch", + "node_descriptor": b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", + "zha_quirks": "IkeaTradfriRemote2Btn", }, { + "device_no": 25, "endpoints": { - "1": { + 1: { "device_type": 2096, "endpoint_id": 1, "in_clusters": [0, 1, 3, 9, 2821, 4096], @@ -468,20 +831,30 @@ DEVICES = [ } }, "entities": ["sensor.ikea_of_sweden_tradfri_remote_control_77665544_power"], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI remote control", + "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", + "zha_quirks": "IkeaTradfriRemote", }, { + "device_no": 26, "endpoints": { - "1": { + 1: { "device_type": 8, "endpoint_id": 1, "in_clusters": [0, 3, 9, 2821, 4096, 64636], "out_clusters": [25, 32, 4096], "profile_id": 260, }, - "242": { + 242: { "device_type": 97, "endpoint_id": 242, "in_clusters": [33], @@ -490,13 +863,16 @@ DEVICES = [ }, }, "entities": [], + "entity_map": {}, "event_channels": [], "manufacturer": "IKEA of Sweden", "model": "TRADFRI signal repeater", + "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", }, { + "device_no": 27, "endpoints": { - "1": { + 1: { "device_type": 2064, "endpoint_id": 1, "in_clusters": [0, 1, 3, 9, 2821, 4096], @@ -505,20 +881,29 @@ DEVICES = [ } }, "entities": ["sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power"], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI wireless dimmer", + "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 28, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], "out_clusters": [10, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 260, "endpoint_id": 2, "in_clusters": [0, 3, 2821], @@ -527,23 +912,37 @@ DEVICES = [ }, }, "entities": [ - "sensor.jasco_products_45852_77665544_smartenergy_metering", "light.jasco_products_45852_77665544_level_on_off", + "sensor.jasco_products_45852_77665544_smartenergy_metering", ], - "event_channels": [6, 8], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.jasco_products_45852_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.jasco_products_45852_77665544_smartenergy_metering", + }, + }, + "event_channels": ["2:0x0006", "2:0x0008"], "manufacturer": "Jasco Products", "model": "45852", + "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { + "device_no": 29, "endpoints": { - "1": { + 1: { "device_type": 256, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 1794, 2821], "out_clusters": [10, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 259, "endpoint_id": 2, "in_clusters": [0, 3, 2821], @@ -552,24 +951,37 @@ DEVICES = [ }, }, "entities": [ - "sensor.jasco_products_45856_77665544_smartenergy_metering", - "switch.jasco_products_45856_77665544_on_off", "light.jasco_products_45856_77665544_on_off", + "sensor.jasco_products_45856_77665544_smartenergy_metering", ], - "event_channels": [6], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.jasco_products_45856_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.jasco_products_45856_77665544_smartenergy_metering", + }, + }, + "event_channels": ["2:0x0006"], "manufacturer": "Jasco Products", "model": "45856", + "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { + "device_no": 30, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], "out_clusters": [10, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 260, "endpoint_id": 2, "in_clusters": [0, 3, 2821], @@ -578,16 +990,30 @@ DEVICES = [ }, }, "entities": [ - "sensor.jasco_products_45857_77665544_smartenergy_metering", "light.jasco_products_45857_77665544_level_on_off", + "sensor.jasco_products_45857_77665544_smartenergy_metering", ], - "event_channels": [6, 8], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.jasco_products_45857_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.jasco_products_45857_77665544_smartenergy_metering", + }, + }, + "event_channels": ["2:0x0006", "2:0x0008"], "manufacturer": "Jasco Products", "model": "45857", + "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", }, { + "device_no": 31, "endpoints": { - "1": { + 1: { "device_type": 3, "endpoint_id": 1, "in_clusters": [ @@ -611,18 +1037,48 @@ DEVICES = [ }, "entities": [ "binary_sensor.keen_home_inc_sv02_610_mp_1_3_77665544_manufacturer_specific", + "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", - "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { + "channels": ["pressure"], + "entity_class": "Pressure", + "entity_id": "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): { + "channels": ["manufacturer_specific"], + "entity_class": "BinarySensor", + "entity_id": "binary_sensor.keen_home_inc_sv02_610_mp_1_3_77665544_manufacturer_specific", + "default_match": True, + }, + }, "event_channels": [], "manufacturer": "Keen Home Inc", "model": "SV02-610-MP-1.3", + "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", }, { + "device_no": 32, "endpoints": { - "1": { + 1: { "device_type": 3, "endpoint_id": 1, "in_clusters": [ @@ -646,18 +1102,48 @@ DEVICES = [ }, "entities": [ "binary_sensor.keen_home_inc_sv02_612_mp_1_2_77665544_manufacturer_specific", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", - "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { + "channels": ["pressure"], + "entity_class": "Pressure", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): { + "channels": ["manufacturer_specific"], + "entity_class": "BinarySensor", + "entity_id": "binary_sensor.keen_home_inc_sv02_612_mp_1_2_77665544_manufacturer_specific", + "default_match": True, + }, + }, "event_channels": [], "manufacturer": "Keen Home Inc", "model": "SV02-612-MP-1.2", + "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", }, { + "device_no": 33, "endpoints": { - "1": { + 1: { "device_type": 3, "endpoint_id": 1, "in_clusters": [ @@ -681,19 +1167,50 @@ DEVICES = [ }, "entities": [ "binary_sensor.keen_home_inc_sv02_612_mp_1_3_77665544_manufacturer_specific", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { + "channels": ["pressure"], + "entity_class": "Pressure", + "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): { + "channels": ["manufacturer_specific"], + "entity_class": "BinarySensor", + "entity_id": "binary_sensor.keen_home_inc_sv02_612_mp_1_3_77665544_manufacturer_specific", + "default_match": True, + }, + }, "event_channels": [], "manufacturer": "Keen Home Inc", "model": "SV02-612-MP-1.3", + "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", + "zha_quirks": "KeenHomeSmartVent", }, { + "device_no": 34, "endpoints": { - "1": { - "device_type": 14, + 1: { + "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 514], "out_clusters": [3, 25], @@ -702,15 +1219,55 @@ DEVICES = [ }, "entities": [ "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", - "switch.king_of_fans_inc_hbuniversalcfremote_77665544_on_off", + "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", + }, + ("fan", "00:11:22:33:44:55:66:77-1-514"): { + "channels": ["fan"], + "entity_class": "ZhaFan", + "entity_id": "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + }, + }, "event_channels": [], "manufacturer": "King Of Fans, Inc.", "model": "HBUniversalCFRemote", + "node_descriptor": b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CeilingFan", }, { + "device_no": 35, "endpoints": { - "1": { + 1: { + "device_type": 2048, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4096, 64769], + "out_clusters": [3, 4, 6, 8, 25, 768, 4096], + "profile_id": 260, + } + }, + "entities": ["sensor.lds_zbt_cctswitch_d0001_77665544_power"], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lds_zbt_cctswitch_d0001_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300"], + "manufacturer": "LDS", + "model": "ZBT-CCTSwitch-D0001", + "node_descriptor": b"\x02@\x80h\x11RR\x00\x00,R\x00\x00", + "zha_quirks": "CCTSwitch", + }, + { + "device_no": 36, + "endpoints": { + 1: { "device_type": 258, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], @@ -719,13 +1276,22 @@ DEVICES = [ } }, "entities": ["light.ledvance_a19_rgbw_77665544_level_light_color_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "LEDVANCE", "model": "A19 RGBW", + "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 37, "endpoints": { - "1": { + 1: { "device_type": 258, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], @@ -734,13 +1300,22 @@ DEVICES = [ } }, "entities": ["light.ledvance_flex_rgbw_77665544_level_light_color_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "LEDVANCE", "model": "FLEX RGBW", + "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 38, "endpoints": { - "1": { + 1: { "device_type": 81, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 2821, 64513, 64520], @@ -749,13 +1324,22 @@ DEVICES = [ } }, "entities": ["switch.ledvance_plug_77665544_on_off"], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.ledvance_plug_77665544_on_off", + } + }, "event_channels": [], "manufacturer": "LEDVANCE", "model": "PLUG", + "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 39, "endpoints": { - "1": { + 1: { "device_type": 258, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], @@ -764,62 +1348,95 @@ DEVICES = [ } }, "entities": ["light.ledvance_rt_rgbw_77665544_level_light_color_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "LEDVANCE", "model": "RT RGBW", + "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 40, "endpoints": { - "1": { + 1: { "device_type": 81, "endpoint_id": 1, "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 16, 2820], "out_clusters": [10, 25], "profile_id": 260, }, - "100": { - "device_type": 263, - "endpoint_id": 100, - "in_clusters": [15], - "out_clusters": [4, 15], - "profile_id": 260, - }, - "2": { + 2: { "device_type": 9, "endpoint_id": 2, "in_clusters": [12], "out_clusters": [4, 12], "profile_id": 260, }, - "3": { + 3: { "device_type": 83, "endpoint_id": 3, "in_clusters": [12], "out_clusters": [12], "profile_id": 260, }, + 100: { + "device_type": 263, + "endpoint_id": 100, + "in_clusters": [15], + "out_clusters": [4, 15], + "profile_id": 260, + }, }, "entities": [ - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", "sensor.lumi_lumi_plug_maus01_77665544_analog_input", "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", - "sensor.lumi_lumi_plug_maus01_77665544_power", + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", "switch.lumi_lumi_plug_maus01_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.lumi_lumi_plug_maus01_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.plug.maus01", + "node_descriptor": b"\x01@\x8e_\x11\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "Plug", }, { + "device_no": 41, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 12, 16, 2820], "out_clusters": [10, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 257, "endpoint_id": 2, "in_clusters": [4, 5, 6, 16], @@ -828,33 +1445,57 @@ DEVICES = [ }, }, "entities": [ - "sensor.lumi_lumi_relay_c2acn01_77665544_analog_input", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", - "sensor.lumi_lumi_relay_c2acn01_77665544_power", "light.lumi_lumi_relay_c2acn01_77665544_on_off", "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + "sensor.lumi_lumi_relay_c2acn01_77665544_analog_input", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.lumi_lumi_relay_c2acn01_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_relay_c2acn01_77665544_analog_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + }, + ("light", "00:11:22:33:44:55:66:77-2"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.relay.c2acn01", + "node_descriptor": b"\x01@\x8e7\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "Relay", }, { + "device_no": 42, "endpoints": { - "1": { + 1: { "device_type": 24321, "endpoint_id": 1, "in_clusters": [0, 1, 3, 18, 25, 65535], "out_clusters": [0, 3, 4, 5, 18, 25, 65535], "profile_id": 260, }, - "2": { + 2: { "device_type": 24322, "endpoint_id": 2, "in_clusters": [3, 18], "out_clusters": [3, 4, 5, 18], "profile_id": 260, }, - "3": { + 3: { "device_type": 24323, "endpoint_id": 3, "in_clusters": [3, 18], @@ -864,31 +1505,56 @@ DEVICES = [ }, "entities": [ "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input", - "sensor.lumi_lumi_remote_b186acn01_77665544_power", "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_2", "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_3", + "sensor.lumi_lumi_remote_b186acn01_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_remote_b186acn01_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_2", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_3", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.remote.b186acn01", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "RemoteB186ACN01", }, { + "device_no": 43, "endpoints": { - "1": { + 1: { "device_type": 24321, "endpoint_id": 1, "in_clusters": [0, 1, 3, 18, 25, 65535], "out_clusters": [0, 3, 4, 5, 18, 25, 65535], "profile_id": 260, }, - "2": { + 2: { "device_type": 24322, "endpoint_id": 2, "in_clusters": [3, 18], "out_clusters": [3, 4, 5, 18], "profile_id": 260, }, - "3": { + 3: { "device_type": 24323, "endpoint_id": 3, "in_clusters": [3, 18], @@ -898,52 +1564,77 @@ DEVICES = [ }, "entities": [ "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input", - "sensor.lumi_lumi_remote_b286acn01_77665544_power", "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_2", "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_3", + "sensor.lumi_lumi_remote_b286acn01_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_remote_b286acn01_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_3", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_2", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.remote.b286acn01", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "RemoteB286ACN01", }, { + "device_no": 44, "endpoints": { - "1": { + 1: { "device_type": 261, "endpoint_id": 1, "in_clusters": [0, 1, 3], "out_clusters": [3, 6, 8, 768], "profile_id": 260, }, - "2": { + 2: { "device_type": -1, "endpoint_id": 2, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "3": { + 3: { "device_type": -1, "endpoint_id": 3, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "4": { + 4: { "device_type": -1, "endpoint_id": 4, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "5": { + 5: { "device_type": -1, "endpoint_id": 5, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "6": { + 6: { "device_type": -1, "endpoint_id": 6, "in_clusters": [], @@ -951,49 +1642,52 @@ DEVICES = [ "profile_id": -1, }, }, - "entities": ["sensor.lumi_lumi_remote_b286opcn01_77665544_power"], - "event_channels": [6, 8, 768], + "entities": [], + "entity_map": {}, + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300"], "manufacturer": "LUMI", "model": "lumi.remote.b286opcn01", + "node_descriptor": b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", }, { + "device_no": 45, "endpoints": { - "1": { + 1: { "device_type": 261, "endpoint_id": 1, "in_clusters": [0, 1, 3], "out_clusters": [3, 6, 8, 768], "profile_id": 260, }, - "2": { + 2: { "device_type": 259, "endpoint_id": 2, "in_clusters": [3], "out_clusters": [3, 6], "profile_id": 260, }, - "3": { + 3: { "device_type": -1, "endpoint_id": 3, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "4": { + 4: { "device_type": -1, "endpoint_id": 4, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "5": { + 5: { "device_type": -1, "endpoint_id": 5, "in_clusters": [], "out_clusters": [], "profile_id": -1, }, - "6": { + 6: { "device_type": -1, "endpoint_id": 6, "in_clusters": [], @@ -1001,52 +1695,70 @@ DEVICES = [ "profile_id": -1, }, }, - "entities": [ - "sensor.lumi_lumi_remote_b486opcn01_77665544_power", - "switch.lumi_lumi_remote_b486opcn01_77665544_on_off", - ], - "event_channels": [6, 8, 768, 6], + "entities": [], + "entity_map": {}, + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], "manufacturer": "LUMI", "model": "lumi.remote.b486opcn01", + "node_descriptor": b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", }, { + "device_no": 46, "endpoints": { - "1": { + 1: { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + } + }, + "entities": [], + "entity_map": {}, + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300"], + "manufacturer": "LUMI", + "model": "lumi.remote.b686opcn01", + "node_descriptor": b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + }, + { + "device_no": 47, + "endpoints": { + 1: { "device_type": 261, "endpoint_id": 1, "in_clusters": [0, 1, 3], "out_clusters": [3, 6, 8, 768], "profile_id": 260, }, - "2": { + 2: { "device_type": 259, "endpoint_id": 2, "in_clusters": [3], "out_clusters": [3, 6], "profile_id": 260, }, - "3": { + 3: { "device_type": None, "endpoint_id": 3, "in_clusters": [], "out_clusters": [], "profile_id": None, }, - "4": { + 4: { "device_type": None, "endpoint_id": 4, "in_clusters": [], "out_clusters": [], "profile_id": None, }, - "5": { + 5: { "device_type": None, "endpoint_id": 5, "in_clusters": [], "out_clusters": [], "profile_id": None, }, - "6": { + 6: { "device_type": None, "endpoint_id": 6, "in_clusters": [], @@ -1054,17 +1766,41 @@ DEVICES = [ "profile_id": None, }, }, - "entities": [ - "sensor.lumi_lumi_remote_b686opcn01_77665544_power", - "switch.lumi_lumi_remote_b686opcn01_77665544_on_off", - ], - "event_channels": [6, 8, 768, 6], + "entities": [], + "entity_map": {}, + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], "manufacturer": "LUMI", "model": "lumi.remote.b686opcn01", + "node_descriptor": b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", }, { + "device_no": 48, "endpoints": { - "8": { + 8: { + "device_type": 256, + "endpoint_id": 8, + "in_clusters": [0, 6], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": ["light.lumi_lumi_router_77665544_on_off_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-8"): { + "channels": ["on_off", "on_off"], + "entity_class": "Light", + "entity_id": "light.lumi_lumi_router_77665544_on_off_on_off", + } + }, + "event_channels": ["8:0x0006"], + "manufacturer": "LUMI", + "model": "lumi.router", + "node_descriptor": b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", + }, + { + "device_no": 49, + "endpoints": { + 8: { "device_type": 256, "endpoint_id": 8, "in_clusters": [0, 6, 11, 17], @@ -1073,27 +1809,143 @@ DEVICES = [ } }, "entities": ["light.lumi_lumi_router_77665544_on_off_on_off"], - "event_channels": [6], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-8"): { + "channels": ["on_off", "on_off"], + "entity_class": "Light", + "entity_id": "light.lumi_lumi_router_77665544_on_off_on_off", + } + }, + "event_channels": ["8:0x0006"], "manufacturer": "LUMI", "model": "lumi.router", + "node_descriptor": b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", }, { + "device_no": 50, "endpoints": { - "1": { + 8: { + "device_type": 256, + "endpoint_id": 8, + "in_clusters": [0, 6, 17], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": ["light.lumi_lumi_router_77665544_on_off_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-8"): { + "channels": ["on_off", "on_off"], + "entity_class": "Light", + "entity_id": "light.lumi_lumi_router_77665544_on_off_on_off", + } + }, + "event_channels": ["8:0x0006"], + "manufacturer": "LUMI", + "model": "lumi.router", + "node_descriptor": b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", + }, + { + "device_no": 51, + "endpoints": { + 1: { + "device_type": 262, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1024], + "out_clusters": [3], + "profile_id": 260, + } + }, + "entities": ["sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance"], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { + "channels": ["illuminance"], + "entity_class": "Illuminance", + "entity_id": "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + }, + }, + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sen_ill.mgl01", + "node_descriptor": b"\x02@\x84n\x12\x7fd\x00\x00,d\x00\x00", + }, + { + "device_no": 52, + "endpoints": { + 1: { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 18, 25, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + 2: { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + 3: { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 12, 18], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input", + "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input_2", + "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input_3", + "sensor.lumi_lumi_sensor_86sw1_77665544_power", + ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_86sw1_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input_3", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input_2", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input", + }, + }, + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_86sw1", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "RemoteB186ACN01", + }, + { + "device_no": 53, + "endpoints": { + 1: { "device_type": 28417, "endpoint_id": 1, "in_clusters": [0, 1, 3, 25], "out_clusters": [0, 3, 4, 5, 18, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 28418, "endpoint_id": 2, "in_clusters": [3, 18], "out_clusters": [3, 4, 5, 18], "profile_id": 260, }, - "3": { + 3: { "device_type": 28419, "endpoint_id": 3, "in_clusters": [3, 12], @@ -1106,27 +1958,47 @@ DEVICES = [ "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_multistate_input", "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-2-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_multistate_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_analog_input", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.sensor_cube.aqgl01", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "CubeAQGL01", }, { + "device_no": 54, "endpoints": { - "1": { + 1: { "device_type": 24322, "endpoint_id": 1, "in_clusters": [0, 1, 3, 25, 1026, 1029, 65535], "out_clusters": [0, 3, 4, 5, 18, 25, 65535], "profile_id": 260, }, - "2": { + 2: { "device_type": 24322, "endpoint_id": 2, "in_clusters": [3], "out_clusters": [3, 4, 5, 18], "profile_id": 260, }, - "3": { + 3: { "device_type": 24323, "endpoint_id": 3, "in_clusters": [3], @@ -1135,17 +2007,37 @@ DEVICES = [ }, }, "entities": [ + "sensor.lumi_lumi_sensor_ht_77665544_humidity", "sensor.lumi_lumi_sensor_ht_77665544_power", "sensor.lumi_lumi_sensor_ht_77665544_temperature", - "sensor.lumi_lumi_sensor_ht_77665544_humidity", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_ht_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.lumi_lumi_sensor_ht_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { + "channels": ["humidity"], + "entity_class": "Humidity", + "entity_id": "sensor.lumi_lumi_sensor_ht_77665544_humidity", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.sensor_ht", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "Weather", }, { + "device_no": 55, "endpoints": { - "1": { + 1: { "device_type": 2128, "endpoint_id": 1, "in_clusters": [0, 1, 3, 25, 65535], @@ -1154,16 +2046,31 @@ DEVICES = [ } }, "entities": [ - "sensor.lumi_lumi_sensor_magnet_77665544_power", "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + "sensor.lumi_lumi_sensor_magnet_77665544_power", ], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_magnet_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Opening", + "entity_id": "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + }, + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "LUMI", "model": "lumi.sensor_magnet", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "Magnet", }, { + "device_no": 56, "endpoints": { - "1": { + 1: { "device_type": 24321, "endpoint_id": 1, "in_clusters": [0, 1, 3, 65535], @@ -1175,13 +2082,28 @@ DEVICES = [ "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", ], - "event_channels": [6], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Opening", + "entity_id": "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + }, + }, + "event_channels": ["1:0x0006"], "manufacturer": "LUMI", "model": "lumi.sensor_magnet.aq2", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "MagnetAQ2", }, { + "device_no": 57, "endpoints": { - "1": { + 1: { "device_type": 263, "endpoint_id": 1, "in_clusters": [0, 1, 3, 1024, 1030, 1280, 65535], @@ -1190,18 +2112,88 @@ DEVICES = [ } }, "entities": [ - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { + "channels": ["illuminance"], + "entity_class": "Illuminance", + "entity_id": "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { + "channels": ["occupancy"], + "entity_class": "Occupancy", + "entity_id": "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.sensor_motion.aq2", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "MotionAQ2", }, { + "device_no": 58, "endpoints": { - "1": { + 1: { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 12, 18, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + "sensor.lumi_lumi_sensor_smoke_77665544_analog_input", + "sensor.lumi_lumi_sensor_smoke_77665544_multistate_input", + "sensor.lumi_lumi_sensor_smoke_77665544_power", + ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_smoke_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-12"): { + "channels": ["analog_input"], + "entity_class": "AnalogInput", + "entity_id": "sensor.lumi_lumi_sensor_smoke_77665544_analog_input", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_smoke_77665544_multistate_input", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + }, + }, + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_smoke", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "MijiaHoneywellSmokeDetectorSensor", + }, + { + "device_no": 59, + "endpoints": { + 1: { "device_type": 6, "endpoint_id": 1, "in_clusters": [0, 1, 3], @@ -1210,13 +2202,23 @@ DEVICES = [ } }, "entities": ["sensor.lumi_lumi_sensor_switch_77665544_power"], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_switch_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "LUMI", "model": "lumi.sensor_switch", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "MijaButton", }, { + "device_no": 60, "endpoints": { - "1": { + 1: { "device_type": 6, "endpoint_id": 1, "in_clusters": [0, 1, 65535], @@ -1225,13 +2227,23 @@ DEVICES = [ } }, "entities": ["sensor.lumi_lumi_sensor_switch_aq2_77665544_power"], - "event_channels": [6], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + } + }, + "event_channels": ["1:0x0006"], "manufacturer": "LUMI", "model": "lumi.sensor_switch.aq2", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "SwitchAQ2", }, { + "device_no": 61, "endpoints": { - "1": { + 1: { "device_type": 6, "endpoint_id": 1, "in_clusters": [0, 1, 18], @@ -1243,13 +2255,28 @@ DEVICES = [ "sensor.lumi_lumi_sensor_switch_aq3_77665544_multistate_input", "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", ], - "event_channels": [6], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-18"): { + "channels": ["multistate_input"], + "entity_class": "Text", + "entity_id": "sensor.lumi_lumi_sensor_switch_aq3_77665544_multistate_input", + }, + }, + "event_channels": ["1:0x0006"], "manufacturer": "LUMI", "model": "lumi.sensor_switch.aq3", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "SwitchAQ3", }, { + "device_no": 62, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 1280], @@ -1261,20 +2288,35 @@ DEVICES = [ "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.sensor_wleak.aq1", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "LeakAQ1", }, { + "device_no": 63, "endpoints": { - "1": { + 1: { "device_type": 10, "endpoint_id": 1, "in_clusters": [0, 1, 3, 25, 257, 1280], "out_clusters": [0, 3, 4, 5, 25], "profile_id": 260, }, - "2": { + 2: { "device_type": 24322, "endpoint_id": 2, "in_clusters": [3], @@ -1284,16 +2326,36 @@ DEVICES = [ }, "entities": [ "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", - "sensor.lumi_lumi_vibration_aq1_77665544_power", "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + "sensor.lumi_lumi_vibration_aq1_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_vibration_aq1_77665544_power", + }, + ("lock", "00:11:22:33:44:55:66:77-1-257"): { + "channels": ["door_lock"], + "entity_class": "ZhaDoorLock", + "entity_id": "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.vibration.aq1", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "VibrationAQ1", }, { + "device_no": 64, "endpoints": { - "1": { + 1: { "device_type": 24321, "endpoint_id": 1, "in_clusters": [0, 1, 3, 1026, 1027, 1029, 65535], @@ -1302,18 +2364,43 @@ DEVICES = [ } }, "entities": [ - "sensor.lumi_lumi_weather_77665544_temperature", - "sensor.lumi_lumi_weather_77665544_power", "sensor.lumi_lumi_weather_77665544_humidity", + "sensor.lumi_lumi_weather_77665544_power", "sensor.lumi_lumi_weather_77665544_pressure", + "sensor.lumi_lumi_weather_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.lumi_lumi_weather_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.lumi_lumi_weather_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { + "channels": ["pressure"], + "entity_class": "Pressure", + "entity_id": "sensor.lumi_lumi_weather_77665544_pressure", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { + "channels": ["humidity"], + "entity_class": "Humidity", + "entity_id": "sensor.lumi_lumi_weather_77665544_humidity", + }, + }, "event_channels": [], "manufacturer": "LUMI", "model": "lumi.weather", + "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", + "zha_quirks": "Weather", }, { + "device_no": 65, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1280], @@ -1325,13 +2412,27 @@ DEVICES = [ "binary_sensor.nyce_3010_77665544_ias_zone", "sensor.nyce_3010_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.nyce_3010_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.nyce_3010_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "NYCE", "model": "3010", + "node_descriptor": b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", }, { + "device_no": 66, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1280], @@ -1343,13 +2444,70 @@ DEVICES = [ "binary_sensor.nyce_3014_77665544_ias_zone", "sensor.nyce_3014_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.nyce_3014_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.nyce_3014_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "NYCE", "model": "3014", + "node_descriptor": b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", }, { + "device_no": 67, "endpoints": { - "3": { + 1: { + "device_type": 5, + "endpoint_id": 1, + "in_clusters": [10, 25], + "out_clusters": [1280], + "profile_id": 260, + }, + 242: { + "device_type": 100, + "endpoint_id": 242, + "in_clusters": [], + "out_clusters": [33], + "profile_id": 41440, + }, + }, + "entities": [], + "entity_map": {}, + "event_channels": [], + "manufacturer": None, + "model": None, + "node_descriptor": b"\x10@\x0f5\x11Y=\x00@\x00=\x00\x00", + }, + { + "device_no": 68, + "endpoints": { + 1: { + "device_type": 48879, + "endpoint_id": 1, + "in_clusters": [], + "out_clusters": [1280], + "profile_id": 260, + } + }, + "entities": [], + "entity_map": {}, + "event_channels": [], + "manufacturer": None, + "model": None, + "node_descriptor": b"\x00@\x8f\xcd\xabR\x80\x00\x00\x00\x80\x00\x00", + }, + { + "device_no": 69, + "endpoints": { + 3: { "device_type": 258, "endpoint_id": 3, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], @@ -1358,13 +2516,23 @@ DEVICES = [ } }, "entities": ["light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off"], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-3"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "OSRAM", "model": "LIGHTIFY A19 RGBW", + "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", + "zha_quirks": "LIGHTIFYA19RGBW", }, { + "device_no": 70, "endpoints": { - "1": { + 1: { "device_type": 1, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 2821], @@ -1372,17 +2540,24 @@ DEVICES = [ "profile_id": 260, } }, - "entities": [ - "binary_sensor.osram_lightify_dimming_switch_77665544_on_off", - "sensor.osram_lightify_dimming_switch_77665544_power", - ], - "event_channels": [6, 8], + "entities": ["sensor.osram_lightify_dimming_switch_77665544_power"], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.osram_lightify_dimming_switch_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "OSRAM", "model": "LIGHTIFY Dimming Switch", + "node_descriptor": b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", + "zha_quirks": "CentraLite3130", }, { + "device_no": 71, "endpoints": { - "3": { + 3: { "device_type": 258, "endpoint_id": 3, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], @@ -1393,13 +2568,23 @@ DEVICES = [ "entities": [ "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off" ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-3"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", + } + }, "event_channels": [], "manufacturer": "OSRAM", "model": "LIGHTIFY Flex RGBW", + "node_descriptor": b"\x19@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", + "zha_quirks": "FlexRGBW", }, { + "device_no": 72, "endpoints": { - "3": { + 3: { "device_type": 258, "endpoint_id": 3, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2820, 64527], @@ -1408,16 +2593,31 @@ DEVICES = [ } }, "entities": [ - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-3"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "OSRAM", "model": "LIGHTIFY RT Tunable White", + "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", + "zha_quirks": "A19TunableWhite", }, { + "device_no": 73, "endpoints": { - "3": { + 3: { "device_type": 16, "endpoint_id": 3, "in_clusters": [0, 3, 4, 5, 6, 2820, 4096, 64527], @@ -1429,48 +2629,62 @@ DEVICES = [ "sensor.osram_plug_01_77665544_electrical_measurement", "switch.osram_plug_01_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-3"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.osram_plug_01_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.osram_plug_01_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "OSRAM", "model": "Plug 01", + "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", }, { + "device_no": 74, "endpoints": { - "1": { + 1: { "device_type": 2064, "endpoint_id": 1, "in_clusters": [0, 1, 32, 4096, 64768], "out_clusters": [3, 4, 5, 6, 8, 25, 768, 4096], "profile_id": 260, }, - "2": { + 2: { "device_type": 2064, "endpoint_id": 2, "in_clusters": [0, 4096, 64768], "out_clusters": [3, 4, 5, 6, 8, 768, 4096], "profile_id": 260, }, - "3": { + 3: { "device_type": 2064, "endpoint_id": 3, "in_clusters": [0, 4096, 64768], "out_clusters": [3, 4, 5, 6, 8, 768, 4096], "profile_id": 260, }, - "4": { + 4: { "device_type": 2064, "endpoint_id": 4, "in_clusters": [0, 4096, 64768], "out_clusters": [3, 4, 5, 6, 8, 768, 4096], "profile_id": 260, }, - "5": { + 5: { "device_type": 2064, "endpoint_id": 5, "in_clusters": [0, 4096, 64768], "out_clusters": [3, 4, 5, 6, 8, 768, 4096], "profile_id": 260, }, - "6": { + 6: { "device_type": 2064, "endpoint_id": 6, "in_clusters": [0, 4096, 64768], @@ -1479,39 +2693,49 @@ DEVICES = [ }, }, "entities": ["sensor.osram_switch_4x_lightify_77665544_power"], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.osram_switch_4x_lightify_77665544_power", + } + }, "event_channels": [ - 6, - 8, - 768, - 6, - 8, - 768, - 6, - 8, - 768, - 6, - 8, - 768, - 6, - 8, - 768, - 6, - 8, - 768, + "1:0x0006", + "1:0x0008", + "1:0x0300", + "2:0x0006", + "2:0x0008", + "2:0x0300", + "3:0x0006", + "3:0x0008", + "3:0x0300", + "4:0x0006", + "4:0x0008", + "4:0x0300", + "5:0x0006", + "5:0x0008", + "5:0x0300", + "6:0x0006", + "6:0x0008", + "6:0x0300", ], "manufacturer": "OSRAM", "model": "Switch 4x-LIGHTIFY", + "node_descriptor": b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", + "zha_quirks": "LightifyX4", }, { + "device_no": 75, "endpoints": { - "1": { + 1: { "device_type": 2096, "endpoint_id": 1, "in_clusters": [0], "out_clusters": [0, 3, 4, 5, 6, 8], "profile_id": 49246, }, - "2": { + 2: { "device_type": 12, "endpoint_id": 2, "in_clusters": [0, 1, 3, 15, 64512], @@ -1520,13 +2744,23 @@ DEVICES = [ }, }, "entities": ["sensor.philips_rwl020_77665544_power"], - "event_channels": [6, 8], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-2-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.philips_rwl020_77665544_power", + } + }, + "event_channels": ["1:0x0006", "1:0x0008"], "manufacturer": "Philips", "model": "RWL020", + "node_descriptor": b"\x02@\x80\x0b\x10G-\x00\x00\x00-\x00\x00", + "zha_quirks": "PhilipsRWL021", }, { + "device_no": 76, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], @@ -1536,16 +2770,36 @@ DEVICES = [ }, "entities": [ "binary_sensor.samjin_button_77665544_ias_zone", - "sensor.samjin_button_77665544_temperature", "sensor.samjin_button_77665544_power", + "sensor.samjin_button_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.samjin_button_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.samjin_button_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.samjin_button_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Samjin", "model": "button", + "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", + "zha_quirks": "SamjinButton", }, { + "device_no": 77, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 64514], @@ -1554,18 +2808,44 @@ DEVICES = [ } }, "entities": [ - "sensor.samjin_multi_77665544_power", - "sensor.samjin_multi_77665544_temperature", "binary_sensor.samjin_multi_77665544_ias_zone", "binary_sensor.samjin_multi_77665544_manufacturer_specific", + "sensor.samjin_multi_77665544_power", + "sensor.samjin_multi_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.samjin_multi_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.samjin_multi_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.samjin_multi_77665544_ias_zone", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): { + "channels": ["manufacturer_specific"], + "entity_class": "BinarySensor", + "entity_id": "binary_sensor.samjin_multi_77665544_manufacturer_specific", + "default_match": True, + }, + }, "event_channels": [], "manufacturer": "Samjin", "model": "multi", + "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", + "zha_quirks": "SmartthingsMultiPurposeSensor", }, { + "device_no": 78, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280], @@ -1578,13 +2858,32 @@ DEVICES = [ "sensor.samjin_water_77665544_power", "sensor.samjin_water_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.samjin_water_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.samjin_water_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.samjin_water_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Samjin", "model": "water", + "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", }, { + "device_no": 79, "endpoints": { - "1": { + 1: { "device_type": 0, "endpoint_id": 1, "in_clusters": [0, 1, 3, 4, 5, 6, 2820, 2821], @@ -1593,18 +2892,30 @@ DEVICES = [ } }, "entities": [ - "binary_sensor.securifi_ltd_unk_model_77665544_on_off", "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", - "sensor.securifi_ltd_unk_model_77665544_power", "switch.securifi_ltd_unk_model_77665544_on_off", ], - "event_channels": [6], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.securifi_ltd_unk_model_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + }, + }, + "event_channels": ["1:0x0006"], "manufacturer": "Securifi Ltd.", "model": None, + "node_descriptor": b"\x01@\x8e\x02\x10RR\x00\x00\x00R\x00\x00", }, { + "device_no": 80, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], @@ -1617,20 +2928,39 @@ DEVICES = [ "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Sercomm Corp.", "model": "SZ-DWS04N_SF", + "node_descriptor": b"\x02@\x801\x11R\xff\x00\x00\x00\xff\x00\x00", }, { + "device_no": 81, "endpoints": { - "1": { + 1: { "device_type": 256, "endpoint_id": 1, "in_clusters": [0, 1, 3, 4, 5, 6, 1794, 2820, 2821], "out_clusters": [3, 10, 25, 2821], "profile_id": 260, }, - "2": { + 2: { "device_type": 259, "endpoint_id": 2, "in_clusters": [0, 1, 3], @@ -1639,20 +2969,36 @@ DEVICES = [ }, }, "entities": [ - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", - "sensor.sercomm_corp_sz_esw01_77665544_power", - "sensor.sercomm_corp_sz_esw01_77665544_power_2", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", - "switch.sercomm_corp_sz_esw01_77665544_on_off", "light.sercomm_corp_sz_esw01_77665544_on_off", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", ], - "event_channels": [6], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.sercomm_corp_sz_esw01_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + }, + }, + "event_channels": ["2:0x0006"], "manufacturer": "Sercomm Corp.", "model": "SZ-ESW01", + "node_descriptor": b"\x01@\x8e1\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 82, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280, 2821], @@ -1662,17 +3008,41 @@ DEVICES = [ }, "entities": [ "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - "sensor.sercomm_corp_sz_pir04_77665544_temperature", "sensor.sercomm_corp_sz_pir04_77665544_illuminance", "sensor.sercomm_corp_sz_pir04_77665544_power", + "sensor.sercomm_corp_sz_pir04_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.sercomm_corp_sz_pir04_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { + "channels": ["illuminance"], + "entity_class": "Illuminance", + "entity_id": "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.sercomm_corp_sz_pir04_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Sercomm Corp.", "model": "SZ-PIR04", + "node_descriptor": b"\x02@\x801\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 83, "endpoints": { - "1": { + 1: { "device_type": 2, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 2820, 2821, 65281], @@ -1684,20 +3054,34 @@ DEVICES = [ "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", "switch.sinope_technologies_rm3250zb_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.sinope_technologies_rm3250zb_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "Sinope Technologies", "model": "RM3250ZB", + "node_descriptor": b"\x11@\x8e\x9c\x11G+\x00\x00*+\x00\x00", }, { + "device_no": 84, "endpoints": { - "1": { + 1: { "device_type": 769, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], "out_clusters": [25, 65281], "profile_id": 260, }, - "196": { + 196: { "device_type": 769, "endpoint_id": 196, "in_clusters": [1], @@ -1706,17 +3090,71 @@ DEVICES = [ }, }, "entities": [ - "sensor.sinope_technologies_th1124zb_77665544_temperature", - "sensor.sinope_technologies_th1124zb_77665544_power", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1123zb_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.sinope_technologies_th1123zb_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + }, + }, + "event_channels": [], + "manufacturer": "Sinope Technologies", + "model": "TH1123ZB", + "node_descriptor": b"\x12@\x8c\x9c\x11G+\x00\x00\x00+\x00\x00", + "zha_quirks": "SinopeTechnologiesThermostat", + }, + { + "device_no": 85, + "endpoints": { + 1: { + "device_type": 769, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], + "out_clusters": [25, 65281], + "profile_id": 260, + }, + 196: { + "device_type": 769, + "endpoint_id": 196, + "in_clusters": [1], + "out_clusters": [], + "profile_id": 49757, + }, + }, + "entities": [ + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + "sensor.sinope_technologies_th1124zb_77665544_temperature", + ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.sinope_technologies_th1124zb_77665544_temperature", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "Sinope Technologies", "model": "TH1124ZB", + "node_descriptor": b"\x11@\x8e\x9c\x11G+\x00\x00\x00+\x00\x00", + "zha_quirks": "SinopeTechnologiesThermostat", }, { + "device_no": 86, "endpoints": { - "1": { + 1: { "device_type": 2, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 9, 15, 2820], @@ -1728,13 +3166,27 @@ DEVICES = [ "sensor.smartthings_outletv4_77665544_electrical_measurement", "switch.smartthings_outletv4_77665544_on_off", ], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.smartthings_outletv4_77665544_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + "channels": ["electrical_measurement"], + "entity_class": "ElectricalMeasurement", + "entity_id": "sensor.smartthings_outletv4_77665544_electrical_measurement", + }, + }, "event_channels": [], "manufacturer": "SmartThings", "model": "outletv4", + "node_descriptor": b"\x01@\x8e\n\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 87, "endpoints": { - "1": { + 1: { "device_type": 32768, "endpoint_id": 1, "in_clusters": [0, 1, 3, 15, 32], @@ -1743,13 +3195,23 @@ DEVICES = [ } }, "entities": ["device_tracker.smartthings_tagv4_77665544_power"], + "entity_map": { + ("device_tracker", "00:11:22:33:44:55:66:77-1"): { + "channels": ["power"], + "entity_class": "ZHADeviceScannerEntity", + "entity_id": "device_tracker.smartthings_tagv4_77665544_power", + } + }, "event_channels": [], "manufacturer": "SmartThings", "model": "tagv4", + "node_descriptor": b"\x02@\x80\n\x11RR\x00\x00\x00R\x00\x00", + "zha_quirks": "SmartThingsTagV4", }, { + "device_no": 88, "endpoints": { - "1": { + 1: { "device_type": 2, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 25], @@ -1758,13 +3220,22 @@ DEVICES = [ } }, "entities": ["switch.third_reality_inc_3rss007z_77665544_on_off"], + "entity_map": { + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.third_reality_inc_3rss007z_77665544_on_off", + } + }, "event_channels": [], "manufacturer": "Third Reality, Inc", "model": "3RSS007Z", + "node_descriptor": b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", }, { + "device_no": 89, "endpoints": { - "1": { + 1: { "device_type": 2, "endpoint_id": 1, "in_clusters": [0, 1, 3, 4, 5, 6, 25], @@ -1776,13 +3247,28 @@ DEVICES = [ "sensor.third_reality_inc_3rss008z_77665544_power", "switch.third_reality_inc_3rss008z_77665544_on_off", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.third_reality_inc_3rss008z_77665544_power", + }, + ("switch", "00:11:22:33:44:55:66:77-1-6"): { + "channels": ["on_off"], + "entity_class": "Switch", + "entity_id": "switch.third_reality_inc_3rss008z_77665544_on_off", + }, + }, "event_channels": [], "manufacturer": "Third Reality, Inc", "model": "3RSS008Z", + "node_descriptor": b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", + "zha_quirks": "Switch", }, { + "device_no": 90, "endpoints": { - "1": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], @@ -1792,16 +3278,133 @@ DEVICES = [ }, "entities": [ "binary_sensor.visonic_mct_340_e_77665544_ias_zone", - "sensor.visonic_mct_340_e_77665544_temperature", "sensor.visonic_mct_340_e_77665544_power", + "sensor.visonic_mct_340_e_77665544_temperature", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.visonic_mct_340_e_77665544_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { + "channels": ["temperature"], + "entity_class": "Temperature", + "entity_id": "sensor.visonic_mct_340_e_77665544_temperature", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "Visonic", "model": "MCT-340 E", + "node_descriptor": b"\x02@\x80\x11\x10RR\x00\x00\x00R\x00\x00", + "zha_quirks": "MCT340E", }, { + "device_no": 91, "endpoints": { - "1": { + 1: { + "device_type": 769, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 32, 513, 514, 516, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + } + }, + "entities": [ + "fan.zen_within_zen_01_77665544_fan", + "sensor.zen_within_zen_01_77665544_power", + ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.zen_within_zen_01_77665544_power", + }, + ("fan", "00:11:22:33:44:55:66:77-1-514"): { + "channels": ["fan"], + "entity_class": "ZhaFan", + "entity_id": "fan.zen_within_zen_01_77665544_fan", + }, + }, + "event_channels": [], + "manufacturer": "Zen Within", + "model": "Zen-01", + "node_descriptor": b"\x02@\x80X\x11R\x80\x00\x00\x00\x80\x00\x00", + }, + { + "device_no": 92, + "endpoints": { + 1: { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 4, 5, 6, 10], + "out_clusters": [25], + "profile_id": 260, + }, + 2: { + "device_type": 256, + "endpoint_id": 2, + "in_clusters": [4, 5, 6], + "out_clusters": [], + "profile_id": 260, + }, + 3: { + "device_type": 256, + "endpoint_id": 3, + "in_clusters": [4, 5, 6], + "out_clusters": [], + "profile_id": 260, + }, + 4: { + "device_type": 256, + "endpoint_id": 4, + "in_clusters": [4, 5, 6], + "out_clusters": [], + "profile_id": 260, + }, + }, + "entities": [ + "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + }, + ("light", "00:11:22:33:44:55:66:77-2"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + }, + ("light", "00:11:22:33:44:55:66:77-3"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + }, + ("light", "00:11:22:33:44:55:66:77-4"): { + "channels": ["on_off"], + "entity_class": "Light", + "entity_id": "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + }, + }, + "event_channels": [], + "manufacturer": "_TYZB01_ns1ndbww", + "model": "TS0004", + "node_descriptor": b"\x01@\x8e\x02\x10R\x00\x02\x00,\x00\x02\x00", + }, + { + "device_no": 93, + "endpoints": { + 1: { "device_type": 1026, "endpoint_id": 1, "in_clusters": [0, 1, 3, 21, 32, 1280, 2821], @@ -1813,13 +3416,28 @@ DEVICES = [ "binary_sensor.netvox_z308e3ed_77665544_ias_zone", "sensor.netvox_z308e3ed_77665544_power", ], + "entity_map": { + ("sensor", "00:11:22:33:44:55:66:77-1-1"): { + "channels": ["power"], + "entity_class": "Battery", + "entity_id": "sensor.netvox_z308e3ed_77665544_power", + }, + ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { + "channels": ["ias_zone"], + "entity_class": "IASZone", + "entity_id": "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + }, + }, "event_channels": [], "manufacturer": "netvox", "model": "Z308E3ED", + "node_descriptor": b"\x02@\x80\x9f\x10RR\x00\x00\x00R\x00\x00", + "zha_quirks": "Z308E3ED", }, { + "device_no": 94, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], @@ -1831,13 +3449,27 @@ DEVICES = [ "light.sengled_e11_g13_77665544_level_on_off", "sensor.sengled_e11_g13_77665544_smartenergy_metering", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.sengled_e11_g13_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.sengled_e11_g13_77665544_smartenergy_metering", + }, + }, "event_channels": [], "manufacturer": "sengled", "model": "E11-G13", + "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 95, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], @@ -1846,16 +3478,30 @@ DEVICES = [ } }, "entities": [ - "sensor.sengled_e12_n14_77665544_smartenergy_metering", "light.sengled_e12_n14_77665544_level_on_off", + "sensor.sengled_e12_n14_77665544_smartenergy_metering", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "on_off"], + "entity_class": "Light", + "entity_id": "light.sengled_e12_n14_77665544_level_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.sengled_e12_n14_77665544_smartenergy_metering", + }, + }, "event_channels": [], "manufacturer": "sengled", "model": "E12-N14", + "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, { + "device_no": 96, "endpoints": { - "1": { + 1: { "device_type": 257, "endpoint_id": 1, "in_clusters": [0, 3, 4, 5, 6, 8, 768, 1794, 2821], @@ -1864,11 +3510,24 @@ DEVICES = [ } }, "entities": [ - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", ], + "entity_map": { + ("light", "00:11:22:33:44:55:66:77-1"): { + "channels": ["level", "light_color", "on_off"], + "entity_class": "Light", + "entity_id": "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + "channels": ["smartenergy_metering"], + "entity_class": "SmartEnergyMetering", + "entity_id": "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + }, + }, "event_channels": [], "manufacturer": "sengled", "model": "Z01-A19NAE26", + "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", }, ] diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py deleted file mode 100644 index 5f57e8b4064..00000000000 --- a/tests/components/zone/test_config_flow.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Tests for zone config flow.""" - -from homeassistant.components.zone import config_flow -from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE -from homeassistant.const import ( - CONF_ICON, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - CONF_RADIUS, -) - -from tests.common import MockConfigEntry - - -async def test_flow_works(hass): - """Test that config flow works.""" - flow = config_flow.ZoneFlowHandler() - flow.hass = hass - - result = await flow.async_step_init( - user_input={ - CONF_NAME: "Name", - CONF_LATITUDE: "1.1", - CONF_LONGITUDE: "2.2", - CONF_RADIUS: "100", - CONF_ICON: "mdi:home", - CONF_PASSIVE: True, - } - ) - - assert result["type"] == "create_entry" - assert result["title"] == "Name" - assert result["data"] == { - CONF_NAME: "Name", - CONF_LATITUDE: "1.1", - CONF_LONGITUDE: "2.2", - CONF_RADIUS: "100", - CONF_ICON: "mdi:home", - CONF_PASSIVE: True, - } - - -async def test_flow_requires_unique_name(hass): - """Test that config flow verifies that each zones name is unique.""" - MockConfigEntry(domain=DOMAIN, data={CONF_NAME: "Name"}).add_to_hass(hass) - flow = config_flow.ZoneFlowHandler() - flow.hass = hass - - result = await flow.async_step_init(user_input={CONF_NAME: "Name"}) - assert result["errors"] == {"base": "name_exists"} - - -async def test_flow_requires_name_different_from_home(hass): - """Test that config flow verifies that each zones name is unique.""" - flow = config_flow.ZoneFlowHandler() - flow.hass = hass - - result = await flow.async_step_init(user_input={CONF_NAME: HOME_ZONE}) - assert result["errors"] == {"base": "name_exists"} diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index d4a76463c18..0835b77579a 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,229 +1,224 @@ """Test zone component.""" - -import unittest -from unittest.mock import Mock +from asynctest import patch +import pytest from homeassistant import setup from homeassistant.components import zone +from homeassistant.components.zone import DOMAIN +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_NAME, + SERVICE_RELOAD, +) +from homeassistant.core import Context +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers import entity_registry -from tests.common import MockConfigEntry, get_test_home_assistant +from tests.common import MockConfigEntry -async def test_setup_entry_successful(hass): - """Test setup entry is successful.""" - entry = Mock() - entry.data = { - zone.CONF_NAME: "Test Zone", - zone.CONF_LATITUDE: 1.1, - zone.CONF_LONGITUDE: -2.2, - zone.CONF_RADIUS: True, +@pytest.fixture +def storage_setup(hass, hass_storage): + """Storage setup.""" + + async def _storage(items=None, config=None): + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": { + "items": [ + { + "id": "from_storage", + "name": "from storage", + "latitude": 1, + "longitude": 2, + "radius": 3, + "passive": False, + "icon": "mdi:from-storage", + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "data": {"items": items}, + } + if config is None: + config = {} + return await setup.async_setup_component(hass, DOMAIN, config) + + return _storage + + +async def test_setup_no_zones_still_adds_home_zone(hass): + """Test if no config is passed in we still get the home zone.""" + assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": None}) + assert len(hass.states.async_entity_ids("zone")) == 1 + state = hass.states.get("zone.home") + assert hass.config.location_name == state.name + assert hass.config.latitude == state.attributes["latitude"] + assert hass.config.longitude == state.attributes["longitude"] + assert not state.attributes.get("passive", False) + + +async def test_setup(hass): + """Test a successful setup.""" + info = { + "name": "Test Zone", + "latitude": 32.880837, + "longitude": -117.237561, + "radius": 250, + "passive": True, } - hass.data[zone.DOMAIN] = {} - assert await zone.async_setup_entry(hass, entry) is True - assert "test_zone" in hass.data[zone.DOMAIN] + assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) + + assert len(hass.states.async_entity_ids("zone")) == 2 + state = hass.states.get("zone.test_zone") + assert info["name"] == state.name + assert info["latitude"] == state.attributes["latitude"] + assert info["longitude"] == state.attributes["longitude"] + assert info["radius"] == state.attributes["radius"] + assert info["passive"] == state.attributes["passive"] -async def test_unload_entry_successful(hass): - """Test unload entry is successful.""" - entry = Mock() - entry.data = { - zone.CONF_NAME: "Test Zone", - zone.CONF_LATITUDE: 1.1, - zone.CONF_LONGITUDE: -2.2, - } - hass.data[zone.DOMAIN] = {} - assert await zone.async_setup_entry(hass, entry) is True - assert await zone.async_unload_entry(hass, entry) is True - assert not hass.data[zone.DOMAIN] +async def test_setup_zone_skips_home_zone(hass): + """Test that zone named Home should override hass home zone.""" + info = {"name": "Home", "latitude": 1.1, "longitude": -2.2} + assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) + + assert len(hass.states.async_entity_ids("zone")) == 1 + state = hass.states.get("zone.home") + assert info["name"] == state.name -class TestComponentZone(unittest.TestCase): - """Test the zone component.""" +async def test_setup_name_can_be_same_on_multiple_zones(hass): + """Test that zone named Home should override hass home zone.""" + info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2} + assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": [info, info]}) + assert len(hass.states.async_entity_ids("zone")) == 3 - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() +async def test_active_zone_skips_passive_zones(hass): + """Test active and passive zones.""" + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Passive Zone", + "latitude": 32.880600, + "longitude": -117.237561, + "radius": 250, + "passive": True, + } + ] + }, + ) + await hass.async_block_till_done() + active = zone.async_active_zone(hass, 32.880600, -117.237561) + assert active is None - def test_setup_no_zones_still_adds_home_zone(self): - """Test if no config is passed in we still get the home zone.""" - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": None}) - assert len(self.hass.states.entity_ids("zone")) == 1 - state = self.hass.states.get("zone.home") - assert self.hass.config.location_name == state.name - assert self.hass.config.latitude == state.attributes["latitude"] - assert self.hass.config.longitude == state.attributes["longitude"] - assert not state.attributes.get("passive", False) - def test_setup(self): - """Test a successful setup.""" - info = { - "name": "Test Zone", - "latitude": 32.880837, - "longitude": -117.237561, - "radius": 250, - "passive": True, - } - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info}) +async def test_active_zone_skips_passive_zones_2(hass): + """Test active and passive zones.""" + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Active Zone", + "latitude": 32.880800, + "longitude": -117.237561, + "radius": 500, + } + ] + }, + ) + await hass.async_block_till_done() + active = zone.async_active_zone(hass, 32.880700, -117.237561) + assert "zone.active_zone" == active.entity_id - assert len(self.hass.states.entity_ids("zone")) == 2 - state = self.hass.states.get("zone.test_zone") - assert info["name"] == state.name - assert info["latitude"] == state.attributes["latitude"] - assert info["longitude"] == state.attributes["longitude"] - assert info["radius"] == state.attributes["radius"] - assert info["passive"] == state.attributes["passive"] - def test_setup_zone_skips_home_zone(self): - """Test that zone named Home should override hass home zone.""" - info = {"name": "Home", "latitude": 1.1, "longitude": -2.2} - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info}) +async def test_active_zone_prefers_smaller_zone_if_same_distance(hass): + """Test zone size preferences.""" + latitude = 32.880600 + longitude = -117.237561 + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Small Zone", + "latitude": latitude, + "longitude": longitude, + "radius": 250, + }, + { + "name": "Big Zone", + "latitude": latitude, + "longitude": longitude, + "radius": 500, + }, + ] + }, + ) - assert len(self.hass.states.entity_ids("zone")) == 1 - state = self.hass.states.get("zone.home") - assert info["name"] == state.name + active = zone.async_active_zone(hass, latitude, longitude) + assert "zone.small_zone" == active.entity_id - def test_setup_name_can_be_same_on_multiple_zones(self): - """Test that zone named Home should override hass home zone.""" - info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2} - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": [info, info]}) - assert len(self.hass.states.entity_ids("zone")) == 3 - def test_setup_registered_zone_skips_home_zone(self): - """Test that config entry named home should override hass home zone.""" - entry = MockConfigEntry(domain=zone.DOMAIN, data={zone.CONF_NAME: "home"}) - entry.add_to_hass(self.hass) - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": None}) - assert len(self.hass.states.entity_ids("zone")) == 0 +async def test_active_zone_prefers_smaller_zone_if_same_distance_2(hass): + """Test zone size preferences.""" + latitude = 32.880600 + longitude = -117.237561 + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Smallest Zone", + "latitude": latitude, + "longitude": longitude, + "radius": 50, + } + ] + }, + ) - def test_setup_registered_zone_skips_configured_zone(self): - """Test if config entry will override configured zone.""" - entry = MockConfigEntry(domain=zone.DOMAIN, data={zone.CONF_NAME: "Test Zone"}) - entry.add_to_hass(self.hass) - info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2} - assert setup.setup_component(self.hass, zone.DOMAIN, {"zone": info}) + active = zone.async_active_zone(hass, latitude, longitude) + assert "zone.smallest_zone" == active.entity_id - assert len(self.hass.states.entity_ids("zone")) == 1 - state = self.hass.states.get("zone.test_zone") - assert not state - def test_active_zone_skips_passive_zones(self): - """Test active and passive zones.""" - assert setup.setup_component( - self.hass, - zone.DOMAIN, - { - "zone": [ - { - "name": "Passive Zone", - "latitude": 32.880600, - "longitude": -117.237561, - "radius": 250, - "passive": True, - } - ] - }, - ) - self.hass.block_till_done() - active = zone.async_active_zone(self.hass, 32.880600, -117.237561) - assert active is None +async def test_in_zone_works_for_passive_zones(hass): + """Test working in passive zones.""" + latitude = 32.880600 + longitude = -117.237561 + assert await setup.async_setup_component( + hass, + zone.DOMAIN, + { + "zone": [ + { + "name": "Passive Zone", + "latitude": latitude, + "longitude": longitude, + "radius": 250, + "passive": True, + } + ] + }, + ) - def test_active_zone_skips_passive_zones_2(self): - """Test active and passive zones.""" - assert setup.setup_component( - self.hass, - zone.DOMAIN, - { - "zone": [ - { - "name": "Active Zone", - "latitude": 32.880800, - "longitude": -117.237561, - "radius": 500, - } - ] - }, - ) - self.hass.block_till_done() - active = zone.async_active_zone(self.hass, 32.880700, -117.237561) - assert "zone.active_zone" == active.entity_id - - def test_active_zone_prefers_smaller_zone_if_same_distance(self): - """Test zone size preferences.""" - latitude = 32.880600 - longitude = -117.237561 - assert setup.setup_component( - self.hass, - zone.DOMAIN, - { - "zone": [ - { - "name": "Small Zone", - "latitude": latitude, - "longitude": longitude, - "radius": 250, - }, - { - "name": "Big Zone", - "latitude": latitude, - "longitude": longitude, - "radius": 500, - }, - ] - }, - ) - - active = zone.async_active_zone(self.hass, latitude, longitude) - assert "zone.small_zone" == active.entity_id - - def test_active_zone_prefers_smaller_zone_if_same_distance_2(self): - """Test zone size preferences.""" - latitude = 32.880600 - longitude = -117.237561 - assert setup.setup_component( - self.hass, - zone.DOMAIN, - { - "zone": [ - { - "name": "Smallest Zone", - "latitude": latitude, - "longitude": longitude, - "radius": 50, - } - ] - }, - ) - - active = zone.async_active_zone(self.hass, latitude, longitude) - assert "zone.smallest_zone" == active.entity_id - - def test_in_zone_works_for_passive_zones(self): - """Test working in passive zones.""" - latitude = 32.880600 - longitude = -117.237561 - assert setup.setup_component( - self.hass, - zone.DOMAIN, - { - "zone": [ - { - "name": "Passive Zone", - "latitude": latitude, - "longitude": longitude, - "radius": 250, - "passive": True, - } - ] - }, - ) - - assert zone.zone.in_zone( - self.hass.states.get("zone.passive_zone"), latitude, longitude - ) + assert zone.in_zone(hass.states.get("zone.passive_zone"), latitude, longitude) async def test_core_config_update(hass): @@ -243,3 +238,252 @@ async def test_core_config_update(hass): assert home_updated.name == "Updated Name" assert home_updated.attributes["latitude"] == 10 assert home_updated.attributes["longitude"] == 20 + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + ent_reg = await entity_registry.async_get_registry(hass) + + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: [ + {"name": "yaml 1", "latitude": 1, "longitude": 2}, + {"name": "yaml 2", "latitude": 3, "longitude": 4}, + ], + }, + ) + + assert count_start + 3 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("zone.yaml_1") + state_2 = hass.states.get("zone.yaml_2") + state_3 = hass.states.get("zone.yaml_3") + + assert state_1 is not None + assert state_1.attributes["latitude"] == 1 + assert state_1.attributes["longitude"] == 2 + assert state_2 is not None + assert state_2.attributes["latitude"] == 3 + assert state_2.attributes["longitude"] == 4 + assert state_3 is None + assert len(ent_reg.entities) == 0 + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: [ + {"name": "yaml 2", "latitude": 3, "longitude": 4}, + {"name": "yaml 3", "latitude": 5, "longitude": 6}, + ] + }, + ): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 3 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("zone.yaml_1") + state_2 = hass.states.get("zone.yaml_2") + state_3 = hass.states.get("zone.yaml_3") + + assert state_1 is None + assert state_2 is not None + assert state_2.attributes["latitude"] == 3 + assert state_2.attributes["longitude"] == 4 + assert state_3 is not None + assert state_3.attributes["latitude"] == 5 + assert state_3.attributes["longitude"] == 6 + + +async def test_load_from_storage(hass, storage_setup): + """Test set up from storage.""" + assert await storage_setup() + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "zoning" + assert state.name == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + +async def test_editable_state_attribute(hass, storage_setup): + """Test editable attribute.""" + assert await storage_setup( + config={DOMAIN: [{"name": "yaml option", "latitude": 3, "longitude": 4}]} + ) + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state.state == "zoning" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "from storage" + assert state.attributes.get(ATTR_EDITABLE) + + state = hass.states.get(f"{DOMAIN}.yaml_option") + assert state.state == "zoning" + assert not state.attributes.get(ATTR_EDITABLE) + + +async def test_ws_list(hass, hass_ws_client, storage_setup): + """Test listing via WS.""" + assert await storage_setup( + config={DOMAIN: [{"name": "yaml option", "latitude": 3, "longitude": 4}]} + ) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 6, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + storage_ent = "from_storage" + yaml_ent = "from_yaml" + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert storage_ent in result + assert yaml_ent not in result + assert result[storage_ent][ATTR_NAME] == "from storage" + + +async def test_ws_delete(hass, hass_ws_client, storage_setup): + """Test WS delete cleans up entity registry.""" + assert await storage_setup() + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + {"id": 6, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": f"{input_id}"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + +async def test_update(hass, hass_ws_client, storage_setup): + """Test updating min/max updates the state.""" + + items = [ + { + "id": "from_storage", + "name": "from storage", + "latitude": 1, + "longitude": 2, + "radius": 3, + "passive": False, + } + ] + assert await storage_setup(items) + + input_id = "from_storage" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state.attributes["latitude"] == 1 + assert state.attributes["longitude"] == 2 + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": f"{input_id}", + "latitude": 3, + "longitude": 4, + "passive": True, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.attributes["latitude"] == 3 + assert state.attributes["longitude"] == 4 + assert state.attributes["passive"] is True + + +async def test_ws_create(hass, hass_ws_client, storage_setup): + """Test create WS.""" + assert await storage_setup(items=[]) + + input_id = "new_input" + input_entity_id = f"{DOMAIN}.{input_id}" + ent_reg = await entity_registry.async_get_registry(hass) + + state = hass.states.get(input_entity_id) + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, input_id) is None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 6, + "type": f"{DOMAIN}/create", + "name": "New Input", + "latitude": 3, + "longitude": 4, + "passive": True, + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get(input_entity_id) + assert state.state == "zoning" + assert state.attributes["latitude"] == 3 + assert state.attributes["longitude"] == 4 + assert state.attributes["passive"] is True + + +async def test_import_config_entry(hass): + """Test we import config entry and then delete it.""" + entry = MockConfigEntry( + domain="zone", + data={ + "name": "from config entry", + "latitude": 1, + "longitude": 2, + "radius": 3, + "passive": False, + "icon": "mdi:from-config-entry", + }, + ) + entry.add_to_hass(hass) + assert await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries()) == 0 + + state = hass.states.get("zone.from_config_entry") + assert state is not None + assert state.attributes[zone.ATTR_LATITUDE] == 1 + assert state.attributes[zone.ATTR_LONGITUDE] == 2 + assert state.attributes[zone.ATTR_RADIUS] == 3 + assert state.attributes[zone.ATTR_PASSIVE] is False + assert state.attributes[ATTR_ICON] == "mdi:from-config-entry" diff --git a/tests/fixtures/aurora.txt b/tests/fixtures/aurora.txt index 92bebf795fc..22e8d0c2476 100644 --- a/tests/fixtures/aurora.txt +++ b/tests/fixtures/aurora.txt @@ -1,114 +1,149 @@ +#Aurora Specification Tabular Values +# Product: Ovation Aurora Short Term Forecast +# Product Valid At: 2019-11-08 18:55 +# Product Generated At: 2019-11-08 18:25 +# +# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center. +# Please send comments and suggestions to SWPC.Webmaster@noaa.gov +# +# Missing Data: (n/a) +# Cadence: 5 minutes +# +# Tabular Data is on the following grid +# +# 1024 values covering 0 to 360 degrees in the horizontal (longitude) direction (0.32846715 degrees/value) +# 512 values covering -90 to 90 degrees in the vertical (latitude) direction (0.3515625 degrees/value) +# Values range from 0 (little or no probability of visible aurora) to 100 (high probability of visible aurora) +# + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 + 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 + 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 + 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 + 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 + 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 + 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 + 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 + 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 + 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 + 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 + 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 + 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 + 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 + 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 + 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 + 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 + 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 + 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 + 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 + 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 + 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 + 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 + 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 + 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 + 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 + 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 + 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 + 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 + 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 + 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 + 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 + 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 + 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 + 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 + 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 + 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 + 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 + 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 + 7 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 + 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 + 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 5 5 + 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 10 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 + 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 13 12 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 + 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 16 16 16 16 16 15 15 15 14 14 14 13 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 7 7 7 7 6 6 6 5 5 5 4 4 4 4 3 3 3 3 3 2 2 2 2 2 + 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 18 18 18 18 18 18 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 7 7 7 6 6 6 6 5 5 5 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 + 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 19 19 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 16 16 16 16 15 15 15 14 14 14 14 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 2 2 1 1 1 1 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 16 15 15 15 14 14 14 14 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 13 13 13 14 14 14 15 15 15 16 16 16 16 17 17 17 18 18 18 19 19 19 20 20 20 20 21 21 21 21 21 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 17 17 17 16 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 11 11 11 12 12 12 13 13 13 14 14 14 15 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 18 18 18 18 18 17 17 16 16 16 15 15 15 14 14 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 13 14 14 15 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 22 23 23 23 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 23 23 23 23 23 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 15 15 15 15 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 19 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 16 16 15 15 15 14 14 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 17 17 17 17 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 23 23 23 22 22 22 22 21 21 21 21 20 20 20 20 20 19 19 19 19 19 18 18 18 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 16 16 16 17 17 17 18 18 18 18 18 19 19 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 15 15 14 14 13 13 12 12 11 11 11 10 10 9 9 9 8 8 7 7 7 6 6 6 5 5 5 4 4 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 16 17 17 17 17 18 18 18 19 19 19 20 20 21 21 21 22 22 23 23 23 23 24 24 24 25 25 25 25 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 15 15 15 15 15 16 16 16 17 17 17 18 18 18 19 19 19 19 19 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 22 22 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 14 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 4 4 4 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 13 14 14 14 15 15 15 15 16 16 16 17 17 17 18 18 19 19 19 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 27 27 27 27 27 27 27 27 27 27 26 26 26 25 25 25 24 24 24 24 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 17 17 16 16 16 16 16 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 15 16 16 16 16 16 16 17 17 17 18 18 18 19 19 19 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 19 19 19 19 18 18 18 17 17 17 17 16 16 16 16 15 15 15 14 14 13 13 12 12 11 11 10 9 9 9 8 8 7 7 6 6 6 5 5 4 4 3 3 3 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 13 13 14 14 14 15 15 15 16 16 17 17 18 18 19 19 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 28 28 28 28 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 24 24 23 23 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 18 18 18 18 19 19 19 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 21 21 21 21 21 21 21 21 21 21 21 20 20 19 19 19 18 18 17 17 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 7 6 6 6 5 5 4 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 + 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 21 21 22 22 23 23 23 24 24 24 25 25 26 26 26 26 26 26 27 27 27 27 27 28 28 28 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 25 25 25 25 25 25 24 24 24 24 23 23 23 23 22 22 22 21 21 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 20 20 20 20 19 19 18 18 17 17 16 16 15 15 14 14 14 14 13 13 13 13 13 13 12 12 12 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 + 4 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 19 19 20 20 20 21 21 22 22 22 23 23 24 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 28 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 24 24 24 24 24 23 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 22 22 22 22 22 22 22 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 17 17 16 16 15 15 14 14 13 13 12 12 12 12 11 11 11 11 11 10 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 4 + 3 3 3 3 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 10 10 11 11 12 12 12 13 13 14 14 15 15 16 16 17 17 18 18 19 19 20 20 20 21 21 22 22 22 23 23 23 24 24 24 25 25 25 25 26 26 26 27 27 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 25 25 24 24 24 23 23 23 23 22 22 22 22 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 17 17 17 16 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 22 22 22 22 22 22 22 21 21 21 21 20 20 20 20 19 19 19 18 18 18 17 16 16 15 15 14 14 13 13 12 12 11 11 11 10 10 10 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 + 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 6 6 6 6 7 7 7 8 8 9 9 9 10 10 11 11 11 12 12 13 13 14 14 15 15 16 16 17 17 18 18 18 19 19 20 20 20 21 21 21 22 22 22 23 23 23 24 24 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 25 25 25 25 24 24 24 24 23 23 23 23 22 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 20 20 20 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 21 21 21 21 20 20 20 19 19 19 19 18 18 18 17 17 16 16 15 15 14 13 13 12 12 11 11 10 10 9 9 8 8 7 7 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 + 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 10 10 10 11 11 12 12 13 13 14 14 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 21 21 21 22 22 23 23 23 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 25 25 25 25 24 24 24 24 23 23 23 22 22 22 21 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 16 15 15 15 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 13 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 17 17 17 16 16 16 15 15 14 14 13 13 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 + 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 9 9 9 10 10 11 11 12 12 13 13 14 14 14 15 15 16 16 17 17 17 18 18 18 19 19 19 20 20 21 21 22 22 22 23 23 24 24 25 25 25 25 25 26 26 26 26 27 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 27 26 26 26 26 26 25 25 25 25 24 24 24 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 18 18 18 17 17 17 17 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 16 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 21 22 22 22 22 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 22 22 21 21 21 20 20 19 19 18 18 18 17 17 17 16 16 16 15 15 15 14 14 13 13 12 11 11 10 10 9 9 8 8 7 7 7 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 7 7 8 8 8 9 9 10 10 11 11 11 12 12 13 13 14 14 15 15 15 16 16 16 17 17 18 18 18 19 19 20 20 21 21 22 22 23 23 24 24 24 24 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 27 27 27 27 27 26 26 26 25 25 25 24 24 24 23 23 22 22 22 21 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 21 22 22 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 22 22 21 21 20 20 19 19 18 18 17 17 16 16 15 15 15 14 14 14 13 13 13 12 11 11 10 10 9 9 8 8 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 15 16 16 16 17 17 18 18 19 19 20 20 21 21 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 26 26 26 26 27 27 27 27 27 28 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 28 27 27 27 27 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 19 19 19 19 19 18 18 18 18 18 18 17 17 17 17 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 16 17 17 17 17 17 17 17 17 18 18 18 18 18 18 19 19 19 19 20 20 20 20 20 21 21 21 21 22 22 22 22 22 23 23 23 23 23 24 24 24 24 24 24 25 25 25 25 24 24 24 24 24 24 23 23 23 23 23 23 23 22 22 22 22 22 21 21 21 20 20 19 19 18 17 17 16 16 15 15 14 14 14 13 13 12 12 12 11 11 11 10 10 9 9 8 8 8 7 7 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 10 10 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 17 17 18 18 19 19 20 20 21 21 21 21 21 21 22 22 22 22 22 22 23 23 24 24 24 25 25 26 26 26 27 27 27 27 27 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 28 28 28 28 28 28 27 27 26 26 26 25 25 25 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 20 20 20 20 20 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 24 24 24 24 24 24 25 25 24 24 24 24 24 23 23 23 23 22 22 22 22 21 21 21 21 21 20 20 20 20 20 19 18 18 17 16 16 15 15 14 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 10 10 10 11 11 12 12 13 13 13 14 14 15 15 16 16 17 17 18 19 19 19 19 19 19 20 20 20 20 20 20 20 21 21 22 22 23 24 24 25 25 26 26 26 27 27 27 27 27 27 28 28 28 28 28 28 28 28 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 29 28 28 28 27 27 27 26 26 26 25 25 25 24 24 24 23 23 23 23 22 22 22 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 22 22 22 22 22 22 22 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 23 23 23 22 22 22 21 21 21 20 20 20 20 19 19 19 19 19 18 18 17 17 16 15 15 14 13 13 12 12 11 11 10 10 9 9 9 8 8 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 13 13 14 14 15 15 16 16 17 17 17 17 17 18 18 18 18 18 18 18 18 19 20 20 21 21 22 22 23 23 24 24 25 25 25 25 25 26 26 26 26 26 26 27 27 27 27 27 27 27 27 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 28 27 27 27 27 26 26 26 25 25 25 25 24 24 24 24 24 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 23 23 23 23 23 23 22 22 22 21 21 20 20 20 19 19 19 18 18 18 18 18 17 17 17 17 16 16 16 15 14 14 13 13 12 12 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 10 11 11 12 12 13 13 14 14 14 15 15 15 16 16 16 16 16 16 16 16 16 16 17 18 18 19 19 20 20 21 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 25 25 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 28 28 27 27 27 27 26 26 26 26 25 25 25 25 25 25 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 22 22 22 21 21 21 20 20 19 19 18 18 17 17 17 16 16 16 16 16 15 15 15 15 15 14 14 13 13 12 12 11 11 11 10 10 9 9 8 8 8 7 7 6 6 6 5 5 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 10 10 11 11 11 12 12 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 16 16 17 18 18 19 19 20 20 21 21 21 22 22 22 22 22 22 23 23 23 23 23 24 24 24 24 24 25 25 25 25 26 26 26 26 26 26 26 26 27 27 27 27 27 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 19 19 18 18 18 17 17 16 16 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 11 11 10 10 10 9 9 9 8 8 7 7 7 6 6 5 5 5 4 4 3 3 3 2 2 2 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 15 15 16 16 17 17 18 18 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 22 22 22 22 23 23 23 23 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 20 20 20 20 20 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 17 17 17 16 16 15 15 15 14 14 13 13 13 13 12 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 10 10 10 11 11 11 12 12 12 13 13 13 14 14 15 15 16 16 16 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 20 20 20 20 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 23 23 23 23 23 23 23 23 23 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 20 20 20 20 19 19 19 19 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 13 13 13 14 14 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 19 19 19 19 18 18 18 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 14 14 14 14 14 14 14 14 14 13 13 13 13 12 12 12 11 11 11 11 10 10 10 9 9 8 8 8 7 7 7 7 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 10 11 11 11 12 12 12 13 13 13 13 14 14 14 14 14 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 17 17 17 16 16 16 15 15 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 11 12 12 12 12 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 8 8 8 8 7 7 7 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 2 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 6 6 6 7 7 7 8 8 8 8 9 9 9 10 10 10 10 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 16 16 16 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 7 6 6 6 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 8 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 14 14 14 14 13 13 13 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 4 4 4 5 5 5 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 3 3 3 3 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 3 4 4 4 4 3 3 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 4 4 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 3 3 4 4 5 5 5 4 4 4 4 4 4 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 2 2 2 3 3 4 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 - 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 - 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 - 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 - 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 - 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 - 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 - 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 - 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 - 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 - 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 - 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 - 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 - 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 - 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 - 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 - 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 - 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 - 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 - 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 7 8 8 8 8 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 - 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 10 10 11 11 11 11 11 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 9 9 9 9 10 10 10 10 11 11 11 12 12 12 12 13 13 13 13 13 13 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 4 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 11 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 5 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 10 10 10 11 11 11 12 12 12 13 13 13 14 14 14 14 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 12 13 13 13 13 14 14 14 14 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 11 11 11 12 12 12 13 13 13 14 14 14 14 14 15 15 15 15 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 13 13 13 13 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 5 4 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 4 4 4 4 5 5 5 6 6 7 7 7 8 8 8 9 9 10 10 10 11 11 12 12 12 13 13 13 14 14 14 14 15 15 15 16 16 16 16 16 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 7 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 8 8 9 9 10 10 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 9 9 9 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 3 3 3 4 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 14 14 15 15 15 16 16 16 16 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 14 13 13 13 13 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 14 14 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 16 16 16 16 16 15 15 15 15 14 14 14 13 13 13 13 12 12 12 12 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 15 15 14 14 14 13 13 13 12 12 12 11 11 11 10 10 10 10 10 10 9 9 9 9 9 8 8 8 8 7 7 7 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 11 11 11 11 10 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 7 7 6 6 6 5 5 5 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 11 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 15 15 15 16 16 17 17 17 17 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 17 17 17 16 16 16 16 15 15 15 14 14 14 13 13 12 12 12 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 9 9 9 8 8 8 8 8 8 7 7 6 6 6 5 5 4 4 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 3 3 4 4 5 5 5 6 6 7 7 7 7 8 8 8 8 9 9 9 10 10 10 10 10 11 11 11 11 12 12 12 13 13 14 14 14 15 15 16 16 16 17 17 17 17 17 18 18 18 18 18 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 17 17 17 17 16 16 16 16 15 15 14 14 14 13 13 13 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 10 10 10 9 9 9 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 5 5 5 4 4 3 3 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 7 7 7 8 8 9 9 10 10 10 11 11 12 12 13 13 14 14 15 15 16 16 16 16 17 17 17 17 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 17 17 17 17 17 16 16 16 15 15 15 14 14 14 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 9 9 9 8 8 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 4 3 3 3 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 6 6 7 7 8 8 9 9 9 10 10 11 11 12 12 13 13 14 14 15 15 15 15 15 16 16 16 16 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 16 16 16 15 15 15 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 10 10 10 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 8 8 7 7 6 6 6 5 5 5 4 4 4 4 4 4 4 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 2 3 3 3 3 3 4 4 4 5 5 6 6 6 7 7 8 8 8 9 9 10 10 11 11 11 12 12 13 13 13 13 13 13 14 14 14 14 14 14 15 15 15 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 15 15 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 7 7 6 6 5 5 5 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 5 5 5 6 6 6 7 7 8 8 8 9 9 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 13 13 14 14 15 15 15 16 16 17 17 17 17 17 17 17 17 17 18 18 18 17 17 17 16 16 16 15 15 14 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 5 5 5 4 4 3 3 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 3 3 3 4 4 4 4 5 5 5 6 6 7 7 7 8 8 8 9 9 9 9 9 9 10 10 10 10 10 10 10 11 11 12 12 13 13 14 14 15 15 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 16 16 16 15 15 15 14 14 14 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 6 6 6 6 6 5 5 5 4 4 4 3 3 3 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 5 5 6 6 6 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 10 10 11 11 12 12 13 13 14 14 15 15 15 15 16 16 16 16 16 17 17 17 17 17 16 16 16 15 15 15 15 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 8 8 8 7 7 7 6 6 5 5 5 5 5 5 5 5 4 4 4 4 4 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 8 8 9 9 10 11 11 12 12 13 13 14 14 14 15 15 15 15 16 16 16 16 17 16 16 16 16 16 15 15 15 15 15 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 11 11 11 11 11 10 10 10 10 9 9 9 9 8 8 8 7 7 6 6 6 5 5 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 3 3 3 3 3 4 4 4 4 4 4 5 5 5 5 5 6 6 7 7 8 8 9 9 10 11 11 12 12 12 12 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 13 13 13 13 13 13 12 12 12 12 12 12 12 11 11 11 11 11 11 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 3 3 3 3 4 4 4 4 5 5 6 7 7 8 8 9 9 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 13 13 13 13 13 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 11 11 11 11 11 10 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 3 3 4 4 5 5 6 6 7 7 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 11 11 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 11 11 11 10 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 5 5 5 4 4 4 4 4 3 3 3 3 3 3 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 3 3 4 4 4 5 5 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 9 9 9 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 10 10 10 9 9 9 9 8 8 8 8 7 7 7 7 6 6 6 5 5 5 4 4 4 3 3 3 3 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 3 3 3 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 7 7 7 8 8 8 9 9 9 10 10 10 9 9 9 9 9 9 9 9 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 9 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 2 2 2 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 6 6 6 7 7 7 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 9 9 9 9 9 10 10 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 9 9 9 8 8 8 7 7 7 7 6 6 6 5 5 5 5 4 4 4 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 5 5 5 5 6 6 6 6 7 7 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 6 6 6 6 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 4 4 4 4 3 3 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 5 5 5 5 5 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -381,6 +416,109 @@ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 2 2 2 2 2 2 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 2 2 2 2 1 1 1 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 2 2 2 2 2 2 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 3 3 3 3 3 3 3 3 3 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 1 1 1 1 0 0 0 1 1 1 1 2 2 2 2 3 3 3 2 2 2 2 2 1 1 1 1 0 0 1 1 1 1 2 2 2 2 3 3 3 3 3 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 3 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2 3 3 3 3 2 2 2 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 + 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + 3 3 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 + 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 + 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 + 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 + 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 + 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 + 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 + 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 + 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 + 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 + 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 + 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 + 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 + 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 + 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 + 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 + 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 + 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 + 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 + 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 + 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 + 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 + 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 + 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 + 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 + 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 + 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 + 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 + 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 27 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 + 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 26 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 + 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 25 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 + 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 + 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 23 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 + 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 21 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 + 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 + 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 18 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 + 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 17 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 + 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 16 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 + 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 + 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 14 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 + 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 + 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 + 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 + 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 + 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 @@ -389,124 +527,3 @@ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 - 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 - 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 - 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 - 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 - 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 - 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 - 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 - 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 - 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 12 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 - 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 - 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 - 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 - 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 - 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 \ No newline at end of file diff --git a/tests/fixtures/nws-weather-fore-null.json b/tests/fixtures/nws-weather-fore-null.json deleted file mode 100644 index 6085bcdada9..00000000000 --- a/tests/fixtures/nws-weather-fore-null.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "@context": [ - "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", - { - "wx": "https://api.weather.gov/ontology#", - "geo": "http://www.opengis.net/ont/geosparql#", - "unit": "http://codes.wmo.int/common/unit/", - "@vocab": "https://api.weather.gov/ontology#" - } - ], - "type": "Feature", - "geometry": { - "type": "GeometryCollection", - "geometries": [ - { - "type": "Point", - "coordinates": [ - -85.014692800000006, - 39.993574700000003 - ] - }, - { - "type": "Polygon", - "coordinates": [ - [ - [ - -85.027968599999994, - 40.005368300000001 - ], - [ - -85.0300814, - 39.983399599999998 - ], - [ - -85.001420100000004, - 39.981779299999999 - ], - [ - -84.999301200000005, - 40.0037479 - ], - [ - -85.027968599999994, - 40.005368300000001 - ] - ] - ] - } - ] - }, - "properties": { - "updated": "2019-08-12T23:17:40+00:00", - "units": "us", - "forecastGenerator": "BaselineForecastGenerator", - "generatedAt": "2019-08-13T00:33:19+00:00", - "updateTime": "2019-08-12T23:17:40+00:00", - "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", - "elevation": { - "value": 366.06479999999999, - "unitCode": "unit:m" - }, - "periods": [ - { - "number": null, - "name": null, - "startTime": null, - "endTime": null, - "isDaytime": null, - "temperature": null, - "temperatureUnit": null, - "temperatureTrend": null, - "windSpeed": null, - "windDirection": null, - "icon": null, - "shortForecast": null, - "detailedForecast": null - } - ] - } -} diff --git a/tests/fixtures/nws-weather-fore-valid.json b/tests/fixtures/nws-weather-fore-valid.json deleted file mode 100644 index b3f4f4ccea8..00000000000 --- a/tests/fixtures/nws-weather-fore-valid.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "@context": [ - "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", - { - "wx": "https://api.weather.gov/ontology#", - "geo": "http://www.opengis.net/ont/geosparql#", - "unit": "http://codes.wmo.int/common/unit/", - "@vocab": "https://api.weather.gov/ontology#" - } - ], - "type": "Feature", - "geometry": { - "type": "GeometryCollection", - "geometries": [ - { - "type": "Point", - "coordinates": [ - -85.014692800000006, - 39.993574700000003 - ] - }, - { - "type": "Polygon", - "coordinates": [ - [ - [ - -85.027968599999994, - 40.005368300000001 - ], - [ - -85.0300814, - 39.983399599999998 - ], - [ - -85.001420100000004, - 39.981779299999999 - ], - [ - -84.999301200000005, - 40.0037479 - ], - [ - -85.027968599999994, - 40.005368300000001 - ] - ] - ] - } - ] - }, - "properties": { - "updated": "2019-08-12T23:17:40+00:00", - "units": "us", - "forecastGenerator": "BaselineForecastGenerator", - "generatedAt": "2019-08-13T00:33:19+00:00", - "updateTime": "2019-08-12T23:17:40+00:00", - "validTimes": "2019-08-12T17:00:00+00:00/P8DT6H", - "elevation": { - "value": 366.06479999999999, - "unitCode": "unit:m" - }, - "periods": [ - { - "number": 1, - "name": "Tonight", - "startTime": "2019-08-12T20:00:00-04:00", - "endTime": "2019-08-13T06:00:00-04:00", - "isDaytime": false, - "temperature": 70, - "temperatureUnit": "F", - "temperatureTrend": null, - "windSpeed": "7 to 13 mph", - "windDirection": "S", - "icon": "https://api.weather.gov/icons/land/night/tsra,40/tsra,90?size=medium", - "shortForecast": "Showers And Thunderstorms", - "detailedForecast": "A detailed forecast." - } - ] - } -} diff --git a/tests/fixtures/nws-weather-obs-null.json b/tests/fixtures/nws-weather-obs-null.json deleted file mode 100644 index 36ae66283e5..00000000000 --- a/tests/fixtures/nws-weather-obs-null.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "@context": [ - "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", - { - "wx": "https://api.weather.gov/ontology#", - "s": "https://schema.org/", - "geo": "http://www.opengis.net/ont/geosparql#", - "unit": "http://codes.wmo.int/common/unit/", - "@vocab": "https://api.weather.gov/ontology#", - "geometry": { - "@id": "s:GeoCoordinates", - "@type": "geo:wktLiteral" - }, - "city": "s:addressLocality", - "state": "s:addressRegion", - "distance": { - "@id": "s:Distance", - "@type": "s:QuantitativeValue" - }, - "bearing": { - "@type": "s:QuantitativeValue" - }, - "value": { - "@id": "s:value" - }, - "unitCode": { - "@id": "s:unitCode", - "@type": "@id" - }, - "forecastOffice": { - "@type": "@id" - }, - "forecastGridData": { - "@type": "@id" - }, - "publicZone": { - "@type": "@id" - }, - "county": { - "@type": "@id" - } - } - ], - "type": "FeatureCollection", - "features": [ - { - "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.400000000000006, - 40.25 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", - "@type": "wx:ObservationStation", - "elevation": { - "value": 286, - "unitCode": "unit:m" - }, - "station": "https://api.weather.gov/stations/KMIE", - "timestamp": "2019-08-12T23:53:00+00:00", - "rawMessage": null, - "textDescription": "Clear", - "icon": null, - "presentWeather": [], - "temperature": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "dewpoint": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "windDirection": { - "value": null, - "unitCode": "unit:degree_(angle)", - "qualityControl": "qc:V" - }, - "windSpeed": { - "value": null, - "unitCode": "unit:m_s-1", - "qualityControl": "qc:V" - }, - "windGust": { - "value": null, - "unitCode": "unit:m_s-1", - "qualityControl": "qc:Z" - }, - "barometricPressure": { - "value": null, - "unitCode": "unit:Pa", - "qualityControl": "qc:V" - }, - "seaLevelPressure": { - "value": null, - "unitCode": "unit:Pa", - "qualityControl": "qc:V" - }, - "visibility": { - "value": null, - "unitCode": "unit:m", - "qualityControl": "qc:C" - }, - "maxTemperatureLast24Hours": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": null - }, - "minTemperatureLast24Hours": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": null - }, - "precipitationLastHour": { - "value": null, - "unitCode": "unit:m", - "qualityControl": "qc:Z" - }, - "precipitationLast3Hours": { - "value": null, - "unitCode": "unit:m", - "qualityControl": "qc:Z" - }, - "precipitationLast6Hours": { - "value": 0, - "unitCode": "unit:m", - "qualityControl": "qc:C" - }, - "relativeHumidity": { - "value": null, - "unitCode": "unit:percent", - "qualityControl": "qc:C" - }, - "windChill": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "heatIndex": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "cloudLayers": [ - { - "base": { - "value": null, - "unitCode": "unit:m" - }, - "amount": "CLR" - } - ] - } - } - ] -} diff --git a/tests/fixtures/nws-weather-obs-valid.json b/tests/fixtures/nws-weather-obs-valid.json deleted file mode 100644 index a6d307fc9b1..00000000000 --- a/tests/fixtures/nws-weather-obs-valid.json +++ /dev/null @@ -1,161 +0,0 @@ -{ - "@context": [ - "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", - { - "wx": "https://api.weather.gov/ontology#", - "s": "https://schema.org/", - "geo": "http://www.opengis.net/ont/geosparql#", - "unit": "http://codes.wmo.int/common/unit/", - "@vocab": "https://api.weather.gov/ontology#", - "geometry": { - "@id": "s:GeoCoordinates", - "@type": "geo:wktLiteral" - }, - "city": "s:addressLocality", - "state": "s:addressRegion", - "distance": { - "@id": "s:Distance", - "@type": "s:QuantitativeValue" - }, - "bearing": { - "@type": "s:QuantitativeValue" - }, - "value": { - "@id": "s:value" - }, - "unitCode": { - "@id": "s:unitCode", - "@type": "@id" - }, - "forecastOffice": { - "@type": "@id" - }, - "forecastGridData": { - "@type": "@id" - }, - "publicZone": { - "@type": "@id" - }, - "county": { - "@type": "@id" - } - } - ], - "type": "FeatureCollection", - "features": [ - { - "id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.400000000000006, - 40.25 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMIE/observations/2019-08-12T23:53:00+00:00", - "@type": "wx:ObservationStation", - "elevation": { - "value": 286, - "unitCode": "unit:m" - }, - "station": "https://api.weather.gov/stations/KMIE", - "timestamp": "2019-08-12T23:53:00+00:00", - "rawMessage": "KMIE 122353Z 19005KT 10SM CLR 27/19 A2987 RMK AO2 SLP104 60000 T02670194 10272 20250 58002", - "textDescription": "Clear", - "icon": "https://api.weather.gov/icons/land/day/skc?size=medium", - "presentWeather": [], - "temperature": { - "value": 26.700000000000045, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "dewpoint": { - "value": 19.400000000000034, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "windDirection": { - "value": 190, - "unitCode": "unit:degree_(angle)", - "qualityControl": "qc:V" - }, - "windSpeed": { - "value": 2.6000000000000001, - "unitCode": "unit:m_s-1", - "qualityControl": "qc:V" - }, - "windGust": { - "value": null, - "unitCode": "unit:m_s-1", - "qualityControl": "qc:Z" - }, - "barometricPressure": { - "value": 101150, - "unitCode": "unit:Pa", - "qualityControl": "qc:V" - }, - "seaLevelPressure": { - "value": 101040, - "unitCode": "unit:Pa", - "qualityControl": "qc:V" - }, - "visibility": { - "value": 16090, - "unitCode": "unit:m", - "qualityControl": "qc:C" - }, - "maxTemperatureLast24Hours": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": null - }, - "minTemperatureLast24Hours": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": null - }, - "precipitationLastHour": { - "value": null, - "unitCode": "unit:m", - "qualityControl": "qc:Z" - }, - "precipitationLast3Hours": { - "value": null, - "unitCode": "unit:m", - "qualityControl": "qc:Z" - }, - "precipitationLast6Hours": { - "value": 0, - "unitCode": "unit:m", - "qualityControl": "qc:C" - }, - "relativeHumidity": { - "value": 64.292485914891955, - "unitCode": "unit:percent", - "qualityControl": "qc:C" - }, - "windChill": { - "value": null, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "heatIndex": { - "value": 27.981288713580284, - "unitCode": "unit:degC", - "qualityControl": "qc:V" - }, - "cloudLayers": [ - { - "base": { - "value": null, - "unitCode": "unit:m" - }, - "amount": "CLR" - } - ] - } - } - ] -} \ No newline at end of file diff --git a/tests/fixtures/nws-weather-sta-valid.json b/tests/fixtures/nws-weather-sta-valid.json deleted file mode 100644 index b4fe086366c..00000000000 --- a/tests/fixtures/nws-weather-sta-valid.json +++ /dev/null @@ -1,996 +0,0 @@ -{ - "@context": [ - "https://raw.githubusercontent.com/geojson/geojson-ld/master/contexts/geojson-base.jsonld", - { - "wx": "https://api.weather.gov/ontology#", - "s": "https://schema.org/", - "geo": "http://www.opengis.net/ont/geosparql#", - "unit": "http://codes.wmo.int/common/unit/", - "@vocab": "https://api.weather.gov/ontology#", - "geometry": { - "@id": "s:GeoCoordinates", - "@type": "geo:wktLiteral" - }, - "city": "s:addressLocality", - "state": "s:addressRegion", - "distance": { - "@id": "s:Distance", - "@type": "s:QuantitativeValue" - }, - "bearing": { - "@type": "s:QuantitativeValue" - }, - "value": { - "@id": "s:value" - }, - "unitCode": { - "@id": "s:unitCode", - "@type": "@id" - }, - "forecastOffice": { - "@type": "@id" - }, - "forecastGridData": { - "@type": "@id" - }, - "publicZone": { - "@type": "@id" - }, - "county": { - "@type": "@id" - }, - "observationStations": { - "@container": "@list", - "@type": "@id" - } - } - ], - "type": "FeatureCollection", - "features": [ - { - "id": "https://api.weather.gov/stations/KMIE", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.393609999999995, - 40.234169999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMIE", - "@type": "wx:ObservationStation", - "elevation": { - "value": 284.988, - "unitCode": "unit:m" - }, - "stationIdentifier": "KMIE", - "name": "Muncie, Delaware County-Johnson Field", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KVES", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.531899899999999, - 40.2044 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KVES", - "@type": "wx:ObservationStation", - "elevation": { - "value": 306.93360000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KVES", - "name": "Versailles Darke County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KAID", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.609769999999997, - 40.106119999999997 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KAID", - "@type": "wx:ObservationStation", - "elevation": { - "value": 276.14879999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KAID", - "name": "Anderson Municipal Airport", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KDAY", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.218609999999998, - 39.906109999999998 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KDAY", - "@type": "wx:ObservationStation", - "elevation": { - "value": 306.93360000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KDAY", - "name": "Dayton, Cox Dayton International Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KGEZ", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.799819999999997, - 39.585459999999998 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KGEZ", - "@type": "wx:ObservationStation", - "elevation": { - "value": 244.1448, - "unitCode": "unit:m" - }, - "stationIdentifier": "KGEZ", - "name": "Shelbyville Municipal Airport", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KMGY", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.224720000000005, - 39.588889999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMGY", - "@type": "wx:ObservationStation", - "elevation": { - "value": 291.9984, - "unitCode": "unit:m" - }, - "stationIdentifier": "KMGY", - "name": "Dayton, Dayton-Wright Brothers Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KHAO", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.520610000000005, - 39.36121 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KHAO", - "@type": "wx:ObservationStation", - "elevation": { - "value": 185.0136, - "unitCode": "unit:m" - }, - "stationIdentifier": "KHAO", - "name": "Butler County Regional Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KFFO", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.049999999999997, - 39.833329900000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KFFO", - "@type": "wx:ObservationStation", - "elevation": { - "value": 250.85040000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KFFO", - "name": "Dayton / Wright-Patterson Air Force Base", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KCVG", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.672290000000004, - 39.044559999999997 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KCVG", - "@type": "wx:ObservationStation", - "elevation": { - "value": 262.12799999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KCVG", - "name": "Cincinnati/Northern Kentucky International Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KEDJ", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.819199999999995, - 40.372300000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KEDJ", - "@type": "wx:ObservationStation", - "elevation": { - "value": 341.98560000000003, - "unitCode": "unit:m" - }, - "stationIdentifier": "KEDJ", - "name": "Bellefontaine Regional Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KFWA", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.206370000000007, - 40.97251 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KFWA", - "@type": "wx:ObservationStation", - "elevation": { - "value": 242.9256, - "unitCode": "unit:m" - }, - "stationIdentifier": "KFWA", - "name": "Fort Wayne International Airport", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KBAK", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.900000000000006, - 39.266669999999998 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KBAK", - "@type": "wx:ObservationStation", - "elevation": { - "value": 199.94880000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KBAK", - "name": "Columbus / Bakalar", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KEYE", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -86.295829999999995, - 39.825000000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KEYE", - "@type": "wx:ObservationStation", - "elevation": { - "value": 249.93600000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KEYE", - "name": "Indianapolis, Eagle Creek Airpark", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KLUK", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.41583, - 39.105829999999997 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KLUK", - "@type": "wx:ObservationStation", - "elevation": { - "value": 146.9136, - "unitCode": "unit:m" - }, - "stationIdentifier": "KLUK", - "name": "Cincinnati, Cincinnati Municipal Airport Lunken Field", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KIND", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -86.281599999999997, - 39.725180000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KIND", - "@type": "wx:ObservationStation", - "elevation": { - "value": 240.792, - "unitCode": "unit:m" - }, - "stationIdentifier": "KIND", - "name": "Indianapolis International Airport", - "timeZone": "America/Indiana/Indianapolis" - } - }, - { - "id": "https://api.weather.gov/stations/KAOH", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.021389999999997, - 40.708060000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KAOH", - "@type": "wx:ObservationStation", - "elevation": { - "value": 296.87520000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KAOH", - "name": "Lima, Lima Allen County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KI69", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.2102, - 39.078400000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KI69", - "@type": "wx:ObservationStation", - "elevation": { - "value": 256.94640000000004, - "unitCode": "unit:m" - }, - "stationIdentifier": "KI69", - "name": "Batavia Clermont County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KILN", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.779169899999999, - 39.428330000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KILN", - "@type": "wx:ObservationStation", - "elevation": { - "value": 327.96480000000003, - "unitCode": "unit:m" - }, - "stationIdentifier": "KILN", - "name": "Wilmington, Airborne Airpark Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KMRT", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.351600000000005, - 40.224699999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMRT", - "@type": "wx:ObservationStation", - "elevation": { - "value": 311.20080000000002, - "unitCode": "unit:m" - }, - "stationIdentifier": "KMRT", - "name": "Marysville Union County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KTZR", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.137219999999999, - 39.900829999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KTZR", - "@type": "wx:ObservationStation", - "elevation": { - "value": 276.14879999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KTZR", - "name": "Columbus, Bolton Field Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KFDY", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.668610000000001, - 41.01361 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KFDY", - "@type": "wx:ObservationStation", - "elevation": { - "value": 248.10720000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KFDY", - "name": "Findlay, Findlay Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KDLZ", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.114800000000002, - 40.279699999999998 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KDLZ", - "@type": "wx:ObservationStation", - "elevation": { - "value": 288.036, - "unitCode": "unit:m" - }, - "stationIdentifier": "KDLZ", - "name": "Delaware Municipal Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KOSU", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.0780599, - 40.078060000000001 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KOSU", - "@type": "wx:ObservationStation", - "elevation": { - "value": 274.92959999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KOSU", - "name": "Columbus, Ohio State University Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KLCK", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.933329999999998, - 39.816670000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KLCK", - "@type": "wx:ObservationStation", - "elevation": { - "value": 227.07600000000002, - "unitCode": "unit:m" - }, - "stationIdentifier": "KLCK", - "name": "Rickenbacker Air National Guard Base", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KMNN", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.068330000000003, - 40.616669999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMNN", - "@type": "wx:ObservationStation", - "elevation": { - "value": 302.97120000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KMNN", - "name": "Marion, Marion Municipal Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KCMH", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.876390000000001, - 39.994999999999997 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KCMH", - "@type": "wx:ObservationStation", - "elevation": { - "value": 248.10720000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KCMH", - "name": "Columbus - John Glenn Columbus International Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KFGX", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -83.743399999999994, - 38.541800000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KFGX", - "@type": "wx:ObservationStation", - "elevation": { - "value": 277.9776, - "unitCode": "unit:m" - }, - "stationIdentifier": "KFGX", - "name": "Flemingsburg Fleming-Mason Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KFFT", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.903329999999997, - 38.184719999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KFFT", - "@type": "wx:ObservationStation", - "elevation": { - "value": 245.0592, - "unitCode": "unit:m" - }, - "stationIdentifier": "KFFT", - "name": "Frankfort, Capital City Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KLHQ", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.663330000000002, - 39.757219900000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KLHQ", - "@type": "wx:ObservationStation", - "elevation": { - "value": 263.95679999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KLHQ", - "name": "Lancaster, Fairfield County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KLOU", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.663610000000006, - 38.227780000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KLOU", - "@type": "wx:ObservationStation", - "elevation": { - "value": 166.11600000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KLOU", - "name": "Louisville, Bowman Field Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KSDF", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -85.72972, - 38.177219999999998 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KSDF", - "@type": "wx:ObservationStation", - "elevation": { - "value": 150.876, - "unitCode": "unit:m" - }, - "stationIdentifier": "KSDF", - "name": "Louisville, Standiford Field", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KVTA", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.462500000000006, - 40.022779999999997 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KVTA", - "@type": "wx:ObservationStation", - "elevation": { - "value": 269.13839999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KVTA", - "name": "Newark, Newark Heath Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KLEX", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -84.6114599, - 38.033900000000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KLEX", - "@type": "wx:ObservationStation", - "elevation": { - "value": 291.084, - "unitCode": "unit:m" - }, - "stationIdentifier": "KLEX", - "name": "Lexington Blue Grass Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KMFD", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.517780000000002, - 40.820279900000003 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KMFD", - "@type": "wx:ObservationStation", - "elevation": { - "value": 395.02080000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KMFD", - "name": "Mansfield - Mansfield Lahm Regional Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KZZV", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.892219999999995, - 39.94444 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KZZV", - "@type": "wx:ObservationStation", - "elevation": { - "value": 274.01519999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KZZV", - "name": "Zanesville, Zanesville Municipal Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KHTS", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -82.555000000000007, - 38.365000000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KHTS", - "@type": "wx:ObservationStation", - "elevation": { - "value": 252.06960000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KHTS", - "name": "Huntington, Tri-State Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KBJJ", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.886669999999995, - 40.873060000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KBJJ", - "@type": "wx:ObservationStation", - "elevation": { - "value": 345.94800000000004, - "unitCode": "unit:m" - }, - "stationIdentifier": "KBJJ", - "name": "Wooster, Wayne County Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KPHD", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.423609999999996, - 40.471939900000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KPHD", - "@type": "wx:ObservationStation", - "elevation": { - "value": 271.88159999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KPHD", - "name": "New Philadelphia, Harry Clever Field", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KPKB", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.439170000000004, - 39.344999999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KPKB", - "@type": "wx:ObservationStation", - "elevation": { - "value": 262.12799999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KPKB", - "name": "Parkersburg, Mid-Ohio Valley Regional Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KCAK", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.443430000000006, - 40.918109999999999 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KCAK", - "@type": "wx:ObservationStation", - "elevation": { - "value": 369.11279999999999, - "unitCode": "unit:m" - }, - "stationIdentifier": "KCAK", - "name": "Akron Canton Regional Airport", - "timeZone": "America/New_York" - } - }, - { - "id": "https://api.weather.gov/stations/KCRW", - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -81.591390000000004, - 38.379440000000002 - ] - }, - "properties": { - "@id": "https://api.weather.gov/stations/KCRW", - "@type": "wx:ObservationStation", - "elevation": { - "value": 299.00880000000001, - "unitCode": "unit:m" - }, - "stationIdentifier": "KCRW", - "name": "Charleston, Yeager Airport", - "timeZone": "America/New_York" - } - } - ], - "observationStations": [ - "https://api.weather.gov/stations/KMIE", - "https://api.weather.gov/stations/KVES", - "https://api.weather.gov/stations/KAID", - "https://api.weather.gov/stations/KDAY", - "https://api.weather.gov/stations/KGEZ", - "https://api.weather.gov/stations/KMGY", - "https://api.weather.gov/stations/KHAO", - "https://api.weather.gov/stations/KFFO", - "https://api.weather.gov/stations/KCVG", - "https://api.weather.gov/stations/KEDJ", - "https://api.weather.gov/stations/KFWA", - "https://api.weather.gov/stations/KBAK", - "https://api.weather.gov/stations/KEYE", - "https://api.weather.gov/stations/KLUK", - "https://api.weather.gov/stations/KIND", - "https://api.weather.gov/stations/KAOH", - "https://api.weather.gov/stations/KI69", - "https://api.weather.gov/stations/KILN", - "https://api.weather.gov/stations/KMRT", - "https://api.weather.gov/stations/KTZR", - "https://api.weather.gov/stations/KFDY", - "https://api.weather.gov/stations/KDLZ", - "https://api.weather.gov/stations/KOSU", - "https://api.weather.gov/stations/KLCK", - "https://api.weather.gov/stations/KMNN", - "https://api.weather.gov/stations/KCMH", - "https://api.weather.gov/stations/KFGX", - "https://api.weather.gov/stations/KFFT", - "https://api.weather.gov/stations/KLHQ", - "https://api.weather.gov/stations/KLOU", - "https://api.weather.gov/stations/KSDF", - "https://api.weather.gov/stations/KVTA", - "https://api.weather.gov/stations/KLEX", - "https://api.weather.gov/stations/KMFD", - "https://api.weather.gov/stations/KZZV", - "https://api.weather.gov/stations/KHTS", - "https://api.weather.gov/stations/KBJJ", - "https://api.weather.gov/stations/KPHD", - "https://api.weather.gov/stations/KPKB", - "https://api.weather.gov/stations/KCAK", - "https://api.weather.gov/stations/KCRW" - ] -} \ No newline at end of file diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index b603f98bb04..afa428805e9 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -176,3 +176,37 @@ async def test_if_numeric_state_not_raise_on_unavailable(hass): hass.states.async_set("sensor.temperature", "unknown") assert not test(hass) assert len(logwarn.mock_calls) == 0 + + +async def test_extract_entities(): + """Test extracting entities.""" + condition.async_extract_entities( + { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature_2", + "below": 110, + }, + ], + } + ) == {"sensor.temperature", "sensor.temperature_2"} + + +async def test_extract_devices(): + """Test extracting devices.""" + condition.async_extract_devices( + { + "condition": "and", + "conditions": [ + {"condition": "device", "device_id": "abcd", "domain": "light"}, + {"condition": "device", "device_id": "qwer", "domain": "switch"}, + ], + } + ) == {"abcd", "qwer"} diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py new file mode 100644 index 00000000000..d7629a393a9 --- /dev/null +++ b/tests/helpers/test_debounce.py @@ -0,0 +1,62 @@ +"""Tests for debounce.""" +from asynctest import CoroutineMock + +from homeassistant.helpers import debounce + + +async def test_immediate_works(hass): + """Test immediate works.""" + calls = [] + debouncer = debounce.Debouncer( + hass, None, 0.01, True, CoroutineMock(side_effect=lambda: calls.append(None)) + ) + + await debouncer.async_call() + assert len(calls) == 1 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 1 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + debouncer.async_cancel() + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 2 + await debouncer._handle_timer_finish() + assert len(calls) == 2 + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + +async def test_not_immediate_works(hass): + """Test immediate works.""" + calls = [] + debouncer = debounce.Debouncer( + hass, None, 0.01, False, CoroutineMock(side_effect=lambda: calls.append(None)) + ) + + await debouncer.async_call() + assert len(calls) == 0 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + await debouncer.async_call() + assert len(calls) == 0 + assert debouncer._timer_task is not None + assert debouncer._execute_at_end_of_timer is True + + debouncer.async_cancel() + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False + + await debouncer.async_call() + assert len(calls) == 0 + await debouncer._handle_timer_finish() + assert len(calls) == 1 + assert debouncer._timer_task is None + assert debouncer._execute_at_end_of_timer is False diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a7fe2c25236..5e748e3adfe 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1022,3 +1022,59 @@ def test_log_exception(): assert p_error == "" else: assert p_error == str(exc) + + +async def test_referenced_entities(): + """Test referenced entities.""" + script_obj = script.Script( + None, + cv.SCRIPT_SCHEMA( + [ + { + "service": "test.script", + "data": {"entity_id": "light.service_not_list"}, + }, + { + "service": "test.script", + "data": {"entity_id": ["light.service_list"]}, + }, + { + "condition": "state", + "entity_id": "sensor.condition", + "state": "100", + }, + {"service": "test.script", "data": {"without": "entity_id"}}, + {"scene": "scene.hello"}, + {"event": "test_event"}, + {"delay": "{{ delay_period }}"}, + ] + ), + ) + assert script_obj.referenced_entities == { + "light.service_not_list", + "light.service_list", + "sensor.condition", + "scene.hello", + } + # Test we cache results. + assert script_obj.referenced_entities is script_obj.referenced_entities + + +async def test_referenced_devices(): + """Test referenced entities.""" + script_obj = script.Script( + None, + cv.SCRIPT_SCHEMA( + [ + {"domain": "light", "device_id": "script-dev-id"}, + { + "condition": "device", + "device_id": "condition-dev-id", + "domain": "switch", + }, + ] + ), + ) + assert script_obj.referenced_devices == {"script-dev-id", "condition-dev-id"} + # Test we cache results. + assert script_obj.referenced_devices is script_obj.referenced_devices diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index b42b30a836a..d90842d1b71 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -23,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component from tests.common import ( + MockEntity, get_test_home_assistant, mock_coro, mock_device_registry, @@ -64,6 +65,54 @@ def mock_entities(): return entities +@pytest.fixture +def area_mock(hass): + """Mock including area info.""" + hass.states.async_set("light.Bowl", STATE_ON) + hass.states.async_set("light.Ceiling", STATE_OFF) + hass.states.async_set("light.Kitchen", STATE_OFF) + + device_in_area = dev_reg.DeviceEntry(area_id="test-area") + device_no_area = dev_reg.DeviceEntry() + device_diff_area = dev_reg.DeviceEntry(area_id="diff-area") + + mock_device_registry( + hass, + { + device_in_area.id: device_in_area, + device_no_area.id: device_no_area, + device_diff_area.id: device_diff_area, + }, + ) + + entity_in_area = ent_reg.RegistryEntry( + entity_id="light.in_area", + unique_id="in-area-id", + platform="test", + device_id=device_in_area.id, + ) + entity_no_area = ent_reg.RegistryEntry( + entity_id="light.no_area", + unique_id="no-area-id", + platform="test", + device_id=device_no_area.id, + ) + entity_diff_area = ent_reg.RegistryEntry( + entity_id="light.diff_area", + unique_id="diff-area-id", + platform="test", + device_id=device_diff_area.id, + ) + mock_registry( + hass, + { + entity_in_area.entity_id: entity_in_area, + entity_no_area.entity_id: entity_no_area, + entity_diff_area.entity_id: entity_diff_area, + }, + ) + + class TestServiceHelpers(unittest.TestCase): """Test the Home Assistant service helpers.""" @@ -204,52 +253,8 @@ async def test_extract_entity_ids(hass): ) -async def test_extract_entity_ids_from_area(hass): +async def test_extract_entity_ids_from_area(hass, area_mock): """Test extract_entity_ids method with areas.""" - hass.states.async_set("light.Bowl", STATE_ON) - hass.states.async_set("light.Ceiling", STATE_OFF) - hass.states.async_set("light.Kitchen", STATE_OFF) - - device_in_area = dev_reg.DeviceEntry(area_id="test-area") - device_no_area = dev_reg.DeviceEntry() - device_diff_area = dev_reg.DeviceEntry(area_id="diff-area") - - mock_device_registry( - hass, - { - device_in_area.id: device_in_area, - device_no_area.id: device_no_area, - device_diff_area.id: device_diff_area, - }, - ) - - entity_in_area = ent_reg.RegistryEntry( - entity_id="light.in_area", - unique_id="in-area-id", - platform="test", - device_id=device_in_area.id, - ) - entity_no_area = ent_reg.RegistryEntry( - entity_id="light.no_area", - unique_id="no-area-id", - platform="test", - device_id=device_no_area.id, - ) - entity_diff_area = ent_reg.RegistryEntry( - entity_id="light.diff_area", - unique_id="diff-area-id", - platform="test", - device_id=device_diff_area.id, - ) - mock_registry( - hass, - { - entity_in_area.entity_id: entity_in_area, - entity_no_area.entity_id: entity_no_area, - entity_diff_area.entity_id: entity_diff_area, - }, - ) - call = ha.ServiceCall("light", "turn_on", {"area_id": "test-area"}) assert {"light.in_area"} == await service.async_extract_entity_ids(hass, call) @@ -301,6 +306,36 @@ async def test_call_with_required_features(hass, mock_entities): assert test_service_mock.call_count == 1 +async def test_call_with_sync_func(hass, mock_entities): + """Test invoking sync service calls.""" + test_service_mock = Mock() + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + test_service_mock, + ha.ServiceCall("test_domain", "test_service", {"entity_id": "light.kitchen"}), + ) + assert test_service_mock.call_count == 1 + + +async def test_call_with_sync_attr(hass, mock_entities): + """Test invoking sync service calls.""" + mock_method = mock_entities["light.kitchen"].sync_method = Mock() + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + "sync_method", + ha.ServiceCall( + "test_domain", + "test_service", + {"entity_id": "light.kitchen", "area_id": "abcd"}, + ), + ) + assert mock_method.call_count == 1 + # We pass empty kwargs because both entity_id and area_id are filtered out + assert mock_method.mock_calls[0][2] == {} + + async def test_call_context_user_not_exist(hass): """Check we don't allow deleted users to do things.""" with pytest.raises(exceptions.UnknownUser) as err: @@ -343,7 +378,7 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == [mock_entities["light.kitchen"]] @@ -374,7 +409,7 @@ async def test_call_context_target_specific( ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == [mock_entities["light.kitchen"]] @@ -417,7 +452,7 @@ async def test_call_no_context_target_all( ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == list(mock_entities.values()) @@ -437,7 +472,7 @@ async def test_call_no_context_target_specific( ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == [mock_entities["light.kitchen"]] @@ -453,7 +488,7 @@ async def test_call_with_match_all( ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == [ mock_entities["light.kitchen"], mock_entities["light.living_room"], @@ -475,7 +510,7 @@ async def test_call_with_omit_entity_id( ) assert len(mock_service_platform_call.mock_calls) == 1 - entities = mock_service_platform_call.mock_calls[0][1][2] + entities = mock_service_platform_call.mock_calls[0][1][3] assert entities == [] @@ -678,3 +713,86 @@ async def test_domain_control_no_user(hass, mock_entities): ) assert len(calls) == 1 + + +async def test_extract_from_service_available_device(hass): + """Test the extraction of entity from service and device is available.""" + entities = [ + MockEntity(name="test_1", entity_id="test_domain.test_1"), + MockEntity(name="test_2", entity_id="test_domain.test_2", available=False), + MockEntity(name="test_3", entity_id="test_domain.test_3"), + MockEntity(name="test_4", entity_id="test_domain.test_4", available=False), + ] + + call_1 = ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) + + assert ["test_domain.test_1", "test_domain.test_3"] == [ + ent.entity_id + for ent in (await service.async_extract_entities(hass, entities, call_1)) + ] + + call_2 = ha.ServiceCall( + "test", + "service", + data={"entity_id": ["test_domain.test_3", "test_domain.test_4"]}, + ) + + assert ["test_domain.test_3"] == [ + ent.entity_id + for ent in (await service.async_extract_entities(hass, entities, call_2)) + ] + + +async def test_extract_from_service_empty_if_no_entity_id(hass): + """Test the extraction from service without specifying entity.""" + entities = [ + MockEntity(name="test_1", entity_id="test_domain.test_1"), + MockEntity(name="test_2", entity_id="test_domain.test_2"), + ] + call = ha.ServiceCall("test", "service") + + assert [] == [ + ent.entity_id + for ent in (await service.async_extract_entities(hass, entities, call)) + ] + + +async def test_extract_from_service_filter_out_non_existing_entities(hass): + """Test the extraction of non existing entities from service.""" + entities = [ + MockEntity(name="test_1", entity_id="test_domain.test_1"), + MockEntity(name="test_2", entity_id="test_domain.test_2"), + ] + + call = ha.ServiceCall( + "test", + "service", + {"entity_id": ["test_domain.test_2", "test_domain.non_exist"]}, + ) + + assert ["test_domain.test_2"] == [ + ent.entity_id + for ent in (await service.async_extract_entities(hass, entities, call)) + ] + + +async def test_extract_from_service_area_id(hass, area_mock): + """Test the extraction using area ID as reference.""" + entities = [ + MockEntity(name="in_area", entity_id="light.in_area"), + MockEntity(name="no_area", entity_id="light.no_area"), + MockEntity(name="diff_area", entity_id="light.diff_area"), + ] + + call = ha.ServiceCall("light", "turn_on", {"area_id": "test-area"}) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 1 + assert extracted[0].entity_id == "light.in_area" + + call = ha.ServiceCall("light", "turn_on", {"area_id": ["test-area", "diff-area"]}) + extracted = await service.async_extract_entities(hass, entities, call) + assert len(extracted) == 2 + assert sorted(ent.entity_id for ent in extracted) == [ + "light.diff_area", + "light.in_area", + ] diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index ea7ae03b5db..737c3b56ecf 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -120,7 +120,7 @@ def test_secrets(isfile_patch, loop): @patch("os.path.isfile", return_value=True) def test_package_invalid(isfile_patch, loop): - """Test a valid platform setup.""" + """Test an invalid package.""" files = { YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index e64672a1e88..48c5360d888 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -3,18 +3,23 @@ import asyncio import logging import os -from unittest.mock import Mock, patch +from unittest.mock import Mock + +from asynctest import patch +import pytest from homeassistant import bootstrap import homeassistant.config as config_util +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util from tests.common import ( + MockConfigEntry, MockModule, + flush_store, get_test_config_dir, mock_coro, mock_integration, - patch_yaml_files, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -23,26 +28,6 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE) _LOGGER = logging.getLogger(__name__) -# prevent .HA_VERSION file from being written -@patch("homeassistant.bootstrap.conf_util.process_ha_config_upgrade", Mock()) -@patch( - "homeassistant.util.location.async_detect_location_info", - Mock(return_value=mock_coro(None)), -) -@patch("os.path.isfile", Mock(return_value=True)) -@patch("os.access", Mock(return_value=True)) -@patch("homeassistant.bootstrap.async_enable_logging", Mock(return_value=True)) -def test_from_config_file(hass): - """Test with configuration file.""" - components = set(["browser", "conversation", "script"]) - files = {"config.yaml": "".join(f"{comp}:\n" for comp in components)} - - with patch_yaml_files(files, True): - yield from bootstrap.async_from_config_file("config.yaml", hass) - - assert components == hass.config.components - - @patch("homeassistant.bootstrap.async_enable_logging", Mock()) @asyncio.coroutine def test_home_assistant_core_config_validation(hass): @@ -54,33 +39,6 @@ def test_home_assistant_core_config_validation(hass): assert result is None -async def test_async_from_config_file_not_mount_deps_folder(loop): - """Test that we not mount the deps folder inside async_from_config_file.""" - hass = Mock(async_add_executor_job=Mock(side_effect=lambda *args: mock_coro())) - - with patch("homeassistant.bootstrap.is_virtual_env", return_value=False), patch( - "homeassistant.bootstrap.async_enable_logging", return_value=mock_coro() - ), patch( - "homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro() - ) as mock_mount, patch( - "homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro() - ): - - await bootstrap.async_from_config_file("mock-path", hass) - assert len(mock_mount.mock_calls) == 1 - - with patch("homeassistant.bootstrap.is_virtual_env", return_value=True), patch( - "homeassistant.bootstrap.async_enable_logging", return_value=mock_coro() - ), patch( - "homeassistant.bootstrap.async_mount_local_lib_path", return_value=mock_coro() - ) as mock_mount, patch( - "homeassistant.bootstrap.async_from_config_dict", return_value=mock_coro() - ): - - await bootstrap.async_from_config_file("mock-path", hass) - assert len(mock_mount.mock_calls) == 0 - - async def test_load_hassio(hass): """Test that we load Hass.io component.""" with patch.dict(os.environ, {}, clear=True): @@ -233,3 +191,169 @@ async def test_setup_after_deps_not_present(hass, caplog): assert "first_dep" not in hass.config.components assert "second_dep" in hass.config.components assert order == ["root", "second_dep"] + + +@pytest.fixture +def mock_is_virtual_env(): + """Mock enable logging.""" + with patch( + "homeassistant.bootstrap.is_virtual_env", return_value=False + ) as is_virtual_env: + yield is_virtual_env + + +@pytest.fixture +def mock_enable_logging(): + """Mock enable logging.""" + with patch("homeassistant.bootstrap.async_enable_logging") as enable_logging: + yield enable_logging + + +@pytest.fixture +def mock_mount_local_lib_path(): + """Mock enable logging.""" + with patch( + "homeassistant.bootstrap.async_mount_local_lib_path" + ) as mount_local_lib_path: + yield mount_local_lib_path + + +@pytest.fixture +def mock_process_ha_config_upgrade(): + """Mock enable logging.""" + with patch( + "homeassistant.config.process_ha_config_upgrade" + ) as process_ha_config_upgrade: + yield process_ha_config_upgrade + + +@pytest.fixture +def mock_ensure_config_exists(): + """Mock enable logging.""" + with patch( + "homeassistant.config.async_ensure_config_exists", return_value=True + ) as ensure_config_exists: + yield ensure_config_exists + + +async def test_setup_hass( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + verbose = Mock() + log_rotate_days = Mock() + log_file = Mock() + log_no_color = Mock() + + with patch( + "homeassistant.config.async_hass_config_yaml", return_value={"browser": {}} + ): + hass = await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=verbose, + log_rotate_days=log_rotate_days, + log_file=log_file, + log_no_color=log_no_color, + skip_pip=True, + safe_mode=False, + ) + + assert "browser" in hass.config.components + + assert len(mock_enable_logging.mock_calls) == 1 + assert mock_enable_logging.mock_calls[0][1] == ( + hass, + verbose, + log_rotate_days, + log_file, + log_no_color, + ) + assert len(mock_mount_local_lib_path.mock_calls) == 1 + assert len(mock_ensure_config_exists.mock_calls) == 1 + assert len(mock_process_ha_config_upgrade.mock_calls) == 1 + + +async def test_setup_hass_invalid_yaml( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + with patch( + "homeassistant.config.async_hass_config_yaml", side_effect=HomeAssistantError + ): + hass = await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ) + + assert "safe_mode" in hass.config.components + assert len(mock_mount_local_lib_path.mock_calls) == 0 + + +async def test_setup_hass_config_dir_nonexistent( + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + mock_ensure_config_exists.return_value = False + + assert ( + await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=False, + ) + is None + ) + + +async def test_setup_hass_safe_mode( + hass, + mock_enable_logging, + mock_is_virtual_env, + mock_mount_local_lib_path, + mock_ensure_config_exists, + mock_process_ha_config_upgrade, +): + """Test it works.""" + # Add a config entry to storage. + MockConfigEntry(domain="browser").add_to_hass(hass) + hass.config_entries._async_schedule_save() + await flush_store(hass.config_entries._store) + + with patch("homeassistant.components.browser.setup") as browser_setup: + hass = await bootstrap.async_setup_hass( + config_dir=get_test_config_dir(), + verbose=False, + log_rotate_days=10, + log_file="", + log_no_color=False, + skip_pip=True, + safe_mode=True, + ) + + assert "safe_mode" in hass.config.components + assert len(mock_mount_local_lib_path.mock_calls) == 0 + + # Validate we didn't try to set up config entry. + assert "browser" not in hass.config.components + assert len(browser_setup.mock_calls) == 0 diff --git a/tests/test_config.py b/tests/test_config.py index b4f7a916d37..1fc92ee954b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -82,7 +82,7 @@ def teardown(): async def test_create_default_config(hass): """Test creation of default config.""" - await config_util.async_create_default_config(hass, CONFIG_DIR) + await config_util.async_create_default_config(hass) assert os.path.isfile(YAML_PATH) assert os.path.isfile(SECRET_PATH) @@ -91,20 +91,13 @@ async def test_create_default_config(hass): assert os.path.isfile(AUTOMATIONS_PATH) -def test_find_config_file_yaml(): - """Test if it finds a YAML config file.""" - create_file(YAML_PATH) - - assert YAML_PATH == config_util.find_config_file(CONFIG_DIR) - - async def test_ensure_config_exists_creates_config(hass): """Test that calling ensure_config_exists. If not creates a new config file. """ with mock.patch("builtins.print") as mock_print: - await config_util.async_ensure_config_exists(hass, CONFIG_DIR) + await config_util.async_ensure_config_exists(hass) assert os.path.isfile(YAML_PATH) assert mock_print.called @@ -113,7 +106,7 @@ async def test_ensure_config_exists_creates_config(hass): async def test_ensure_config_exists_uses_existing_config(hass): """Test that calling ensure_config_exists uses existing config.""" create_file(YAML_PATH) - await config_util.async_ensure_config_exists(hass, CONFIG_DIR) + await config_util.async_ensure_config_exists(hass) with open(YAML_PATH) as f: content = f.read() @@ -172,13 +165,9 @@ async def test_create_default_config_returns_none_if_write_error(hass): Non existing folder returns None. """ + hass.config.config_dir = os.path.join(CONFIG_DIR, "non_existing_dir/") with mock.patch("builtins.print") as mock_print: - assert ( - await config_util.async_create_default_config( - hass, os.path.join(CONFIG_DIR, "non_existing_dir/") - ) - is None - ) + assert await config_util.async_create_default_config(hass) is False assert mock_print.called @@ -331,7 +320,6 @@ def test_config_upgrade_same_version(hass): assert opened_file.write.call_count == 0 -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) def test_config_upgrade_no_file(hass): """Test update of version on upgrade, with no version file.""" mock_open = mock.mock_open() @@ -359,7 +347,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): "version": 1, } await config_util.async_process_ha_core_config( - hass, {"whitelist_external_dirs": "/tmp"} + hass, {"whitelist_external_dirs": "/etc"} ) assert hass.config.latitude == 55 @@ -369,7 +357,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == "Europe/Copenhagen" assert len(hass.config.whitelist_external_dirs) == 2 - assert "/tmp" in hass.config.whitelist_external_dirs + assert "/etc" in hass.config.whitelist_external_dirs assert hass.config.config_source == SOURCE_STORAGE @@ -389,7 +377,7 @@ async def test_updating_configuration(hass, hass_storage): } hass_storage["core.config"] = dict(core_data) await config_util.async_process_ha_core_config( - hass, {"whitelist_external_dirs": "/tmp"} + hass, {"whitelist_external_dirs": "/etc"} ) await hass.config.async_update(latitude=50) @@ -414,7 +402,7 @@ async def test_override_stored_configuration(hass, hass_storage): "version": 1, } await config_util.async_process_ha_core_config( - hass, {"latitude": 60, "whitelist_external_dirs": "/tmp"} + hass, {"latitude": 60, "whitelist_external_dirs": "/etc"} ) assert hass.config.latitude == 60 @@ -424,7 +412,7 @@ async def test_override_stored_configuration(hass, hass_storage): assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC assert hass.config.time_zone.zone == "Europe/Copenhagen" assert len(hass.config.whitelist_external_dirs) == 2 - assert "/tmp" in hass.config.whitelist_external_dirs + assert "/etc" in hass.config.whitelist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML @@ -439,7 +427,7 @@ async def test_loading_configuration(hass): "name": "Huis", CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, "time_zone": "America/New_York", - "whitelist_external_dirs": "/tmp", + "whitelist_external_dirs": "/etc", }, ) @@ -450,7 +438,7 @@ async def test_loading_configuration(hass): assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL assert hass.config.time_zone.zone == "America/New_York" assert len(hass.config.whitelist_external_dirs) == 2 - assert "/tmp" in hass.config.whitelist_external_dirs + assert "/etc" in hass.config.whitelist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d2519a495e2..da3fb740694 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1080,6 +1080,77 @@ async def test_unique_id_existing_entry(hass, manager): assert len(async_remove_entry.mock_calls) == 1 +async def test_unique_id_update_existing_entry(hass, manager): + """Test that we update an entry if there already is an entry with unique ID.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + ) + entry.add_to_hass(hass) + + mock_integration( + hass, MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + await self._abort_if_unique_id_configured(updates={"host": "1.1.1.1"}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "1.1.1.1" + assert entry.data["additional"] == "data" + + +async def test_unique_id_not_update_existing_entry(hass, manager): + """Test that we do not update an entry if existing entry has the data.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + ) + entry.add_to_hass(hass) + + mock_integration( + hass, MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + await self._abort_if_unique_id_configured(updates={"host": "0.0.0.0"}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_update_entry" + ) as async_update_entry: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "0.0.0.0" + assert entry.data["additional"] == "data" + assert len(async_update_entry.mock_calls) == 0 + + async def test_unique_id_in_progress(hass, manager): """Test that we abort if there is already a flow in progress with same unique id.""" mock_integration(hass, MockModule("comp")) diff --git a/tests/test_core.py b/tests/test_core.py index 4229b0fb5c0..aa0c615ec04 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -881,17 +881,17 @@ class TestConfig(unittest.TestCase): def test_path_with_file(self): """Test get_config_path method.""" - self.config.config_dir = "/tmp/ha-config" - assert "/tmp/ha-config/test.conf" == self.config.path("test.conf") + self.config.config_dir = "/test/ha-config" + assert "/test/ha-config/test.conf" == self.config.path("test.conf") def test_path_with_dir_and_file(self): """Test get_config_path method.""" - self.config.config_dir = "/tmp/ha-config" - assert "/tmp/ha-config/dir/test.conf" == self.config.path("dir", "test.conf") + self.config.config_dir = "/test/ha-config" + assert "/test/ha-config/dir/test.conf" == self.config.path("dir", "test.conf") def test_as_dict(self): """Test as dict.""" - self.config.config_dir = "/tmp/ha-config" + self.config.config_dir = "/test/ha-config" expected = { "latitude": 0, "longitude": 0, @@ -900,7 +900,7 @@ class TestConfig(unittest.TestCase): "location_name": "Home", "time_zone": "UTC", "components": set(), - "config_dir": "/tmp/ha-config", + "config_dir": "/test/ha-config", "whitelist_external_dirs": set(), "version": __version__, "config_source": "default", diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 6dd6eafca1d..3f03619a052 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -92,6 +92,19 @@ async def test_detect_location_info_ipapi(aioclient_mock, session): assert info.use_metric +async def test_detect_location_info_ipapi_exhaust(aioclient_mock, session): + """Test detect location info using ipapi.co.""" + aioclient_mock.get(location_util.IPAPI, json={"latitude": "Sign up to access"}) + aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) + + info = await location_util.async_detect_location_info(session, _test_real=True) + + assert info is not None + # ip_api result because ipapi got skipped + assert info.country_code == "US" + assert len(aioclient_mock.mock_calls) == 2 + + async def test_detect_location_info_ip_api(aioclient_mock, session): """Test detect location info using ip-api.com.""" aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 622d87d1a27..140859ccb73 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -97,10 +97,10 @@ def test_include_yaml(): @patch("homeassistant.util.yaml.loader.os.walk") def test_include_dir_list(mock_walk): """Test include dir list yaml.""" - mock_walk.return_value = [["/tmp", [], ["two.yaml", "one.yaml"]]] + mock_walk.return_value = [["/test", [], ["two.yaml", "one.yaml"]]] - with patch_yaml_files({"/tmp/one.yaml": "one", "/tmp/two.yaml": "two"}): - conf = "key: !include_dir_list /tmp" + with patch_yaml_files({"/test/one.yaml": "one", "/test/two.yaml": "two"}): + conf = "key: !include_dir_list /test" with io.StringIO(conf) as file: doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == sorted(["one", "two"]) @@ -110,19 +110,19 @@ def test_include_dir_list(mock_walk): def test_include_dir_list_recursive(mock_walk): """Test include dir recursive list yaml.""" mock_walk.return_value = [ - ["/tmp", ["tmp2", ".ignore", "ignore"], ["zero.yaml"]], - ["/tmp/tmp2", [], ["one.yaml", "two.yaml"]], - ["/tmp/ignore", [], [".ignore.yaml"]], + ["/test", ["tmp2", ".ignore", "ignore"], ["zero.yaml"]], + ["/test/tmp2", [], ["one.yaml", "two.yaml"]], + ["/test/ignore", [], [".ignore.yaml"]], ] with patch_yaml_files( { - "/tmp/zero.yaml": "zero", - "/tmp/tmp2/one.yaml": "one", - "/tmp/tmp2/two.yaml": "two", + "/test/zero.yaml": "zero", + "/test/tmp2/one.yaml": "one", + "/test/tmp2/two.yaml": "two", } ): - conf = "key: !include_dir_list /tmp" + conf = "key: !include_dir_list /test" with io.StringIO(conf) as file: assert ( ".ignore" in mock_walk.return_value[0][1] @@ -137,11 +137,11 @@ def test_include_dir_list_recursive(mock_walk): def test_include_dir_named(mock_walk): """Test include dir named yaml.""" mock_walk.return_value = [ - ["/tmp", [], ["first.yaml", "second.yaml", "secrets.yaml"]] + ["/test", [], ["first.yaml", "second.yaml", "secrets.yaml"]] ] - with patch_yaml_files({"/tmp/first.yaml": "one", "/tmp/second.yaml": "two"}): - conf = "key: !include_dir_named /tmp" + with patch_yaml_files({"/test/first.yaml": "one", "/test/second.yaml": "two"}): + conf = "key: !include_dir_named /test" correct = {"first": "one", "second": "two"} with io.StringIO(conf) as file: doc = yaml_loader.yaml.safe_load(file) @@ -152,19 +152,19 @@ def test_include_dir_named(mock_walk): def test_include_dir_named_recursive(mock_walk): """Test include dir named yaml.""" mock_walk.return_value = [ - ["/tmp", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], - ["/tmp/tmp2", [], ["second.yaml", "third.yaml"]], - ["/tmp/ignore", [], [".ignore.yaml"]], + ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], + ["/test/tmp2", [], ["second.yaml", "third.yaml"]], + ["/test/ignore", [], [".ignore.yaml"]], ] with patch_yaml_files( { - "/tmp/first.yaml": "one", - "/tmp/tmp2/second.yaml": "two", - "/tmp/tmp2/third.yaml": "three", + "/test/first.yaml": "one", + "/test/tmp2/second.yaml": "two", + "/test/tmp2/third.yaml": "three", } ): - conf = "key: !include_dir_named /tmp" + conf = "key: !include_dir_named /test" correct = {"first": "one", "second": "two", "third": "three"} with io.StringIO(conf) as file: assert ( @@ -179,12 +179,12 @@ def test_include_dir_named_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") def test_include_dir_merge_list(mock_walk): """Test include dir merge list yaml.""" - mock_walk.return_value = [["/tmp", [], ["first.yaml", "second.yaml"]]] + mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] with patch_yaml_files( - {"/tmp/first.yaml": "- one", "/tmp/second.yaml": "- two\n- three"} + {"/test/first.yaml": "- one", "/test/second.yaml": "- two\n- three"} ): - conf = "key: !include_dir_merge_list /tmp" + conf = "key: !include_dir_merge_list /test" with io.StringIO(conf) as file: doc = yaml_loader.yaml.safe_load(file) assert sorted(doc["key"]) == sorted(["one", "two", "three"]) @@ -194,19 +194,19 @@ def test_include_dir_merge_list(mock_walk): def test_include_dir_merge_list_recursive(mock_walk): """Test include dir merge list yaml.""" mock_walk.return_value = [ - ["/tmp", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], - ["/tmp/tmp2", [], ["second.yaml", "third.yaml"]], - ["/tmp/ignore", [], [".ignore.yaml"]], + ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], + ["/test/tmp2", [], ["second.yaml", "third.yaml"]], + ["/test/ignore", [], [".ignore.yaml"]], ] with patch_yaml_files( { - "/tmp/first.yaml": "- one", - "/tmp/tmp2/second.yaml": "- two", - "/tmp/tmp2/third.yaml": "- three\n- four", + "/test/first.yaml": "- one", + "/test/tmp2/second.yaml": "- two", + "/test/tmp2/third.yaml": "- three\n- four", } ): - conf = "key: !include_dir_merge_list /tmp" + conf = "key: !include_dir_merge_list /test" with io.StringIO(conf) as file: assert ( ".ignore" in mock_walk.return_value[0][1] @@ -220,15 +220,15 @@ def test_include_dir_merge_list_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") def test_include_dir_merge_named(mock_walk): """Test include dir merge named yaml.""" - mock_walk.return_value = [["/tmp", [], ["first.yaml", "second.yaml"]]] + mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] files = { - "/tmp/first.yaml": "key1: one", - "/tmp/second.yaml": "key2: two\nkey3: three", + "/test/first.yaml": "key1: one", + "/test/second.yaml": "key2: two\nkey3: three", } with patch_yaml_files(files): - conf = "key: !include_dir_merge_named /tmp" + conf = "key: !include_dir_merge_named /test" with io.StringIO(conf) as file: doc = yaml_loader.yaml.safe_load(file) assert doc["key"] == {"key1": "one", "key2": "two", "key3": "three"} @@ -238,19 +238,19 @@ def test_include_dir_merge_named(mock_walk): def test_include_dir_merge_named_recursive(mock_walk): """Test include dir merge named yaml.""" mock_walk.return_value = [ - ["/tmp", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], - ["/tmp/tmp2", [], ["second.yaml", "third.yaml"]], - ["/tmp/ignore", [], [".ignore.yaml"]], + ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], + ["/test/tmp2", [], ["second.yaml", "third.yaml"]], + ["/test/ignore", [], [".ignore.yaml"]], ] with patch_yaml_files( { - "/tmp/first.yaml": "key1: one", - "/tmp/tmp2/second.yaml": "key2: two", - "/tmp/tmp2/third.yaml": "key3: three\nkey4: four", + "/test/first.yaml": "key1: one", + "/test/tmp2/second.yaml": "key2: two", + "/test/tmp2/third.yaml": "key3: three\nkey4: four", } ): - conf = "key: !include_dir_merge_named /tmp" + conf = "key: !include_dir_merge_named /test" with io.StringIO(conf) as file: assert ( ".ignore" in mock_walk.return_value[0][1]