From 4cd83e71dd6475b2c33ec23a3dc7f59b2bca3ddb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Jan 2020 21:07:04 +0100 Subject: [PATCH 001/393] Version bump to 0.105.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a9642e88e15..facb365f75c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 104 +MINOR_VERSION = 105 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From ed6aef2fd7b613b01351a7f639213ffff6bcf636 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 8 Jan 2020 21:22:56 +0100 Subject: [PATCH 002/393] Implement capability attributes (#30545) * Implement capability attributes * Fix HeOS update order * Fix test --- homeassistant/components/fan/__init__.py | 6 ++++- homeassistant/components/heos/media_player.py | 4 ++- .../components/media_player/__init__.py | 20 ++++++++++++-- homeassistant/components/vacuum/__init__.py | 13 ++++++++- .../components/water_heater/__init__.py | 27 +++++++++++++------ tests/components/fan/test_init.py | 2 +- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index fe6843ed6b9..44b33af0c6e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -47,7 +47,6 @@ ATTR_DIRECTION = "direction" PROP_TO_ATTR = { "speed": ATTR_SPEED, - "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, "current_direction": ATTR_DIRECTION, } @@ -178,6 +177,11 @@ class FanEntity(ToggleEntity): """Return the current direction of the fan.""" return None + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + return {ATTR_SPEED_LIST: self.speed_list} + @property def state_attributes(self) -> dict: """Return optional state attributes.""" diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 10ea28ca16c..9016a8b3cea 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -115,7 +115,6 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_added_to_hass(self): """Device added to hass.""" - self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] # Update state when attributes of the player change self._signals.append( self._player.heos.dispatcher.connect( @@ -242,6 +241,9 @@ class HeosMediaPlayer(MediaPlayerDevice): current_support = [CONTROL_TO_SUPPORT[control] for control in controls] self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) + if self._source_manager is None: + self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] + async def async_will_remove_from_hass(self): """Disconnect the device when removed.""" for signal_remove in self._signals: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 1375a0ed429..83c117d6c05 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -149,9 +149,7 @@ ATTR_TO_PROPERTY = [ ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, - ATTR_INPUT_SOURCE_LIST, ATTR_SOUND_MODE, - ATTR_SOUND_MODE_LIST, ATTR_MEDIA_SHUFFLE, ] @@ -784,6 +782,24 @@ class MediaPlayerDevice(Entity): return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token, image_hash) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + data = {} + + if supported_features & SUPPORT_SELECT_SOURCE: + source_list = self.source_list + if source_list: + data[ATTR_INPUT_SOURCE_LIST] = source_list + + if supported_features & SUPPORT_SELECT_SOUND_MODE: + sound_mode_list = self.sound_mode_list + if sound_mode_list: + data[ATTR_SOUND_MODE_LIST] = sound_mode_list + + return data + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 5dd4682e7cc..225a6ed72bc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -246,6 +246,12 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" @@ -260,7 +266,6 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): if self.fan_speed is not None: data[ATTR_FAN_SPEED] = self.fan_speed - data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list return data @@ -323,6 +328,12 @@ class StateVacuumDevice(_BaseVacuum): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index c5ba009717c..8da94ff1098 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -143,6 +143,25 @@ class WaterHeaterDevice(Entity): return PRECISION_TENTHS return PRECISION_WHOLE + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + + data = { + ATTR_MIN_TEMP: show_temp( + self.hass, self.min_temp, self.temperature_unit, self.precision + ), + ATTR_MAX_TEMP: show_temp( + self.hass, self.max_temp, self.temperature_unit, self.precision + ), + } + + if supported_features & SUPPORT_OPERATION_MODE: + data[ATTR_OPERATION_LIST] = self.operation_list + + return data + @property def state_attributes(self): """Return the optional state attributes.""" @@ -153,12 +172,6 @@ class WaterHeaterDevice(Entity): self.temperature_unit, self.precision, ), - ATTR_MIN_TEMP: show_temp( - self.hass, self.min_temp, self.temperature_unit, self.precision - ), - ATTR_MAX_TEMP: show_temp( - self.hass, self.max_temp, self.temperature_unit, self.precision - ), ATTR_TEMPERATURE: show_temp( self.hass, self.target_temperature, @@ -183,8 +196,6 @@ class WaterHeaterDevice(Entity): if supported_features & SUPPORT_OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation - if self.operation_list: - data[ATTR_OPERATION_LIST] = self.operation_list if supported_features & SUPPORT_AWAY_MODE: is_away = self.is_away_mode_on diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index 316504381ec..5d66edea9c7 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -31,7 +31,7 @@ class TestFanEntity(unittest.TestCase): assert "off" == self.fan.state assert 0 == len(self.fan.speed_list) assert 0 == self.fan.supported_features - assert {"speed_list": []} == self.fan.state_attributes + assert {"speed_list": []} == self.fan.capability_attributes # Test set_speed not required self.fan.oscillate(True) with pytest.raises(NotImplementedError): From 103d352b1f112f688ff687c21bcc233ecdb48017 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Wed, 8 Jan 2020 14:27:48 -0600 Subject: [PATCH 003/393] Add Doorbird events to logbook (#30588) * Add Doorbird events to logbook * Add logbook to dependencies --- homeassistant/components/doorbird/__init__.py | 3 +++ homeassistant/components/doorbird/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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"] } From ff5f890e79863ec6d58afb376b3e5c39b5f1b3f2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 8 Jan 2020 22:33:51 +0100 Subject: [PATCH 004/393] Fix problem with restoring POE control (#30597) --- homeassistant/components/unifi/switch.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 5b64f573ccd..b1f62131eb4 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -42,12 +42,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _, mac = entity.unique_id.split("-", 1) - if mac in controller.api.clients or mac not in controller.api.clients_all: + if mac in controller.api.clients: + switches_off.append(entity.unique_id) continue - client = controller.api.clients_all[mac] - controller.api.clients.process_raw([client.raw]) - switches_off.append(entity.unique_id) + if mac in controller.api.clients_all: + client = controller.api.clients_all[mac] + controller.api.clients.process_raw([client.raw]) + switches_off.append(entity.unique_id) + continue @callback def update_controller(): From 9ce5c65b142ffcb2dd9439c99337592de366a91f Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 9 Jan 2020 00:32:57 +0100 Subject: [PATCH 005/393] Update pyhomematic to 0.1.63 (#30594) --- homeassistant/components/homematic/__init__.py | 1 + homeassistant/components/homematic/binary_sensor.py | 1 + homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index cfaffad6ce3..01bc94ce58f 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -173,6 +173,7 @@ HM_DEVICE_TYPES = { "IPMultiIO", "TiltIP", "IPShutterContactSabotage", + "IPContact", ], DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], DISCOVER_LOCKS: ["KeyMatic"], diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index cc2907c64fb..1832652406d 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -29,6 +29,7 @@ SENSOR_TYPES_CLASS = { "SmokeV2": DEVICE_CLASS_SMOKE, "TiltSensor": None, "WeatherSensor": None, + "IPContact": DEVICE_CLASS_OPENING, } diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 709e99a232e..c4e09c36b8e 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,7 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.62"], + "requirements": ["pyhomematic==0.1.63"], "dependencies": [], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/requirements_all.txt b/requirements_all.txt index bd9a5bdf66e..2163689bfb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pyhik==0.2.5 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.62 +pyhomematic==0.1.63 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c52ca769b78..99c72dff746 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.62 +pyhomematic==0.1.63 # homeassistant.components.icloud pyicloud==0.9.1 From 8062bed53ea188a26be9ee413bc42f71b48f9806 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 9 Jan 2020 00:31:49 +0000 Subject: [PATCH 006/393] [ci skip] Translation update --- .../components/axis/.translations/lb.json | 3 ++- .../components/brother/.translations/da.json | 1 + .../components/brother/.translations/en.json | 1 + .../components/brother/.translations/lb.json | 23 +++++++++++++++++++ .../components/brother/.translations/pl.json | 1 + .../components/brother/.translations/ru.json | 1 + .../components/brother/.translations/sl.json | 23 +++++++++++++++++++ .../components/deconz/.translations/lb.json | 8 ++++++- .../components/deconz/.translations/pl.json | 8 ++++++- .../components/deconz/.translations/ru.json | 8 ++++++- .../components/deconz/.translations/sl.json | 8 ++++++- .../components/gios/.translations/da.json | 3 +++ .../components/gios/.translations/en.json | 3 +++ .../components/gios/.translations/lb.json | 20 ++++++++++++++++ .../components/gios/.translations/pl.json | 3 +++ .../components/local_ip/.translations/lb.json | 16 +++++++++++++ .../components/sentry/.translations/lb.json | 18 +++++++++++++++ 17 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/brother/.translations/lb.json create mode 100644 homeassistant/components/brother/.translations/sl.json create mode 100644 homeassistant/components/gios/.translations/lb.json create mode 100644 homeassistant/components/local_ip/.translations/lb.json create mode 100644 homeassistant/components/sentry/.translations/lb.json diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 24ee0e24125..589932cd68e 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert", "bad_config_file": "Feelerhaft Donn\u00e9e\u00eb aus der Konfiguratioun's Datei", "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", - "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat" + "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat", + "updated_configuration": "Konfiguratioun vum Apparat gouf mat der neier Adress aktualis\u00e9iert" }, "error": { "already_configured": "Apparat ass scho konfigur\u00e9iert", diff --git a/homeassistant/components/brother/.translations/da.json b/homeassistant/components/brother/.translations/da.json index 70b5857796b..2ec79228194 100644 --- a/homeassistant/components/brother/.translations/da.json +++ b/homeassistant/components/brother/.translations/da.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Denne printer er allerede konfigureret.", "unsupported_model": "Denne printermodel underst\u00f8ttes ikke." }, "error": { diff --git a/homeassistant/components/brother/.translations/en.json b/homeassistant/components/brother/.translations/en.json index b9b3bd55651..d586bcea1f8 100644 --- a/homeassistant/components/brother/.translations/en.json +++ b/homeassistant/components/brother/.translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "This printer is already configured.", "unsupported_model": "This printer model is not supported." }, "error": { diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json new file mode 100644 index 00000000000..e9ffc2c4da7 --- /dev/null +++ b/homeassistant/components/brother/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "D\u00ebse Printer Modell g\u00ebtt net \u00ebnnerst\u00ebtzt." + }, + "error": { + "connection_error": "Feeler bei der Verbindung.", + "snmp_error": "SNMP Server ausgeschalt oder Printer net \u00ebnnerst\u00ebtzt.", + "wrong_host": "Ong\u00ebltege Numm oder IP Adresse" + }, + "step": { + "user": { + "data": { + "host": "Printer Numm oder IP Adresse", + "type": "Typ vum Printer" + }, + "description": "Brother Printer Integratioun ariichten. Am Fall vun Problemer kuckt op: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + } + }, + "title": "Brother Printer" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/pl.json b/homeassistant/components/brother/.translations/pl.json index 658c54354ac..14fe4024f34 100644 --- a/homeassistant/components/brother/.translations/pl.json +++ b/homeassistant/components/brother/.translations/pl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ta drukarka jest ju\u017c skonfigurowana.", "unsupported_model": "Ten model drukarki nie jest obs\u0142ugiwany." }, "error": { diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json index 995ddeec3d4..8bce23e5292 100644 --- a/homeassistant/components/brother/.translations/ru.json +++ b/homeassistant/components/brother/.translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "error": { diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json new file mode 100644 index 00000000000..99caf69a86f --- /dev/null +++ b/homeassistant/components/brother/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "Ta model tiskalnika ni podprt." + }, + "error": { + "connection_error": "Napaka v povezavi.", + "snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.", + "wrong_host": "Neveljavno ime gostitelja ali IP naslov." + }, + "step": { + "user": { + "data": { + "host": "Gostiteljsko ime tiskalnika ali naslov IP", + "type": "Vrsta tiskalnika" + }, + "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother", + "title": "Brother Tiskalnik" + } + }, + "title": "Brother Tiskalnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 07f88732c62..4b04cfa03ce 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", "remote_double_tap": "Apparat \"{subtype}\" zwee mol gedr\u00e9ckt", + "remote_double_tap_any_side": "Apparat gouf 2 mol ugetippt op enger S\u00e4it", "remote_falling": "Apparat am fr\u00e4ie Fall", + "remote_flip_180_degrees": "Apparat \u00ebm 180 Grad gedr\u00e9int", + "remote_flip_90_degrees": "Apparat \u00ebm 90 Grad gedr\u00e9int", "remote_gyro_activated": "Apparat ger\u00ebselt", "remote_moved": "Apparat beweegt mat \"{subtype}\" erop", + "remote_moved_any_side": "Apparat gouf mat enger S\u00e4it bewegt", "remote_rotate_from_side_1": "Apparat rot\u00e9iert vun der \"S\u00e4it 1\" op \"{subtype}\"", "remote_rotate_from_side_2": "Apparat rot\u00e9iert vun der \"S\u00e4it 2\" op \"{subtype}\"", "remote_rotate_from_side_3": "Apparat rot\u00e9iert vun der \"S\u00e4it 3\" op \"{subtype}\"", "remote_rotate_from_side_4": "Apparat rot\u00e9iert vun der \"S\u00e4it 4\" op \"{subtype}\"", "remote_rotate_from_side_5": "Apparat rot\u00e9iert vun der \"S\u00e4it 5\" op \"{subtype}\"", - "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"" + "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"", + "remote_turned_clockwise": "Apparat mam Auere Wee gedr\u00e9int", + "remote_turned_counter_clockwise": "Apparat g\u00e9int den Auere Wee gedr\u00e9int" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index eafecf87d03..df85e7b8d1d 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", + "remote_double_tap_any_side": "urz\u0105dzenie dwukrotnie pukni\u0119te z dowolnej strony", "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", + "remote_flip_180_degrees": "urz\u0105dzenie odwr\u00f3cone o 180 stopni", + "remote_flip_90_degrees": "urz\u0105dzenie odwr\u00f3cone o 90 stopni", "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "remote_moved": "urz\u0105dzenie poruszone z \"{subtype}\" w g\u00f3r\u0119", + "remote_moved_any_side": "urz\u0105dzenie przesuni\u0119te dowoln\u0105 stron\u0105 do g\u00f3ry", "remote_rotate_from_side_1": "urz\u0105dzenie obr\u00f3cone ze \"strona 1\" na \"{subtype}\"", "remote_rotate_from_side_2": "urz\u0105dzenie obr\u00f3cone ze \"strona 2\" na \"{subtype}\"", "remote_rotate_from_side_3": "urz\u0105dzenie obr\u00f3cone ze \"strona 3\" na \"{subtype}\"", "remote_rotate_from_side_4": "urz\u0105dzenie obr\u00f3cone ze \"strona 4\" na \"{subtype}\"", "remote_rotate_from_side_5": "urz\u0105dzenie obr\u00f3cone ze \"strona 5\" na \"{subtype}\"", - "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"" + "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"", + "remote_turned_clockwise": "urz\u0105dzenie obr\u00f3cone zgodnie z ruchem wskaz\u00f3wek zegara", + "remote_turned_counter_clockwise": "urz\u0105dzenie obr\u00f3cone przeciwnie do ruchu wskaz\u00f3wek zegara" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 57ec138c402..87c3fb646f2 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \"{subtype}\" \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_double_tap_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", + "remote_flip_180_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", + "remote_flip_90_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438, \u043a\u043e\u0433\u0434\u0430 \"{subtype}\" \u0441\u0432\u0435\u0440\u0445\u0443", + "remote_moved_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438", "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 1 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 2 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 3 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 4 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 5 \u043d\u0430 \"{subtype}\"", - "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"" + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"", + "remote_turned_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435", + "remote_turned_counter_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index ca4c6a3d636..385de6f0f01 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", "remote_double_tap": "Naprava \"{subtype}\" dvakrat dotaknjena", + "remote_double_tap_any_side": "Naprava je bila dvojno tapnjena na katerokoli stran", "remote_falling": "Naprava v prostem padu", + "remote_flip_180_degrees": "Naprava se je obrnila za 180 stopinj", + "remote_flip_90_degrees": "Naprava se je obrnila za 90 stopinj", "remote_gyro_activated": "Naprava se je pretresla", "remote_moved": "Naprava je premaknjena s \"{subtype}\" navzgor", + "remote_moved_any_side": "Naprava se je premikala s katero koli stranjo navzgor", "remote_rotate_from_side_1": "Naprava je zasukana iz \"strani 1\" v \"{subtype}\"", "remote_rotate_from_side_2": "Naprava je zasukana iz \"strani 2\" v \"{subtype}\"", "remote_rotate_from_side_3": "Naprava je zasukana iz \"strani 3\" v \"{subtype}\"", "remote_rotate_from_side_4": "Naprava je zasukana iz \"strani 4\" v \"{subtype}\"", "remote_rotate_from_side_5": "Naprava je zasukana iz \"strani 5\" v \"{subtype}\"", - "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"" + "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"", + "remote_turned_clockwise": "Naprava se je obrnila v smeri urinega kazalca", + "remote_turned_counter_clockwise": "Naprava se je obrnila v nasprotni smeri urinega kazalca" } }, "options": { diff --git a/homeassistant/components/gios/.translations/da.json b/homeassistant/components/gios/.translations/da.json index b4855da7951..bd0e947f1dc 100644 --- a/homeassistant/components/gios/.translations/da.json +++ b/homeassistant/components/gios/.translations/da.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a-integration for denne m\u00e5lestation er allerede konfigureret." + }, "error": { "cannot_connect": "Kan ikke oprette forbindelse til GIO\u015a-serveren.", "invalid_sensors_data": "Ugyldige sensordata for denne m\u00e5lestation.", diff --git a/homeassistant/components/gios/.translations/en.json b/homeassistant/components/gios/.translations/en.json index 2ff0d8c60f3..0a85aaa9d15 100644 --- a/homeassistant/components/gios/.translations/en.json +++ b/homeassistant/components/gios/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a integration for this measuring station is already configured." + }, "error": { "cannot_connect": "Cannot connect to the GIO\u015a server.", "invalid_sensors_data": "Invalid sensors' data for this measuring station.", diff --git a/homeassistant/components/gios/.translations/lb.json b/homeassistant/components/gios/.translations/lb.json new file mode 100644 index 00000000000..ed42ad3a7ae --- /dev/null +++ b/homeassistant/components/gios/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Konnt sech net mam GIO\u015a Server verbannen.", + "invalid_sensors_data": "Ong\u00eblteg Sensor Donn\u00e9e\u00eb fir d\u00ebs Miess Statioun", + "wrong_station_id": "ID vun der Miess Statioun ass net korrekt." + }, + "step": { + "user": { + "data": { + "name": "Numm vun der Integratioun", + "station_id": "ID vun der Miess Statioun" + }, + "description": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz) Loft Qualit\u00e9it Integratioun ariichten. Fir w\u00e9ider H\u00ebllef mat der Konfiuratioun kuckt hei: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/pl.json b/homeassistant/components/gios/.translations/pl.json index d3623004fba..677762c2930 100644 --- a/homeassistant/components/gios/.translations/pl.json +++ b/homeassistant/components/gios/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Integracja GIO\u015a dla tej stacji pomiarowej jest ju\u017c skonfigurowana." + }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem GIO\u015a.", "invalid_sensors_data": "Nieprawid\u0142owe dane sensor\u00f3w dla tej stacji pomiarowej.", diff --git a/homeassistant/components/local_ip/.translations/lb.json b/homeassistant/components/local_ip/.translations/lb.json new file mode 100644 index 00000000000..aa249f184ce --- /dev/null +++ b/homeassistant/components/local_ip/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integratioun ass scho konfigur\u00e9iert mat engem Sensor mat deem Numm" + }, + "step": { + "user": { + "data": { + "name": "Numm vum Sensor" + }, + "title": "Lokal IP Adresse" + } + }, + "title": "Lokal IP Adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/lb.json b/homeassistant/components/sentry/.translations/lb.json new file mode 100644 index 00000000000..e91f57a1585 --- /dev/null +++ b/homeassistant/components/sentry/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ass scho konfigur\u00e9iert" + }, + "error": { + "bad_dsn": "Ong\u00eblteg DSN", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "description": "Gitt \u00e4r Sentry DSN un", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file From fe0b537291c153dd58bb3a190f3d9b7c803dbf3e Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Wed, 8 Jan 2020 21:03:26 -0800 Subject: [PATCH 007/393] Template alarm panel (#30487) * Added support for template alarm panel * Rewrote tests in new async format * Fix stale docstring * Update to tests, standardization on NAME vs FRIENDLY_NAME --- .../template/alarm_control_panel.py | 283 +++++++++ .../template/test_alarm_control_panel.py | 556 ++++++++++++++++++ 2 files changed, 839 insertions(+) create mode 100644 homeassistant/components/template/alarm_control_panel.py create mode 100644 tests/components/template/test_alarm_control_panel.py 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/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 From 260596d11be787e56dc37594bc196dd455cd7b86 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 8 Jan 2020 23:51:30 -0600 Subject: [PATCH 008/393] Fix upnp raw sensor state formatting when None (#30444) --- homeassistant/components/upnp/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 06e4a86401f..81fd5c025b9 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -126,6 +126,9 @@ class RawUPnPIGDSensor(UpnpSensor): @property def state(self) -> str: """Return the state of the device.""" + if self._state is None: + return None + return format(self._state, "d") @property From 6b7be35f4a10733fad85fc16a65bd39e02059c2d Mon Sep 17 00:00:00 2001 From: Watchfox <45469709+Watchfox@users.noreply.github.com> Date: Thu, 9 Jan 2020 08:40:10 +0100 Subject: [PATCH 009/393] Fix aurora sensor not converting latitude and longitude correctly (#28643) --- .../components/aurora/binary_sensor.py | 19 +- tests/components/aurora/test_binary_sensor.py | 6 +- tests/fixtures/aurora.txt | 479 +++++++++--------- 3 files changed, 262 insertions(+), 242 deletions(-) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 454c3ad2405..f8038bcced3 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,5 +1,6 @@ """Support for aurora forecast data sensor.""" from datetime import timedelta +from math import floor import logging from aiohttp.hdrs import USER_AGENT @@ -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/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/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 From 290043aed6f6e83b4bf4b1b3321b3ff7d16e9ce0 Mon Sep 17 00:00:00 2001 From: Olen Date: Thu, 9 Jan 2020 09:42:18 +0100 Subject: [PATCH 010/393] Add neato boundary name to state if it exists (#29915) * Add boundary name to state if it exists If the robot is cleaning a pre defined area with a name, add the name to the state-attribute. * Reformat patch * Removing whitespace * Even more formatting That black did not catch on first run... --- homeassistant/components/neato/vacuum.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index d8a3e4ded45..92e1539da4f 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -208,6 +208,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: From 9ebf5ea413a28cf71715ff09a8689ac6987eddd0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 9 Jan 2020 10:25:03 +0100 Subject: [PATCH 011/393] Fix aurora import order (#30606) --- homeassistant/components/aurora/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index f8038bcced3..6963d836685 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -1,7 +1,7 @@ """Support for aurora forecast data sensor.""" from datetime import timedelta -from math import floor import logging +from math import floor from aiohttp.hdrs import USER_AGENT import requests From a4c1114c8eabd57d6b9adf47cc7f8cf11437093e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 9 Jan 2020 11:09:34 +0100 Subject: [PATCH 012/393] Set body size for Proxy / streams to 16mb (#30608) --- homeassistant/components/http/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c720d134c9f..0d93461f90f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -54,6 +54,8 @@ DEFAULT_DEVELOPMENT = "0" DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 +MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 + HTTP_SCHEMA = vol.Schema( { @@ -188,7 +190,9 @@ class HomeAssistantHTTP: ssl_profile, ): """Initialize the HTTP Home Assistant server.""" - app = self.app = web.Application(middlewares=[]) + app = self.app = web.Application( + middlewares=[], client_max_size=MAX_CLIENT_SIZE + ) app[KEY_HASS] = hass # This order matters From a99135a09e6ddfd2d9c05b0f9aa8e67c841a0ee4 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 9 Jan 2020 12:36:32 +0000 Subject: [PATCH 013/393] tweak honeywell manifest (#30612) --- homeassistant/components/honeywell/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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": [], From 4149bd653de03db842591cc874c170cf33eaf970 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 10 Jan 2020 00:03:27 +1100 Subject: [PATCH 014/393] Fix statistics sensor honouring max_age (#27372) * added update listener if max_age is set * remove commented out code * streamline test code * schedule next update based on the next state to expire * fixed update process * isort * fixed callback function * fixed log message * removed logging from test case --- homeassistant/components/statistics/sensor.py | 35 ++++++++++- tests/components/statistics/test_sensor.py | 58 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) 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/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 = { From 3a3d448f17df26f9772c6438a8a2153365f0ab17 Mon Sep 17 00:00:00 2001 From: William Sutton Date: Thu, 9 Jan 2020 09:25:19 -0500 Subject: [PATCH 015/393] Add preset scheduling to radiothermostat (#29847) * Added preset scheduling to radiothermostat. Added alternate scheduling & religious scheduling to climate/const.py * Fix Flake8 Errors in climate.py * Fixing more flake8 errors in climate.py Removed duplicate set_preset_mode def * Fixed more flake8 errors. Please be the end of these errors. * Fixed black formatting * Fixed black, broke flake8, fixed flake8 * Fixed CODE_TO_FAN_STATE black error * Fixed isort issues * Local isort broke black formatting Docs should run isort before black. Default isort will undo certain black formatting. * Removed last commas from imports * Added removed line * Fixed formatting Hopefully this is what the CI pipeline is looking for. * Ran isort from git repo root, utilizing setup.cfg. * One more try * fixed added definition and fixed logger string * fixed formatting * lost a close-paren * Update const.py Removed radiotherm specific presets * Update climate.py Moved preset definitions into radiotherm climate.py --- homeassistant/components/climate/const.py | 1 - .../components/radiotherm/climate.py | 48 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) 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/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, + ) From f878fabd09be579a43702ddd9ada3f67a43e5ea3 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 9 Jan 2020 19:17:16 +0300 Subject: [PATCH 016/393] Fix TOD component incorrectly determining the state between sunrise and sunset (#30199) * TOD fix * Comment added * Review * Review * Review --- homeassistant/components/tod/binary_sensor.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 From 208a123c47e35f3b45de81b84025e28888d227b5 Mon Sep 17 00:00:00 2001 From: Davide Varricchio <45564538+bannhead@users.noreply.github.com> Date: Thu, 9 Jan 2020 19:07:23 +0100 Subject: [PATCH 017/393] Update PYAEHW4A1 to version 0.3.4 (#30616) * Update manifest.json * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/hisense_aehw4a1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 2163689bfb6..fe14f075618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1122,7 +1122,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99c72dff746..59001b5e1cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,7 +390,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 From 669844e4ddc7c3733ef03e4baf0e3ba6d1c5c3a6 Mon Sep 17 00:00:00 2001 From: Daniel Lashua Date: Thu, 9 Jan 2020 13:45:42 -0600 Subject: [PATCH 018/393] Check netgear device_tracker link_rate to ensure device is connected (#30581) * check link_rate to ensure device is connected * black --- homeassistant/components/netgear/device_tracker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 From 7052cdded18ccc7efb6e0f95af4e378eb8d8d292 Mon Sep 17 00:00:00 2001 From: tiagofreire-pt <41837236+tiagofreire-pt@users.noreply.github.com> Date: Thu, 9 Jan 2020 22:49:13 +0000 Subject: [PATCH 019/393] Change nomenclature for Roborock fan speeds (#30614) * Change nomenclature for Roborock fan speeds * Update test_vacuum.py * Update test_vacuum.py --- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) 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/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 From 955aa1de39894dab0339ff2378914f2d60f6fb18 Mon Sep 17 00:00:00 2001 From: Nathan <8044836+nemccarthy@users.noreply.github.com> Date: Fri, 10 Jan 2020 10:20:16 +1100 Subject: [PATCH 020/393] =?UTF-8?q?Add=20INFO=20logging=20to=20generic=5Ft?= =?UTF-8?q?hermostat=20component=20for=20keep-alive=20turn=20=E2=80=A6=20(?= =?UTF-8?q?#28740)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add INFO logging to generic_thermostat component for keep-alive turn on/off * run black --- homeassistant/components/generic_thermostat/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 58514934fc7..e179b576f70 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 From 4fb36451c2008f0e2644a2b00b232bf5c0f5c70b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 10 Jan 2020 00:31:50 +0000 Subject: [PATCH 021/393] [ci skip] Translation update --- .../components/adguard/.translations/ko.json | 2 +- .../components/axis/.translations/it.json | 3 ++- .../components/brother/.translations/it.json | 24 +++++++++++++++++++ .../components/brother/.translations/no.json | 1 + .../brother/.translations/zh-Hant.json | 1 + .../components/deconz/.translations/it.json | 8 ++++++- .../components/deconz/.translations/no.json | 8 ++++++- .../deconz/.translations/zh-Hant.json | 8 ++++++- .../components/gios/.translations/it.json | 23 ++++++++++++++++++ .../components/gios/.translations/no.json | 3 +++ .../components/gios/.translations/ru.json | 3 +++ .../gios/.translations/zh-Hant.json | 3 +++ .../components/local_ip/.translations/it.json | 16 +++++++++++++ .../components/sentry/.translations/it.json | 18 ++++++++++++++ 14 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/brother/.translations/it.json create mode 100644 homeassistant/components/gios/.translations/it.json create mode 100644 homeassistant/components/local_ip/.translations/it.json create mode 100644 homeassistant/components/sentry/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index e1f39259292..02bbb75cd2b 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home" }, "user": { diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 3f303140c68..9e2eecf5747 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "bad_config_file": "Dati errati dal file di configurazione", "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", - "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" + "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis", + "updated_configuration": "Configurazione del dispositivo aggiornata con nuovo indirizzo host" }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", diff --git a/homeassistant/components/brother/.translations/it.json b/homeassistant/components/brother/.translations/it.json new file mode 100644 index 00000000000..43bdb7aec7b --- /dev/null +++ b/homeassistant/components/brother/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Questa stampante \u00e8 gi\u00e0 configurata.", + "unsupported_model": "Questo modello di stampante non \u00e8 supportato." + }, + "error": { + "connection_error": "Errore di connessione.", + "snmp_error": "Server SNMP spento o stampante non supportata.", + "wrong_host": "Nome host o indirizzo IP non valido." + }, + "step": { + "user": { + "data": { + "host": "Nome host o indirizzo IP della stampante", + "type": "Tipo di stampante" + }, + "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" + } + }, + "title": "Stampante Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/no.json b/homeassistant/components/brother/.translations/no.json index 57cfd03d216..d4cf935f156 100644 --- a/homeassistant/components/brother/.translations/no.json +++ b/homeassistant/components/brother/.translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Denne skriveren er allerede konfigurert.", "unsupported_model": "Denne skrivermodellen er ikke st\u00f8ttet." }, "error": { diff --git a/homeassistant/components/brother/.translations/zh-Hant.json b/homeassistant/components/brother/.translations/zh-Hant.json index 0ee27bf77d4..cff89ea38ca 100644 --- a/homeassistant/components/brother/.translations/zh-Hant.json +++ b/homeassistant/components/brother/.translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 99e5622129f..980409d6987 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", + "remote_double_tap_any_side": "Dispositivo toccato due volte su qualsiasi lato", "remote_falling": "Dispositivo in caduta libera", + "remote_flip_180_degrees": "Dispositivo capovolto di 180 gradi", + "remote_flip_90_degrees": "Dispositivo capovolto di 90 gradi", "remote_gyro_activated": "Dispositivo in vibrazione", "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", + "remote_moved_any_side": "Dispositivo spostato con qualsiasi lato verso l'alto", "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"", "remote_rotate_from_side_3": "Dispositivo ruotato da \"lato 3\" a \"{subtype}\"", "remote_rotate_from_side_4": "Dispositivo ruotato da \"lato 4\" a \"{subtype}\"", "remote_rotate_from_side_5": "Dispositivo ruotato da \"lato 5\" a \"{subtype}\"", - "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"" + "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo ruotato in senso orario", + "remote_turned_counter_clockwise": "Dispositivo ruotato in senso antiorario" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 6d1969e98a7..d6133542c64 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_double_tap": "Enheten \" {subtype} \" dobbeltklikket", + "remote_double_tap_any_side": "Enheten dobbeltklikket p\u00e5 alle sider", "remote_falling": "Enheten er i fritt fall", + "remote_flip_180_degrees": "Enheten er snudd 180 grader", + "remote_flip_90_degrees": "Enheten er snudd 90 grader", "remote_gyro_activated": "Enhet er ristet", "remote_moved": "Enheten ble flyttet med \"{under type}\" opp", + "remote_moved_any_side": "Enheten flyttet med alle sider opp", "remote_rotate_from_side_1": "Enheten rotert fra \"side 1\" til \" {subtype} \"", "remote_rotate_from_side_2": "Enheten rotert fra \"side 2\" til \" {subtype} \"", "remote_rotate_from_side_3": "Enheten rotert fra \"side 3\" til \" {subtype} \"", "remote_rotate_from_side_4": "Enheten rotert fra \"side 4\" til \" {subtype} \"", "remote_rotate_from_side_5": "Enheten rotert fra \"side 5\" til \" {subtype} \"", - "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"" + "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"", + "remote_turned_clockwise": "Enheten dreide med klokken", + "remote_turned_counter_clockwise": "Enheten dreide mot klokken" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 4df506ad76f..96ab68a8dbb 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", + "remote_double_tap_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u96d9\u9ede\u9078", "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", + "remote_flip_180_degrees": "\u8a2d\u5099\u65cb\u8f49 180 \u5ea6", + "remote_flip_90_degrees": "\u8a2d\u5099\u65cb\u8f49 90 \u5ea6", "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_moved_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u671d\u4e0a", "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d" + "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_turned_clockwise": "\u8a2d\u5099\u9806\u6642\u91dd\u65cb\u8f49", + "remote_turned_counter_clockwise": "\u8a2d\u5099\u9006\u6642\u91dd\u65cb\u8f49" } }, "options": { diff --git a/homeassistant/components/gios/.translations/it.json b/homeassistant/components/gios/.translations/it.json new file mode 100644 index 00000000000..b3d1b9a71cf --- /dev/null +++ b/homeassistant/components/gios/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione GIO\u015a per questa stazione di misurazione \u00e8 gi\u00e0 configurata." + }, + "error": { + "cannot_connect": "Impossibile connettersi al server GIO\u015a.", + "invalid_sensors_data": "Dati dei sensori non validi per questa stazione di misura.", + "wrong_station_id": "L'ID della stazione di misura non \u00e8 corretto." + }, + "step": { + "user": { + "data": { + "name": "Nome dell'integrazione", + "station_id": "ID della stazione di misura" + }, + "description": "Impostare l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/no.json b/homeassistant/components/gios/.translations/no.json index 3abfe3bfbb8..b045c51e563 100644 --- a/homeassistant/components/gios/.translations/no.json +++ b/homeassistant/components/gios/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a-integrasjon for denne m\u00e5lestasjonen er allerede konfigurert." + }, "error": { "cannot_connect": "Kan ikke koble til GIO\u015a-tjeneren", "invalid_sensors_data": "Ugyldig sensordata for denne m\u00e5lestasjonen", diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json index ea2c2997d4d..69ffff98517 100644 --- a/homeassistant/components/gios/.translations/ru.json +++ b/homeassistant/components/gios/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\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 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.", "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", diff --git a/homeassistant/components/gios/.translations/zh-Hant.json b/homeassistant/components/gios/.translations/zh-Hant.json index 19d13572c72..3f10f2eb37b 100644 --- a/homeassistant/components/gios/.translations/zh-Hant.json +++ b/homeassistant/components/gios/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u6b64 GIO\u015a \u76e3\u6e2c\u7ad9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 GIO\u015a \u4f3a\u670d\u5668\u3002", "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", diff --git a/homeassistant/components/local_ip/.translations/it.json b/homeassistant/components/local_ip/.translations/it.json new file mode 100644 index 00000000000..a33e892c6ec --- /dev/null +++ b/homeassistant/components/local_ip/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con questo nome" + }, + "step": { + "user": { + "data": { + "name": "Nome del sensore" + }, + "title": "Indirizzo IP locale" + } + }, + "title": "Indirizzo IP locale" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/it.json b/homeassistant/components/sentry/.translations/it.json new file mode 100644 index 00000000000..4d0cd3178e7 --- /dev/null +++ b/homeassistant/components/sentry/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry \u00e8 gi\u00e0 configurato" + }, + "error": { + "bad_dsn": "DSN non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "description": "Inserisci il tuo DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file From ef05aa2f39f07068f033900171779f4916a5a3e9 Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 10 Jan 2020 03:19:10 +0100 Subject: [PATCH 022/393] Add Samsung TV config flow (#28306) * add config flow * add tests * add user step error handling * remove unload function * add missing test file * handle authentication correctly * remove old discovery mode * better handling of remote class * optimized abort messages * add already configured test for user flow * Import order * use ip property instead context * Black * small syntax * use snake_case * Revert "use ip property instead context" This reverts commit 91502407eb216f8a0b1b90e3e6fb165b81406f8f. * disable wrong pylint errors * disable wrong no-member * Try to fix review comments * Try to fix review comments * Fix missing self * Fix ip checks * methods to functions * simplify user check * remove user errors * use async_setup for config * fix after rebase * import config to user config flow * patch all samsungctl * fix after rebase * fix notes * remove unused variable * ignore old setup function * fix after merge * pass configuration to import step * isort * fix recursion * remove timeout config * add turn on action (dry without testing) * use upstream checks * cleanup * minor * correctly await async method * ignore unused import * async call send_key * Revert "async call send_key" This reverts commit f37057819fd751a654779da743d0300751e963be. * fix comments * fix timeout test * test turn on action * Update media_player.py * Update test_media_player.py * Update test_media_player.py * use async executor * use newer ssdp data * update manually configured with ssdp data * dont setup component directly * ensure list * check updated device info * Update config_flow.py * Update __init__.py * fix duplicate check * simplified unique check * move method detection to config_flow * move unique test to init * fix after real world test * optimize config_validation * update device_info on ssdp discovery * cleaner update listener * fix lint * fix method signature * add note for manual config to confirm message * fix turn_on_action * pass script * patch delay * remove device info update --- .../components/discovery/__init__.py | 1 - .../components/samsungtv/__init__.py | 59 +++ .../components/samsungtv/config_flow.py | 184 +++++++++ homeassistant/components/samsungtv/const.py | 8 + .../components/samsungtv/manifest.json | 14 +- .../components/samsungtv/media_player.py | 175 ++------ .../components/samsungtv/strings.json | 26 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 1 - requirements_test_all.txt | 1 - .../components/samsungtv/test_config_flow.py | 388 ++++++++++++++++++ tests/components/samsungtv/test_init.py | 97 +++++ .../components/samsungtv/test_media_player.py | 333 ++++----------- 14 files changed, 896 insertions(+), 397 deletions(-) create mode 100644 homeassistant/components/samsungtv/config_flow.py create mode 100644 homeassistant/components/samsungtv/strings.json create mode 100644 tests/components/samsungtv/test_config_flow.py create mode 100644 tests/components/samsungtv/test_init.py 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/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 6b4f0e31f02..5647b407bfb 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1 +1,60 @@ """The Samsung TV integration.""" +import socket + +import voluptuous as vol + +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: + for entry_config in config[DOMAIN]: + 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, "media_player") + ) + + return True diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py new file mode 100644 index 00000000000..0bf39cc248b --- /dev/null +++ b/homeassistant/components/samsungtv/config_flow.py @@ -0,0 +1,184 @@ +"""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_FRIENDLY_NAME, + 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, + CONF_ON_ACTION, + 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_FOUND = "not_found" +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._on_script = None + self._port = None + self._title = None + self._uuid = None + + def _get_entry(self): + return self.async_create_entry( + title=self._title, + data={ + CONF_HOST: self._host, + CONF_ID: self._uuid, + CONF_IP_ADDRESS: self._ip, + CONF_MANUFACTURER: self._manufacturer, + CONF_METHOD: self._method, + CONF_MODEL: self._model, + CONF_NAME: self._name, + CONF_ON_ACTION: self._on_script, + 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, + "timeout": 1, + } + 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): + LOGGER.debug("Failing config: %s", config) + + LOGGER.debug("No working config found") + return RESULT_NOT_FOUND + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + self._on_script = user_input.get(CONF_ON_ACTION) + 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._title = user_input.get(CONF_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[ATTR_UPNP_MANUFACTURER] + self._model = user_input[ATTR_UPNP_MODEL_NAME] + self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] + if self._name.startswith("[TV]"): + self._name = self._name[4:] + self._title = f"{self._name} ({self._model})" + self._uuid = user_input[ATTR_UPNP_UDN] + if self._uuid.startswith("uuid:"): + self._uuid = self._uuid[5:] + + config_entry = await self.async_set_unique_id(ip_address) + if config_entry: + config_entry.data[CONF_ID] = self._uuid + 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") + + 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} + ) diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py index 83d74743844..7cf71e406cb 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 Remote" + +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..0d0a360fc20 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": [ + { + "deviceType": "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..e7153a7f5d4 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,20 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( - CONF_BROADCAST_ADDRESS, CONF_HOST, - CONF_MAC, - CONF_NAME, + CONF_ID, + CONF_METHOD, 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 +49,33 @@ 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.""" + turn_on_action = config_entry.data.get(CONF_ON_ACTION) + on_script = Script(hass, turn_on_action) if turn_on_action else None + 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._name = config_entry.title + self._uuid = config_entry.data.get(CONF_ID) + self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) + self._model = config_entry.data.get(CONF_MODEL) + self._on_script = on_script + self._update_listener = None # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -141,57 +88,20 @@ class SamsungTVDevice(MediaPlayerDevice): # Generate a configuration for the Samsung library self._config = { "name": "HomeAssistant", - "description": name, + "description": self._name, "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") 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()) @@ -219,9 +129,6 @@ class SamsungTVDevice(MediaPlayerDevice): # 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 @@ -256,6 +163,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 +186,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 +261,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..ee762503e5c --- /dev/null +++ b/homeassistant/components/samsungtv/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "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 authentication.", + "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 authentication. 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 authenticated to connect to this Samsung TV.", + "not_found": "No supported Samsung TV devices found on the network.", + "not_supported": "This Samsung TV devices is currently not supported." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6f3f0e714f6..c5ea3f1a5d9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ FLOWS = [ "point", "ps4", "rainmachine", + "samsungtv", "sentry", "simplisafe", "smartthings", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index adf3a345bbe..01e0726ce54 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -27,6 +27,11 @@ SSDP = { "manufacturer": "Royal Philips Electronics" } ], + "samsungtv": [ + { + "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1" + } + ], "sonos": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/requirements_all.txt b/requirements_all.txt index fe14f075618..9fc7a4f27dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59001b5e1cb..e38912df53f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,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 diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py new file mode 100644 index 00000000000..ce6741f0703 --- /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": 1, +} +AUTODETECT_LEGACY = { + "name": "HomeAssistant", + "description": "HomeAssistant", + "id": "ha.component.samsung", + "method": "legacy", + "port": None, + "host": "fake_host", + "timeout": 1, +} + + +@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] is None + 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_found(hass): + """Test starting a flow by user but no device found.""" + with patch( + "homeassistant.components.samsungtv.config_flow.Remote", + side_effect=OSError("Boom"), + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + + # device not found + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == "not_found" + + +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_name (fake_model)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake_name" + 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_name (fake2_model)" + assert result["data"][CONF_HOST] == "fake2_host" + assert result["data"][CONF_NAME] == "fake2_name" + 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_found(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_found" + + +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_found" + 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..55ec52b56ae --- /dev/null +++ b/tests/components/samsungtv/test_init.py @@ -0,0 +1,97 @@ +"""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": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_NAME], + "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"), patch( + "homeassistant.components.samsungtv.config_flow.socket" + ), patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote: + 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..3afedda746e 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,46 @@ 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.socket"), patch( + "homeassistant.components.samsungtv.config_flow.Remote" + ), patch( "homeassistant.components.samsungtv.media_player.SamsungRemote" ) as remote_class, patch( - "homeassistant.components.samsungtv.media_player.socket" - ) as socket_class: + "homeassistant.components.samsungtv.socket" + ): remote = mock.Mock() remote_class.return_value = remote - socket = mock.Mock() - socket_class.return_value = socket 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 +104,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): @@ -254,7 +147,7 @@ async def test_update_off(hass, remote, mock_now): assert state.state == STATE_OFF -async def test_send_key(hass, remote, wakeonlan): +async def test_send_key(hass, remote): """Test for send key.""" await setup_samsungtv(hass, MOCK_CONFIG) assert await hass.services.async_call( @@ -267,85 +160,6 @@ async def test_send_key(hass, remote, wakeonlan): 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) @@ -417,7 +231,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 +239,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 +248,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 +272,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 +297,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): @@ -583,37 +409,20 @@ async def test_media_previous_track(hass, remote): assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")] -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 From d25aa1f1830f1d3fad42dde3f9bdfae2ad98b7f2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 9 Jan 2020 21:53:47 -0500 Subject: [PATCH 023/393] Convert vizio component from sync to async component (#30605) * add device_info property and move component to async * use new VizioAsync class to have proper async support * remove hass from VizioDevice init since it is not needed * update requirements_all * missed type hint * updates based on review * pyvizio version bump * additional fixes based on review * mistake in last commit * remove device_info property because it can't be used unless this integration has config flow support --- homeassistant/components/vizio/__init__.py | 5 +- homeassistant/components/vizio/const.py | 1 - homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 128 +++++++++++------- requirements_all.txt | 2 +- 5 files changed, 86 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 3ffbf46f928..aa6f724bc59 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,4 +1,5 @@ """The vizio component.""" + import voluptuous as vol from homeassistant.const import ( @@ -8,6 +9,7 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_VOLUME_STEP, @@ -17,7 +19,7 @@ from .const import ( ) -def validate_auth(config): +def validate_auth(config: ConfigType) -> ConfigType: """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" token = config.get(CONF_ACCESS_TOKEN) if config[CONF_DEVICE_CLASS] == "tv" and not token: @@ -25,6 +27,7 @@ def validate_auth(config): f"When '{CONF_DEVICE_CLASS}' is 'tv' then '{CONF_ACCESS_TOKEN}' is required.", path=[CONF_ACCESS_TOKEN], ) + return config diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 828c4e600e0..c4c1ba3199b 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -6,7 +6,6 @@ DEFAULT_NAME = "Vizio SmartCast" DEFAULT_VOLUME_STEP = 1 DEFAULT_DEVICE_CLASS = "tv" DEVICE_ID = "pyvizio" -DEVICE_NAME = "Python Vizio" DOMAIN = "vizio" diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 7ae0570fa86..5a4c0f4a4cc 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.0.12"], + "requirements": ["pyvizio==0.0.15"], "dependencies": [], "codeowners": ["@raman325"] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 418cf8e3835..84b745baf92 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,8 +1,10 @@ """Vizio SmartCast Device support.""" + from datetime import timedelta import logging +from typing import Any, Callable, Dict, List -from pyvizio import Vizio +from pyvizio import VizioAsync import voluptuous as vol from homeassistant import util @@ -25,9 +27,12 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import VIZIO_SCHEMA, validate_auth -from .const import CONF_VOLUME_STEP, DEFAULT_NAME, DEVICE_ID, ICON +from .const import CONF_VOLUME_STEP, DEVICE_ID, ICON _LOGGER = logging.getLogger(__name__) @@ -52,32 +57,43 @@ SUPPORTED_COMMANDS = { PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable[[List[Entity], bool], None], + discovery_info: Dict[str, Any] = 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, token, device_type, async_get_clientsession(hass, False) + ) + if not await device.can_connect(): fail_auth_msg = "" if token: - fail_auth_msg = " and auth token is correct" + fail_auth_msg = ", auth token is correct" _LOGGER.error( "Failed to set up Vizio platform, please check if host " - "is valid and available%s", + "is valid and available, device type is correct%s", fail_auth_msg, ) return - add_entities([device], True) + async_add_entities([VizioDevice(device, name, volume_step, device_type)], 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, device: VizioAsync, name: str, volume_step: int, device_type: str + ) -> None: """Initialize Vizio device.""" self._name = name @@ -88,31 +104,32 @@ class VizioDevice(MediaPlayerDevice): 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 = device self._max_volume = float(self._device.get_max_volume()) self._unique_id = None self._icon = ICON[device_type] @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() if not self._unique_id: - self._unique_id = self._device.get_esn() + self._unique_id = await self._device.get_esn() + + is_on = await self._device.get_power_state() if is_on: self._state = STATE_ON - volume = self._device.get_current_volume() + volume = await self._device.get_current_volume() if volume is not None: self._volume_level = float(volume) / self._max_volume - input_ = self._device.get_current_input() + input_ = await self._device.get_current_input() if input_ is not None: self._current_input = input_.meta_name - inputs = self._device.get_inputs() + inputs = await self._device.get_inputs() if inputs is not None: self._available_inputs = [input_.name for input_ in inputs] @@ -127,100 +144,115 @@ class VizioDevice(MediaPlayerDevice): self._available_inputs = None @property - def state(self): + 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 - def turn_on(self): + async def async_turn_on(self) -> None: """Turn the device on.""" - self._device.pow_on() - def turn_off(self): + await self._device.pow_on() + + async def async_turn_off(self) -> None: """Turn the device off.""" - self._device.pow_off() - def mute_volume(self, mute): + await self._device.pow_off() + + 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() - def media_next_track(self): + await self._device.ch_down() + + async def async_media_next_track(self) -> None: """Send next channel command.""" - self._device.ch_up() - def select_source(self, source): + await self._device.ch_up() + + async def async_select_source(self, source: str) -> None: """Select input source.""" - self._device.input_switch(source) - def volume_up(self): + await self._device.input_switch(source) + + 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(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(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) 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) self._volume_level = volume - self._device.vol_down(num=num) diff --git a/requirements_all.txt b/requirements_all.txt index 9fc7a4f27dc..0224969b479 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1696,7 +1696,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.0.12 +pyvizio==0.0.15 # homeassistant.components.velux pyvlx==0.2.12 From ec1109329bb5f23e99ab2ed26c211e08423990f6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 10 Jan 2020 16:57:52 +0100 Subject: [PATCH 024/393] Upgrade praw to 6.5.0 (#30643) --- homeassistant/components/reddit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 0224969b479..05b853d9781 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1028,7 +1028,7 @@ pocketcasts==0.1 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e38912df53f..57086fd5f16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -350,7 +350,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 From 5e3747a05887848f967b45894f8010855e2ea5cc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 10 Jan 2020 16:58:13 +0100 Subject: [PATCH 025/393] Upgrade shodan to 1.21.2 (#30641) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 007f6ef1d99..dac6efdfce2 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.2"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 05b853d9781..473c2420386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ sentry-sdk==0.13.5 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.21.1 +shodan==1.21.2 # homeassistant.components.simplepush simplepush==1.1.4 From 53a42ccd5d286ce57f6d2ed01ed5c90b21ee47b6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 10 Jan 2020 16:59:40 +0100 Subject: [PATCH 026/393] Upgrade pylast to 3.2.0 (#30644) --- homeassistant/components/lastfm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 473c2420386..f2e7d81b7af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1327,7 +1327,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 From d65f2ac31a7f0f51f4e7901d9736af089ae3125e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 10 Jan 2020 12:55:39 -0600 Subject: [PATCH 027/393] Do not save last_seen if older than prev_seen (#30647) Also add warnings when updates skipped similar to google_maps --- .../components/life360/device_tracker.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index b2ba1ca3164..b7b0415a1b3 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -195,7 +195,10 @@ class Life360Scanner: ) reported = False - self._dev_data[dev_id] = last_seen or prev_seen, reported + # Don't remember last_seen unless it's really an update. + if not last_seen or prev_seen and last_seen <= prev_seen: + last_seen = prev_seen + self._dev_data[dev_id] = last_seen, reported return prev_seen @@ -218,7 +221,17 @@ class Life360Scanner: return # Only update when we truly have an update. - if not last_seen or prev_seen and last_seen <= prev_seen: + if not last_seen: + _LOGGER.warning("%s: Ignoring update because timestamp is missing", dev_id) + return + if prev_seen and last_seen < prev_seen: + _LOGGER.warning( + "%s: Ignoring update because timestamp is older than last timestamp", + dev_id, + ) + _LOGGER.debug("%s < %s", last_seen, prev_seen) + return + if last_seen == prev_seen: return lat = loc.get("latitude") From fb2e0593468fa644c76c28ad78cf8efa3fbdf919 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 10 Jan 2020 19:55:49 +0100 Subject: [PATCH 028/393] Upgrade colorlog to 4.1.0 (#30642) --- homeassistant/scripts/check_config.py | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) 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/requirements_all.txt b/requirements_all.txt index f2e7d81b7af..03575074939 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -385,7 +385,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57086fd5f16..f23c944370b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 74cde3de6e5384d6ac3d857c0d75052b14ec57af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 10 Jan 2020 20:56:07 +0200 Subject: [PATCH 029/393] Upgrade pydocstyle to 5.0.2 (#30648) https://github.com/PyCQA/pydocstyle/blob/5.0.2/docs/release_notes.rst --- .pre-commit-config-all.yaml | 2 +- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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 From c13b46173767c47c15e07427584a2e6a8a2b7298 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 10 Jan 2020 19:56:41 +0100 Subject: [PATCH 030/393] Bump Adafruit_BBIO to 1.1.1 (#30630) --- homeassistant/components/bbb_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index e919e0c66bf..42670d510da 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -2,7 +2,7 @@ "domain": "bbb_gpio", "name": "BeagleBone Black GPIO", "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", - "requirements": ["Adafruit_BBIO==1.0.0"], + "requirements": ["Adafruit_BBIO==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 03575074939..0265ce132e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -31,7 +31,7 @@ Adafruit-GPIO==1.0.3 Adafruit-SHT31==1.0.2 # homeassistant.components.bbb_gpio -# Adafruit_BBIO==1.0.0 +# Adafruit_BBIO==1.1.1 # homeassistant.components.homekit HAP-python==2.6.0 From d883ee62f8b216e30898f843fff423ef0364cee4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 10 Jan 2020 19:57:08 +0100 Subject: [PATCH 031/393] deCONZ - Disable daylight sensor by default (#30625) * Dont enable daylight sensor by default * Fix tests --- homeassistant/components/deconz/deconz_device.py | 8 +++++++- tests/components/deconz/test_sensor.py | 12 ++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 68daee6cf26..a9a1e2cdb1f 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/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" From 3348f4f6d186238881dfadb0082f6b63175f919c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jan 2020 19:57:37 +0100 Subject: [PATCH 032/393] Add search integration (#30511) * Add search integration * Add scenes and config entry support * Update comments * Add support for groups * Allow querying config entry * Update manifest * Fix scene tests --- CODEOWNERS | 1 + homeassistant/components/group/__init__.py | 18 ++ .../components/homeassistant/scene.py | 40 ++- homeassistant/components/search/__init__.py | 211 ++++++++++++++++ homeassistant/components/search/manifest.json | 12 + homeassistant/helpers/device_registry.py | 12 + homeassistant/helpers/entity_registry.py | 12 + tests/components/homeassistant/test_scene.py | 49 ++++ tests/components/search/__init__.py | 1 + tests/components/search/test_init.py | 228 ++++++++++++++++++ 10 files changed, 581 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/search/__init__.py create mode 100644 homeassistant/components/search/manifest.json create mode 100644 tests/components/search/__init__.py create mode 100644 tests/components/search/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index b30e15d36ca..fddb106a07a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -281,6 +281,7 @@ 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 diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index c8a138abe41..92d811c06fb 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -183,6 +183,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) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c79c22e36a3..a142c787506 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.""" diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py new file mode 100644 index 00000000000..574fc5ee773 --- /dev/null +++ b/homeassistant/components/search/__init__.py @@ -0,0 +1,211 @@ +"""The Search integration.""" +from collections import defaultdict + +import voluptuous as vol + +from homeassistant.components import group, 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" + + +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"} + + 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 = set() + + @callback + def async_search(self, item_type, item_id): + """Find results.""" + self.results[item_type].add(item_id) + self._to_resolve.add((item_type, item_id)) + + while self._to_resolve: + search_type, search_id = self._to_resolve.pop() + 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. + self.results["entity"] -= self.results["script"] + self.results["entity"] -= self.results["scene"] + self.results["entity"] -= self.results["automation"] + self.results["entity"] -= self.results["group"] + + # Remove entry into graph from search results. + self.results[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.add((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) + + # Extra: Find automations that reference this device + + @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) + + # 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 ("scene", "automation", "script", "group"): + 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. + """ + # Extra: Check with automation integration what entities/devices they reference + + @callback + def _resolve_script(self, script_entity_id) -> None: + """Resolve a script. + + Will only be called if script is an entry point. + """ + # Extra: Check with script integration what entities/devices they reference + + @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..337ce45f9bf --- /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"], + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 512334c8d3c..41c78a2f070 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -375,3 +375,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_registry.py b/homeassistant/helpers/entity_registry.py index 2673162a841..a8a7fdab2c8 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -445,6 +445,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/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index d3bbac44df8..672de5827f1 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 @@ -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 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/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..cce98faa290 --- /dev/null +++ b/tests/components/search/test_init.py @@ -0,0 +1,228 @@ +"""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", + }, + }, + ] + }, + ) + + # 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"}, + } + + 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"), + ): + 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"}, + } + 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" + + +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], + } From 3f29c234b8b2cae1c754bf55cdb2f283f8a62701 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jan 2020 21:35:31 +0100 Subject: [PATCH 033/393] Add Ring config flow (#30564) * Add Ring config flow * Address comments + migrate platforms to config entry * Migrate camera too * Address comments * Fix order config flows * setup -> async_setup --- .../components/ring/.translations/en.json | 28 +++++ homeassistant/components/ring/__init__.py | 112 ++++++++++++++---- .../components/ring/binary_sensor.py | 39 ++---- homeassistant/components/ring/camera.py | 66 ++++------- homeassistant/components/ring/config_flow.py | 105 ++++++++++++++++ homeassistant/components/ring/light.py | 15 ++- homeassistant/components/ring/manifest.json | 3 +- homeassistant/components/ring/sensor.py | 44 +++---- homeassistant/components/ring/strings.json | 27 +++++ homeassistant/components/ring/switch.py | 15 ++- homeassistant/generated/config_flows.py | 1 + tests/components/ring/common.py | 13 +- tests/components/ring/conftest.py | 4 + tests/components/ring/test_binary_sensor.py | 22 +++- tests/components/ring/test_config_flow.py | 58 +++++++++ tests/components/ring/test_init.py | 6 +- tests/components/ring/test_sensor.py | 23 +++- 17 files changed, 435 insertions(+), 146 deletions(-) create mode 100644 homeassistant/components/ring/.translations/en.json create mode 100644 homeassistant/components/ring/config_flow.py create mode 100644 homeassistant/components/ring/strings.json create mode 100644 tests/components/ring/test_config_flow.py diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json new file mode 100644 index 00000000000..db4665b6c0a --- /dev/null +++ b/homeassistant/components/ring/.translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "2fa": { + "data": { + "2fa": "Two-factor code" + }, + "title": "Enter two-factor authentication" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Connect to the device" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a68749b2c67..18c753f4dc9 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,12 +1,16 @@ """Support for Ring Doorbell/Chimes.""" +import asyncio from datetime import timedelta +from functools import partial import logging +from pathlib import Path from requests.exceptions import ConnectTimeout, HTTPError from ring_doorbell import Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant import config_entries +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_time_interval @@ -21,6 +25,7 @@ NOTIFICATION_TITLE = "Ring Setup" DATA_RING_DOORBELLS = "ring_doorbells" DATA_RING_STICKUP_CAMS = "ring_stickup_cams" DATA_RING_CHIMES = "ring_chimes" +DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" DEFAULT_CACHEDB = ".ring_cache.pickle" @@ -29,13 +34,14 @@ SIGNAL_UPDATE_RING = "ring_update" SCAN_INTERVAL = timedelta(seconds=10) +PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") + CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + vol.Optional(DOMAIN): vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, } ) }, @@ -43,27 +49,39 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Ring component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - scan_interval = conf[CONF_SCAN_INTERVAL] + if DOMAIN not in config: + return True + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": config[DOMAIN]["username"], + "password": config[DOMAIN]["password"], + }, + ) + ) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + cache = hass.config.path(DEFAULT_CACHEDB) try: - cache = hass.config.path(DEFAULT_CACHEDB) - ring = Ring(username=username, password=password, cache_file=cache) - if not ring.is_connected: - return False - hass.data[DATA_RING_CHIMES] = chimes = ring.chimes - hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams - - ring_devices = chimes + doorbells + stickup_cams - + ring = await hass.async_add_executor_job( + partial( + Ring, + username=entry.data["username"], + password="invalid-password", + cache_file=cache, + ) + ) except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.create( + hass.components.persistent_notification.async_create( "Error: {}
" "You will need to restart hass after fixing." "".format(ex), @@ -72,6 +90,28 @@ def setup(hass, config): ) return False + if not ring.is_connected: + _LOGGER.error("Unable to connect to Ring service") + return False + + await hass.async_add_executor_job(finish_setup_entry, hass, ring) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +def finish_setup_entry(hass, ring): + """Finish setting up entry.""" + hass.data[DATA_RING_CHIMES] = chimes = ring.chimes + hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells + hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams + + ring_devices = chimes + doorbells + stickup_cams + def service_hub_refresh(service): hub_refresh() @@ -92,6 +132,36 @@ def setup(hass, config): hass.services.register(DOMAIN, "update", service_hub_refresh) # register scan interval for ring - track_time_interval(hass, timer_hub_refresh, scan_interval) + hass.data[DATA_TRACK_INTERVAL] = track_time_interval( + hass, timer_hub_refresh, SCAN_INTERVAL + ) - return True + +async def async_unload_entry(hass, entry): + """Unload Ring 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 + + await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL]) + + hass.services.async_remove(DOMAIN, "update") + + hass.data.pop(DATA_RING_DOORBELLS) + hass.data.pop(DATA_RING_STICKUP_CAMS) + hass.data.pop(DATA_RING_CHIMES) + hass.data.pop(DATA_TRACK_INTERVAL) + + return unload_ok + + +async def async_remove_entry(hass, entry): + """Act when an entry is removed.""" + await hass.async_add_executor_job(Path(hass.config.path(DEFAULT_CACHEDB)).unlink) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 86d26ec25b4..0706752ffb2 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -2,22 +2,10 @@ from datetime import timedelta import logging -import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) -import homeassistant.helpers.config_validation as cv - -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, -) +from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS _LOGGER = logging.getLogger(__name__) @@ -29,35 +17,24 @@ SENSOR_TYPES = { "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for a Ring device.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Ring binary sensors from a config entry.""" ring_doorbells = hass.data[DATA_RING_DOORBELLS] ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] sensors = [] for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "doorbell" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) for device in ring_stickup_cams: # ring.stickup_cams is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingBinarySensor(hass, device, sensor_type)) - add_entities(sensors, True) + async_add_entities(sensors, True) class RingBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 1d2fe6ff67b..a3b34afa056 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -5,13 +5,11 @@ import logging from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame -import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util @@ -20,77 +18,57 @@ from . import ( ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, - NOTIFICATION_ID, SIGNAL_UPDATE_RING, ) -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" - FORCE_REFRESH_INTERVAL = timedelta(minutes=45) _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = "Ring Camera Setup" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" ring_doorbell = hass.data[DATA_RING_DOORBELLS] ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] cams = [] - cams_no_plan = [] for camera in ring_doorbell + ring_stickup_cams: - if camera.has_subscription: - cams.append(RingCam(hass, camera, config)) - else: - cams_no_plan.append(camera) + if not camera.has_subscription: + continue - # show notification for all cameras without an active subscription - if cams_no_plan: - cameras = str(", ".join([camera.name for camera in cams_no_plan])) + camera = await hass.async_add_executor_job(RingCam, hass, camera) + cams.append(camera) - err_msg = ( - """A Ring Protect Plan is required for the""" - """ following cameras: {}.""".format(cameras) - ) - - _LOGGER.error(err_msg) - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(err_msg), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - add_entities(cams, True) - return True + async_add_entities(cams, True) class RingCam(Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera, device_info): + def __init__(self, hass, camera): """Initialize a Ring Door Bell camera.""" super().__init__() self._camera = camera self._hass = hass self._name = self._camera.name self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._last_video_id = self._camera.last_recording_id self._video_url = self._camera.recording_url(self._last_video_id) self._utcnow = dt_util.utcnow() self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): @@ -131,11 +109,7 @@ class RingCam(Camera): return image = await asyncio.shield( - ffmpeg.get_image( - self._video_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, - ) + ffmpeg.get_image(self._video_url, output_format=IMAGE_JPEG,) ) return image @@ -146,7 +120,7 @@ class RingCam(Camera): return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera(self._video_url, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera(self._video_url) try: stream_reader = await stream.get_reader() diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py new file mode 100644 index 00000000000..bdb60cc26c5 --- /dev/null +++ b/homeassistant/components/ring/config_flow.py @@ -0,0 +1,105 @@ +"""Config flow for Ring integration.""" +from functools import partial +import logging + +from oauthlib.oauth2 import AccessDeniedError +from ring_doorbell import Ring +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions + +from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + cache = hass.config.path(DEFAULT_CACHEDB) + + def otp_callback(): + if "2fa" in data: + return data["2fa"] + + raise Require2FA + + try: + ring = await hass.async_add_executor_job( + partial( + Ring, + username=data["username"], + password=data["password"], + cache_file=cache, + auth_callback=otp_callback, + ) + ) + except AccessDeniedError: + raise InvalidAuth + + if not ring.is_connected: + raise InvalidAuth + + +class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ring.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + user_pass = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input["username"]) + + return self.async_create_entry( + title=user_input["username"], + data={"username": user_input["username"]}, + ) + except Require2FA: + self.user_pass = user_input + + return await self.async_step_2fa() + + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({"username": str, "password": str}), + errors=errors, + ) + + async def async_step_2fa(self, user_input=None): + """Handle 2fa step.""" + if user_input: + return await self.async_step_user({**self.user_pass, **user_input}) + + return self.async_show_form( + step_id="2fa", data_schema=vol.Schema({"2fa": str}), + ) + + async def async_step_import(self, user_input): + """Handle import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) + + +class Require2FA(exceptions.HomeAssistantError): + """Error to indicate we require 2FA.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index fe048731352..1b360f24f1f 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -23,7 +23,7 @@ ON_STATE = "on" OFF_STATE = "off" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" cameras = hass.data[DATA_RING_STICKUP_CAMS] lights = [] @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_capability("light"): lights.append(RingLight(device)) - add_entities(lights, True) + async_add_entities(lights, True) class RingLight(Light): @@ -44,10 +44,19 @@ class RingLight(Light): self._unique_id = self._device.id self._light_on = False self._no_updates_until = dt_util.utcnow() + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 124df7d162b..b8a3c26bd8b 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "requirements": ["ring_doorbell==0.2.9"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": [], + "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index b54c750664e..532f15f94c1 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,16 +1,8 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -20,7 +12,6 @@ from . import ( DATA_RING_CHIMES, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, SIGNAL_UPDATE_RING, ) @@ -67,19 +58,8 @@ SENSOR_TYPES = { ], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" ring_chimes = hass.data[DATA_RING_CHIMES] ring_doorbells = hass.data[DATA_RING_DOORBELLS] @@ -87,22 +67,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for device in ring_chimes: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "chime" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_doorbells: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "doorbell" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_stickup_cams: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: + for sensor_type in SENSOR_TYPES: if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: sensors.append(RingSensor(hass, device, sensor_type)) - add_entities(sensors, True) - return True + async_add_entities(sensors, True) class RingSensor(Entity): @@ -122,10 +101,19 @@ class RingSensor(Entity): self._state = None self._tz = str(hass.config.time_zone) self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json new file mode 100644 index 00000000000..6dff7c00ba6 --- /dev/null +++ b/homeassistant/components/ring/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Ring", + "step": { + "user": { + "title": "Sign-in with Ring account", + "data": { + "username": "Username", + "password": "Password" + } + }, + "2fa": { + "title": "Two-factor authentication", + "data": { + "2fa": "Two-factor code" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 86f5c65d87c..51c9e64377b 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -22,7 +22,7 @@ SIREN_ICON = "mdi:alarm-bell" SKIP_UPDATES_DELAY = timedelta(seconds=5) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" cameras = hass.data[DATA_RING_STICKUP_CAMS] switches = [] @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.has_capability("siren"): switches.append(SirenSwitch(device)) - add_entities(switches, True) + async_add_entities(switches, True) class BaseRingSwitch(SwitchDevice): @@ -41,10 +41,19 @@ class BaseRingSwitch(SwitchDevice): self._device = device self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" + self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None @callback def _update_callback(self): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c5ea3f1a5d9..76e10becfb2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -66,6 +66,7 @@ FLOWS = [ "point", "ps4", "rainmachine", + "ring", "samsungtv", "sentry", "simplisafe", diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index e5042a935d6..1afc597415e 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,14 +1,15 @@ """Common methods used across the tests for ring devices.""" +from unittest.mock import patch + from homeassistant.components.ring import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry + async def setup_platform(hass, platform): """Set up the ring platform and prerequisites.""" - config = { - DOMAIN: {CONF_USERNAME: "foo", CONF_PASSWORD: "bar", CONF_SCAN_INTERVAL: 1000}, - platform: {"platform": DOMAIN}, - } - assert await async_setup_component(hass, platform, config) + MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass) + with patch("homeassistant.components.ring.PLATFORMS", [platform]): + assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index b61840769a2..e4b516496e7 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -36,6 +36,10 @@ def requests_mock_fixture(ring_mock): "https://api.ring.com/clients_api/ring_devices", text=load_fixture("ring_devices.json"), ) + mock.get( + "https://api.ring.com/clients_api/dings/active", + text=load_fixture("ring_ding_active.json"), + ) # Mocks the response for getting the history of a device mock.get( "https://api.ring.com/clients_api/doorbots/987652/history", diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index c0b538b8eff..5a04017f54b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,13 +1,20 @@ """The tests for the Ring binary sensor platform.""" +from asyncio import run_coroutine_threadsafe import os import unittest +from unittest.mock import patch import requests_mock from homeassistant.components import ring as base_ring from homeassistant.components.ring import binary_sensor as ring -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import ( + get_test_config_dir, + get_test_home_assistant, + load_fixture, + mock_storage, +) from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -68,8 +75,17 @@ class TestRingBinarySensorSetup(unittest.TestCase): text=load_fixture("ring_chime_health_attrs.json"), ) - base_ring.setup(self.hass, VALID_CONFIG) - ring.setup_platform(self.hass, self.config, self.add_entities, None) + with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): + run_coroutine_threadsafe( + base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop + ).result() + run_coroutine_threadsafe( + self.hass.async_block_till_done(), self.hass.loop + ).result() + run_coroutine_threadsafe( + ring.async_setup_entry(self.hass, None, self.add_entities), + self.hass.loop, + ).result() for device in self.DEVICES: device.update() diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py new file mode 100644 index 00000000000..46925069c31 --- /dev/null +++ b/tests/components/ring/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Ring config flow.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.ring import DOMAIN +from homeassistant.components.ring.config_flow import InvalidAuth + +from tests.common import mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ring.config_flow.Ring", + return_value=Mock(is_connected=True), + ), patch( + "homeassistant.components.ring.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.ring.async_setup_entry", return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "hello@home-assistant.io", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "hello@home-assistant.io" + assert result2["data"] == { + "username": "hello@home-assistant.io", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "hello@home-assistant.io", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 4d3fede89a9..cfc19da78bf 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,4 +1,5 @@ """The tests for the Ring component.""" +from asyncio import run_coroutine_threadsafe from copy import deepcopy from datetime import timedelta import os @@ -59,7 +60,10 @@ class TestRing(unittest.TestCase): "https://api.ring.com/clients_api/doorbots/987652/health", text=load_fixture("ring_doorboot_health_attrs.json"), ) - response = ring.setup(self.hass, self.config) + response = run_coroutine_threadsafe( + ring.async_setup(self.hass, self.config), self.hass.loop + ).result() + assert response @requests_mock.Mocker() diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index dd9d36f80a1..0102020e3c2 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,6 +1,8 @@ """The tests for the Ring sensor platform.""" +from asyncio import run_coroutine_threadsafe import os import unittest +from unittest.mock import patch import requests_mock @@ -8,7 +10,12 @@ from homeassistant.components import ring as base_ring import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import ( + get_test_config_dir, + get_test_home_assistant, + load_fixture, + mock_storage, +) from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -76,8 +83,18 @@ class TestRingSensorSetup(unittest.TestCase): "https://api.ring.com/clients_api/chimes/999999/health", text=load_fixture("ring_chime_health_attrs.json"), ) - base_ring.setup(self.hass, VALID_CONFIG) - ring.setup_platform(self.hass, self.config, self.add_entities, None) + + with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): + run_coroutine_threadsafe( + base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop + ).result() + run_coroutine_threadsafe( + self.hass.async_block_till_done(), self.hass.loop + ).result() + run_coroutine_threadsafe( + ring.async_setup_entry(self.hass, None, self.add_entities), + self.hass.loop, + ).result() for device in self.DEVICES: device.update() From d0062fc7407bd506b0374bd23643db537ff1512a Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 10 Jan 2020 16:34:50 -0500 Subject: [PATCH 034/393] Fix Alexa ChangeReports Filter non-proactively_reported_properties (#30655) * Fix Change Report Properties. * Fix Change Report Properties. --- homeassistant/components/alexa/capabilities.py | 12 ++++++++++++ homeassistant/components/alexa/entities.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 26d07760747..c2698211241 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -503,6 +503,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 +552,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 +598,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 diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d6fa0415640..b14bebb3302 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -255,6 +255,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 From e2f591e5bc8d62347cf90499db82d880239d25f3 Mon Sep 17 00:00:00 2001 From: NobleKangaroo <34781835+NobleKangaroo@users.noreply.github.com> Date: Fri, 10 Jan 2020 16:35:18 -0500 Subject: [PATCH 035/393] Remove self as Emulated Hue codeowner (#30654) --- CODEOWNERS | 1 - homeassistant/components/emulated_hue/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fddb106a07a..344adf9b8fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,7 +89,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 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" } From 74a198e37b9fc3d189d307cc5396609513d9e0e6 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 10 Jan 2020 17:11:50 -0500 Subject: [PATCH 036/393] Implement TimeHoldController Interface in Alexa (#30650) * Implement Alexa.TimeHoldController Interface * Add test for timer resume directive. --- .../components/alexa/capabilities.py | 26 ++++++++++++ homeassistant/components/alexa/entities.py | 16 ++++++++ homeassistant/components/alexa/handlers.py | 27 ++++++++++++ tests/components/alexa/test_smart_home.py | 41 +++++++++++++++++++ 4 files changed, 110 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c2698211241..c6d422f5c2b 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1665,3 +1665,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/entities.py b/homeassistant/components/alexa/entities.py index b14bebb3302..6d1997589a4 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -19,6 +19,7 @@ from homeassistant.components import ( script, sensor, switch, + timer, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -61,6 +62,7 @@ from .capabilities import ( AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaTimeHoldController, AlexaToggleController, ) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES @@ -708,3 +710,17 @@ 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) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 74c1b24d42b..b920f11821a 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -10,6 +10,7 @@ from homeassistant.components import ( input_number, light, media_player, + timer, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -1396,3 +1397,29 @@ 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} + + await hass.services.async_call( + entity.domain, timer.SERVICE_PAUSE, 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} + + await hass.services.async_call( + entity.domain, timer.SERVICE_START, data, blocking=False, context=context + ) + + return directive.response() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 37301c3555e..23100bc2078 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3037,3 +3037,44 @@ 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 + ) From d6512c8a9f74f17b794f9bdca38974cbe98294c0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 11 Jan 2020 00:31:55 +0000 Subject: [PATCH 037/393] [ci skip] Translation update --- .../components/axis/.translations/ru.json | 6 ++--- .../components/brother/.translations/es.json | 24 +++++++++++++++++ .../components/brother/.translations/lb.json | 1 + .../components/brother/.translations/ru.json | 2 +- .../cert_expiry/.translations/zh-Hant.json | 2 +- .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/es.json | 8 +++++- .../components/deconz/.translations/ru.json | 4 +-- .../components/elgato/.translations/ru.json | 2 +- .../components/gios/.translations/es.json | 3 +++ .../components/gios/.translations/lb.json | 3 +++ .../homekit_controller/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../huawei_lte/.translations/ru.json | 4 +-- .../components/hue/.translations/ru.json | 4 +-- .../components/ring/.translations/da.json | 27 +++++++++++++++++++ .../components/ring/.translations/en.json | 5 ++-- .../samsungtv/.translations/da.json | 26 ++++++++++++++++++ .../samsungtv/.translations/en.json | 26 ++++++++++++++++++ .../samsungtv/.translations/es.json | 26 ++++++++++++++++++ .../samsungtv/.translations/it.json | 26 ++++++++++++++++++ .../samsungtv/.translations/lb.json | 26 ++++++++++++++++++ .../samsungtv/.translations/no.json | 26 ++++++++++++++++++ .../samsungtv/.translations/ru.json | 26 ++++++++++++++++++ .../components/solarlog/.translations/ru.json | 4 +-- .../components/toon/.translations/ru.json | 2 +- .../components/tradfri/.translations/ru.json | 4 +-- .../components/wled/.translations/ru.json | 2 +- 28 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/brother/.translations/es.json create mode 100644 homeassistant/components/ring/.translations/da.json create mode 100644 homeassistant/components/samsungtv/.translations/da.json create mode 100644 homeassistant/components/samsungtv/.translations/en.json create mode 100644 homeassistant/components/samsungtv/.translations/es.json create mode 100644 homeassistant/components/samsungtv/.translations/it.json create mode 100644 homeassistant/components/samsungtv/.translations/lb.json create mode 100644 homeassistant/components/samsungtv/.translations/no.json create mode 100644 homeassistant/components/samsungtv/.translations/ru.json diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 3506b636baa..b0da189d20f 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis.", "updated_configuration": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "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.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, diff --git a/homeassistant/components/brother/.translations/es.json b/homeassistant/components/brother/.translations/es.json new file mode 100644 index 00000000000..f4e53e20793 --- /dev/null +++ b/homeassistant/components/brother/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "unsupported_model": "Este modelo de impresora no es compatible." + }, + "error": { + "connection_error": "Error de conexi\u00f3n.", + "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." + }, + "step": { + "user": { + "data": { + "host": "Nombre del host o direcci\u00f3n IP de la impresora", + "type": "Tipo de impresora" + }, + "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" + } + }, + "title": "Impresora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json index e9ffc2c4da7..dd051b1bb0c 100644 --- a/homeassistant/components/brother/.translations/lb.json +++ b/homeassistant/components/brother/.translations/lb.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", "unsupported_model": "D\u00ebse Printer Modell g\u00ebtt net \u00ebnnerst\u00ebtzt." }, "error": { diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json index 8bce23e5292..eb12f2f1225 100644 --- a/homeassistant/components/brother/.translations/ru.json +++ b/homeassistant/components/brother/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "error": { diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index c710deae5c1..a14361376df 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -21,6 +21,6 @@ "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" } }, - "title": "\u8a8d\u8b49\u5df2\u904e\u671f" + "title": "\u6191\u8b49\u671f\u9650" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 00a517f701f..c9ab31597d7 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index adbe68153f7..6f5513d9729 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -77,15 +77,21 @@ "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_flip_180_degrees": "Dispositivo volteado 180 grados", + "remote_flip_90_degrees": "Dispositivo volteado 90 grados", "remote_gyro_activated": "Dispositivo sacudido", "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", - "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"" + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"", + "remote_turned_clockwise": "Dispositivo girado en el sentido de las agujas del reloj", + "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 87c3fb646f2..29b584fb9bb 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "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.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.", diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json index 7f52b5adee5..2b5fb72c507 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "connection_error": "\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 Elgato Key Light." }, "error": { diff --git a/homeassistant/components/gios/.translations/es.json b/homeassistant/components/gios/.translations/es.json index 9be1581329a..fb9eead7d2c 100644 --- a/homeassistant/components/gios/.translations/es.json +++ b/homeassistant/components/gios/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3n de GIO\u015a para esta estaci\u00f3n de medici\u00f3n ya est\u00e1 configurada." + }, "error": { "cannot_connect": "No se puede conectar al servidor GIO\u015a.", "invalid_sensors_data": "Datos de sensores no v\u00e1lidos para esta estaci\u00f3n de medici\u00f3n.", diff --git a/homeassistant/components/gios/.translations/lb.json b/homeassistant/components/gios/.translations/lb.json index ed42ad3a7ae..3b23ba5eee5 100644 --- a/homeassistant/components/gios/.translations/lb.json +++ b/homeassistant/components/gios/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "GIO\u015a Integratioun fir d\u00ebs Miess Statioun ass scho konfigur\u00e9iert." + }, "error": { "cannot_connect": "Konnt sech net mam GIO\u015a Server verbannen.", "invalid_sensors_data": "Ong\u00eblteg Sensor Donn\u00e9e\u00eb fir d\u00ebs Miess Statioun", diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index 44a57a1eb25..41393acb26b 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u044d\u0442\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 35f52a7b284..1ba33b0e6ee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index 3850b86167a..c7c9e2033ef 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", + "already_in_progress": "\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.", "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index c749a498e44..3425cb82d01 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,8 +2,8 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "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.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", diff --git a/homeassistant/components/ring/.translations/da.json b/homeassistant/components/ring/.translations/da.json new file mode 100644 index 00000000000..45aebd1ebd5 --- /dev/null +++ b/homeassistant/components/ring/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "2fa": { + "data": { + "2fa": "Tofaktorkode" + }, + "title": "Tofaktorgodkendelse" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "title": "Log ind med Ring-konto" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json index db4665b6c0a..54caa8f96e7 100644 --- a/homeassistant/components/ring/.translations/en.json +++ b/homeassistant/components/ring/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect, please try again", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, @@ -13,14 +12,14 @@ "data": { "2fa": "Two-factor code" }, - "title": "Enter two-factor authentication" + "title": "Two-factor authentication" }, "user": { "data": { "password": "Password", "username": "Username" }, - "title": "Connect to the device" + "title": "Sign-in with Ring account" } }, "title": "Ring" diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json new file mode 100644 index 00000000000..594127688c2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dette Samsung-tv er allerede konfigureret.", + "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", + "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", + "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", + "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Samsung-tv {model}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", + "title": "Samsung-tv" + }, + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "name": "Navn" + }, + "description": "Indtast dine Samsung-tv-oplysninger. Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse.", + "title": "Samsung-tv" + } + }, + "title": "Samsung-tv" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/en.json b/homeassistant/components/samsungtv/.translations/en.json new file mode 100644 index 00000000000..24ab81c007c --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "This Samsung TV is already configured.", + "already_in_progress": "Samsung TV configuration is already in progress.", + "auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.", + "not_found": "No supported Samsung TV devices found on the network.", + "not_supported": "This Samsung TV devices is currently not supported." + }, + "step": { + "confirm": { + "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 authentication. Manual configurations for this TV will be overwritten.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host or IP address", + "name": "Name" + }, + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json new file mode 100644 index 00000000000..3535d4bc65f --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Este televisor Samsung ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", + "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", + "not_found": "No se encontraron televisiones Samsung compatibles en la red.", + "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Introduce la informaci\u00f3n de tu televisor Samsung. Si nunca conect\u00f3 Home Assistant antes de ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json new file mode 100644 index 00000000000..c783db24720 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", + "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", + "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.", + "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", + "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o indirizzo IP", + "name": "Nome" + }, + "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/lb.json b/homeassistant/components/samsungtv/.translations/lb.json new file mode 100644 index 00000000000..fe1f02e55ea --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebs Samsung TV ass scho konfigur\u00e9iert.", + "already_in_progress": "Konfiguratioun fir d\u00ebs Samsung TV ass schonn am gaang.", + "auth_missing": "Home Assistant ass net authentifiz\u00e9iert fir sech mat d\u00ebsem Samsung TV ze verbannen.", + "not_found": "Keng \u00ebnnerst\u00ebtzte Samsung TV am Netzwierk fonnt.", + "not_supported": "D\u00ebsen Samsung TV Modell g\u00ebtt momentan net \u00ebnnerst\u00ebtzt" + }, + "step": { + "confirm": { + "description": "W\u00ebllt dir de Samsung TV {model} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun g\u00ebtt iwwerschriwwen.", + "title": "Samsnung TV" + }, + "user": { + "data": { + "host": "Numm oder IP Adresse", + "name": "Numm" + }, + "description": "Gitt \u00e4r Samsung TV Informatiounen un. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen.", + "title": "Samsnung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json new file mode 100644 index 00000000000..dcd437642b2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Samsung TV-en er allerede konfigurert.", + "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", + "auth_missing": "Home Assistant er ikke autentisert for \u00e5 koble til denne Samsung TV-en.", + "not_found": "Ingen st\u00f8ttede Samsung TV-enheter funnet i nettverket.", + "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." + }, + "step": { + "confirm": { + "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Vert eller IP-adresse", + "name": "Navn" + }, + "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/ru.json b/homeassistant/components/samsungtv/.translations/ru.json new file mode 100644 index 00000000000..d5dd11a1b80 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "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.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } + }, + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json index 7f40935e5a5..b64496c4591 100644 --- a/homeassistant/components/solarlog/.translations/ru.json +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "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": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." }, "step": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 427f717e3ad..75b46d3f600 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -9,7 +9,7 @@ }, "error": { "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "display_exists": "\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." }, "step": { "authenticate": { diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 2e3dc8331be..7d2925fd3f2 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." + "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.", + "already_in_progress": "\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\u044f\u0435\u0442\u0441\u044f." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json index cd4c3c3b066..a884a20b337 100644 --- a/homeassistant/components/wled/.translations/ru.json +++ b/homeassistant/components/wled/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", "connection_error": "\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." }, "error": { From 669c89e8c0c422d550f0ebaf5fb6bb37c8e57317 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 11 Jan 2020 00:33:48 +0000 Subject: [PATCH 038/393] Fix HomeKit with entity registry restoration where supported_features is a non-None falsey (#30657) * Fix homekit with #30094 * Fix test --- homeassistant/helpers/entity_registry.py | 6 +- tests/components/homekit/test_type_covers.py | 73 +++++++++++++++ tests/components/homekit/test_type_fans.py | 40 ++++++++ tests/components/homekit/test_type_lights.py | 36 ++++++++ .../homekit/test_type_media_players.py | 56 ++++++++++++ .../homekit/test_type_thermostats.py | 91 +++++++++++++++++++ tests/helpers/test_entity_registry.py | 2 +- 7 files changed, 300 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index a8a7fdab2c8..acb155ae594 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -502,13 +502,13 @@ def async_setup_entity_restore( attrs: Dict[str, Any] = {ATTR_RESTORED: True} - if entry.capabilities: + if entry.capabilities is not None: attrs.update(entry.capabilities) - if entry.supported_features: + if entry.supported_features is not None: attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features - if entry.device_class: + if entry.device_class is not None: attrs[ATTR_DEVICE_CLASS] = entry.device_class states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7bf92b28de2..fb73c132e30 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -13,6 +13,7 @@ from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -20,6 +21,8 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -308,3 +311,73 @@ async def test_window_open_close_stop(hass, hk_driver, cls, events): assert acc.char_position_state.value == 2 assert len(events) == 3 assert events[-1].data[ATTR_VALUE] is None + + +async def test_window_basic_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "cover", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "cover", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={}, + supported_features=SUPPORT_STOP, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.window_basic(hass, hk_driver, "Cover", "cover.simple", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + acc = cls.window_basic(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + +async def test_window_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "cover", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "cover", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={}, + supported_features=SUPPORT_STOP, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.window(hass, hk_driver, "Cover", "cover.simple", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None + + acc = cls.window(hass, hk_driver, "Cover", "cover.all_info_set", 2, None) + assert acc.category == 14 + assert acc.char_current_position is not None + assert acc.char_target_position is not None + assert acc.char_position_state is not None diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 5631791d7a2..9bcca3cc452 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -24,10 +24,13 @@ from homeassistant.components.homekit.util import HomeKitSpeedMapping from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -226,3 +229,40 @@ async def test_fan_speed(hass, hk_driver, cls, events): assert call_set_speed[0].data[ATTR_SPEED] == "ludicrous" assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "ludicrous" + + +async def test_fan_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "fan", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "fan", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={"speed_list": ["off", "low", "medium", "high"]}, + supported_features=SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.fan(hass, hk_driver, "Fan", "fan.simple", 2, None) + assert acc.category == 3 + assert acc.char_active is not None + assert acc.char_direction is None + assert acc.char_speed is None + assert acc.char_swing is None + + acc = cls.fan(hass, hk_driver, "Fan", "fan.all_info_set", 2, None) + assert acc.category == 3 + assert acc.char_active is not None + assert acc.char_direction is not None + assert acc.char_speed is not None + assert acc.char_swing is not None diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 510cfa4f666..c1811a2e2fc 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -17,10 +17,13 @@ from homeassistant.components.light import ( from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, STATE_UNKNOWN, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -205,3 +208,36 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" + + +async def test_light_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "light", "hue", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "light", + "hue", + "9012", + suggested_object_id="all_info_set", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None) + assert acc.category == 5 # Lightbulb + assert acc.chars == [] + assert acc.char_on.value == 0 + + acc = cls.light(hass, hk_driver, "Light", "light.all_info_set", 2, None) + assert acc.category == 5 # Lightbulb + assert acc.chars == ["Brightness"] + assert acc.char_on.value == 0 diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index aa007b4d04c..366617ee988 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -24,12 +24,15 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service @@ -336,3 +339,56 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): assert acc.char_active.value == 1 assert not caplog.messages or "Error" not in caplog.messages[-1] + + +async def test_tv_restore(hass, hk_driver, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "media_player", + "generic", + "1234", + suggested_object_id="simple", + device_class=DEVICE_CLASS_TV, + ) + registry.async_get_or_create( + "media_player", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ + ATTR_INPUT_SOURCE_LIST: ["HDMI 1", "HDMI 2", "HDMI 3", "HDMI 4"], + }, + supported_features=3469, + device_class=DEVICE_CLASS_TV, + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = TelevisionMediaPlayer( + hass, hk_driver, "MediaPlayer", "media_player.simple", 2, None + ) + assert acc.category == 31 + assert acc.chars_tv == [] + assert acc.chars_speaker == [] + assert acc.support_select_source is False + assert not hasattr(acc, "char_input_source") + + acc = TelevisionMediaPlayer( + hass, hk_driver, "MediaPlayer", "media_player.all_info_set", 2, None + ) + assert acc.category == 31 + assert acc.chars_tv == ["RemoteKey"] + assert acc.chars_speaker == [ + "Name", + "Active", + "VolumeControlType", + "VolumeSelector", + "Volume", + ] + assert acc.support_select_source is True + assert acc.char_input_source is not None diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 174b72f780a..c96cfdae602 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -41,8 +41,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, + EVENT_HOMEASSISTANT_START, TEMP_FAHRENHEIT, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce @@ -517,6 +520,51 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "climate", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "climate", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ + ATTR_MIN_TEMP: 60, + ATTR_MAX_TEMP: 70, + ATTR_HVAC_MODES: [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF], + }, + supported_features=0, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (7, 35) + assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { + "cool", + "heat", + "heat_cool", + "off", + } + + acc = cls.thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (60.0, 70.0) + assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { + "heat_cool", + "off", + } + + async def test_thermostat_hvac_modes(hass, hk_driver, cls): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -671,3 +719,46 @@ async def test_water_heater_get_temperature_range(hass, hk_driver, cls): ) await hass.async_block_till_done() assert acc.get_temperature_range() == (15.5, 21.0) + + +async def test_water_heater_restore(hass, hk_driver, cls, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "water_heater", "generic", "1234", suggested_object_id="simple", + ) + registry.async_get_or_create( + "water_heater", + "generic", + "9012", + suggested_object_id="all_info_set", + capabilities={ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}, + supported_features=0, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = cls.thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) + assert acc.category == 9 + assert acc.get_temperature_range() == (7, 35) + assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { + "Cool", + "Heat", + "Off", + } + + acc = cls.thermostat( + hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None + ) + assert acc.category == 9 + assert acc.get_temperature_range() == (60.0, 70.0) + assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { + "Cool", + "Heat", + "Off", + } diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 7f45ff0d174..e532d99f333 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -511,7 +511,7 @@ async def test_restore_states(hass): simple = hass.states.get("light.simple") assert simple is not None assert simple.state == STATE_UNAVAILABLE - assert simple.attributes == {"restored": True} + assert simple.attributes == {"restored": True, "supported_features": 0} disabled = hass.states.get("light.disabled") assert disabled is None From 605b0ceb5fd50df938c19758e093c005ba9ddfe8 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 10 Jan 2020 20:26:37 -0500 Subject: [PATCH 039/393] Add support for variable fan speed list length. (#30574) --- .../components/alexa/capabilities.py | 35 +++---- homeassistant/components/alexa/const.py | 14 --- homeassistant/components/alexa/handlers.py | 19 ++-- tests/components/alexa/test_capabilities.py | 37 +++++--- tests/components/alexa/test_smart_home.py | 94 +++++++++++++++---- tests/components/alexa/test_state_report.py | 2 + 6 files changed, 134 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c6d422f5c2b..d1b7917f263 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -31,7 +31,6 @@ from .const import ( API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, Inputs, ) from .errors import UnsupportedProperty @@ -1273,8 +1272,12 @@ class AlexaRangeController(AlexaCapability): # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - speed = self.entity.attributes.get(fan.ATTR_SPEED) - return RANGE_FAN_MAP.get(speed, 0) + speed_list = self.entity.attributes[fan.ATTR_SPEED_LIST] + speed = self.entity.attributes[fan.ATTR_SPEED] + 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}": @@ -1302,24 +1305,22 @@ 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 = [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() # Cover Position Resources 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/handlers.py b/homeassistant/components/alexa/handlers.py index b920f11821a..510efe4b610 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -51,8 +51,6 @@ from .const import ( API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, PERCENTAGE_FAN_MAP, - RANGE_FAN_MAP, - SPEED_FAN_MAP, Cause, Inputs, ) @@ -1096,8 +1094,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" @@ -1174,9 +1174,16 @@ async def async_api_adjust_range(hass, config, directive, context): 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: 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 23100bc2078..dd6faab8e96 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -132,7 +132,7 @@ def get_capability(capabilities, capability_name, instance=None): for capability in capabilities: if instance and capability["instance"] == instance: return capability - elif capability["interface"] == capability_name: + if capability["interface"] == capability_name: return capability return None @@ -497,11 +497,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 +510,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 +555,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 +572,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 +661,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", "warp_speed"], "speed": "medium", }, ) @@ -701,6 +692,60 @@ 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"] == 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": "warp speed", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}}, + ] + }, + } in presets + call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", @@ -712,9 +757,20 @@ async def test_fan_range(hass): ) 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"] == "warp_speed" + await assert_range_changes( hass, - [("low", "-1"), ("high", "1"), ("medium", "0")], + [("low", "-1"), ("high", "1"), ("medium", "0"), ("warp_speed", "99")], "Alexa.RangeController", "AdjustRangeValue", "fan#test_5", @@ -733,7 +789,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", }, ) @@ -752,7 +808,7 @@ async def test_fan_range_off(hass): await assert_range_changes( hass, - [("off", "-3")], + [("off", "-3"), ("off", "-99")], "Alexa.RangeController", "AdjustRangeValue", "fan#test_6", 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, }, ) From 008dddb17c8cf3fc79488ea2562e0d5a78db5d91 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 Jan 2020 20:30:58 -0500 Subject: [PATCH 040/393] Fix ZHA temperature sensor restoration (#30661) * Add test for restoring state for zha temp. * Don't restore unit of measurement for ZHA sensors. Properly restore ZHA temperature sensor state. --- homeassistant/components/zha/sensor.py | 20 +++- tests/components/zha/test_sensor.py | 123 +++++++++++++++++++++++-- 2 files changed, 135 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2d39d562bf5..3b73a9793c9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -12,9 +12,15 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, DOMAIN, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import fahrenheit_to_celsius from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, @@ -160,7 +166,6 @@ class Sensor(ZhaEntity): def async_restore_last_state(self, last_state): """Restore previous state.""" self._state = last_state.state - self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback async def async_state_attr_provider(self): @@ -277,3 +282,14 @@ class Temperature(Sensor): _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + if last_state.state == STATE_UNKNOWN: + return + if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS: + ftemp = float(last_state.state) + self._state = round(fahrenheit_to_celsius(ftemp), 1) + return + self._state = last_state.state diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index b2daf4da765..3e02542a4fb 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,5 @@ """Test zha sensor.""" +import pytest import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement @@ -6,7 +7,20 @@ import zigpy.zcl.clusters.smartenergy as smartenergy import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +import homeassistant.config as config_util +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.helpers import restore_state +from homeassistant.util import dt as dt_util from .common import ( async_enable_traffic, @@ -39,7 +53,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # ensure the sensor entity was created for each id in cluster_ids for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and devices @@ -55,7 +69,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # test that the sensors now have a state of unknown for cluster_id in cluster_ids: zigpy_device_info = zigpy_device_infos[cluster_id] - entity_id = zigpy_device_info["entity_id"] + entity_id = zigpy_device_info[ATTR_ENTITY_ID] assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic @@ -128,7 +142,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] zha_device = zha_gateway.get_device(zigpy_device.ieee) device_info["zha_device"] = zha_device - device_info["entity_id"] = await find_entity_id(DOMAIN, zha_device, hass) + device_info[ATTR_ENTITY_ID] = await find_entity_id(DOMAIN, zha_device, hass) await hass.async_block_till_done() return device_infos @@ -187,6 +201,103 @@ def assert_state(hass, device_info, state, unit_of_measurement): This is used to ensure that the logic in each sensor class handled the attribute report it received correctly. """ - hass_state = hass.states.get(device_info["entity_id"]) + hass_state = hass.states.get(device_info[ATTR_ENTITY_ID]) assert hass_state.state == state - assert hass_state.attributes.get("unit_of_measurement") == unit_of_measurement + assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement + + +@pytest.fixture +def hass_ms(hass): + """Hass instance with measurement system.""" + + async def _hass_ms(meas_sys): + await config_util.async_process_ha_core_config( + hass, {CONF_UNIT_SYSTEM: meas_sys} + ) + await hass.async_block_till_done() + return hass + + return _hass_ms + + +@pytest.fixture +def core_rs(hass_storage): + """Core.restore_state fixture.""" + + def _storage(entity_id, uom, state): + now = dt_util.utcnow().isoformat() + + hass_storage[restore_state.STORAGE_KEY] = { + "version": restore_state.STORAGE_VERSION, + "key": restore_state.STORAGE_KEY, + "data": [ + { + "state": { + "entity_id": entity_id, + "state": str(state), + "attributes": {ATTR_UNIT_OF_MEASUREMENT: uom}, + "last_changed": now, + "last_updated": now, + "context": { + "id": "3c2243ff5f30447eb12e7348cfd5b8ff", + "user_id": None, + }, + }, + "last_seen": now, + } + ], + } + return + + return _storage + + +@pytest.mark.parametrize( + "uom, raw_temp, expected, restore", + [ + (TEMP_CELSIUS, 2900, 29, False), + (TEMP_CELSIUS, 2900, 29, True), + (TEMP_FAHRENHEIT, 2900, 84, False), + (TEMP_FAHRENHEIT, 2900, 84, True), + ], +) +async def test_temp_uom( + uom, raw_temp, expected, restore, hass_ms, config_entry, zha_gateway, core_rs +): + """Test zha temperature sensor unit of measurement.""" + + entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" + if restore: + core_rs(entity_id, uom, state=(expected - 2)) + + hass = await hass_ms( + CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL + ) + + # list of cluster ids to create devices and sensor entities for + temp_cluster = measurement.TemperatureMeasurement + cluster_ids = [temp_cluster.cluster_id] + + # devices that were created from cluster_ids list above + zigpy_device_infos = await async_build_devices( + hass, zha_gateway, config_entry, cluster_ids + ) + + zigpy_device_info = zigpy_device_infos[temp_cluster.cluster_id] + zha_device = zigpy_device_info["zha_device"] + if not restore: + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and devices + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + # test that the sensors now have a state of unknown + if not restore: + assert hass.states.get(entity_id).state == STATE_UNKNOWN + + await send_attribute_report(hass, zigpy_device_info["cluster"], 0, raw_temp) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert round(float(state.state)) == expected + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == uom From c1c90b8034b0283323a68ce1fec38cc6874a1ccc Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sat, 11 Jan 2020 03:59:11 +0000 Subject: [PATCH 041/393] Update ON/OFF condition and triggers to match documentation and UI, issue #30462 (#30663) --- homeassistant/components/binary_sensor/device_condition.py | 4 ++-- homeassistant/components/binary_sensor/device_trigger.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 842790e0178..aa9a9d25e72 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -90,7 +90,7 @@ IS_ON = [ CONF_IS_GAS, CONF_IS_HOT, CONF_IS_LIGHT, - CONF_IS_LOCKED, + CONF_IS_NOT_LOCKED, CONF_IS_MOIST, CONF_IS_MOTION, CONF_IS_MOVING, @@ -112,7 +112,7 @@ IS_OFF = [ CONF_IS_NOT_COLD, CONF_IS_NOT_CONNECTED, CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, + CONF_IS_LOCKED, CONF_IS_NOT_MOIST, CONF_IS_NOT_MOVING, CONF_IS_NOT_OCCUPIED, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 288cc101d93..f4799828c68 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -91,7 +91,7 @@ TURNED_ON = [ CONF_GAS, CONF_HOT, CONF_LIGHT, - CONF_LOCKED, + CONF_NOT_LOCKED, CONF_MOIST, CONF_MOTION, CONF_MOVING, @@ -113,7 +113,7 @@ TURNED_OFF = [ CONF_NOT_COLD, CONF_NOT_CONNECTED, CONF_NOT_HOT, - CONF_NOT_LOCKED, + CONF_LOCKED, CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, From 9c551ae85d68a8c82c706d6e2cf044cfd5533054 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 10 Jan 2020 23:27:41 -0500 Subject: [PATCH 042/393] Use storage based collections for input_number entities (#30576) * Use collections for input_number. * Add tests. * Typo fix. * Make editable a public attribute. * Move async_setup to top. * Update homeassistant/components/input_number/__init__.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/input_number/__init__.py Co-Authored-By: Paulus Schoutsen * Cleanup. * async_write_ha_state() Co-authored-by: Paulus Schoutsen --- .../components/input_number/__init__.py | 192 ++++++++++---- tests/components/input_number/test_init.py | 245 +++++++++++++++++- 2 files changed, 383 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a4438020886..deedfdab2de 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, entity_registry 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( @@ -80,22 +109,61 @@ CONFIG_SCHEMA = vol.Schema( 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() - if conf is None: + 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[DOMAIN].items()] + ) + 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: typing.Optional[typing.Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + + 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) + + 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: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **conf} for id_, conf in conf[DOMAIN].items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -115,86 +183,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 +308,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 +322,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 +336,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/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 6d032b639cf..f9763168354 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", @@ -302,8 +358,189 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): 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 From b128814acf028a7dbdcdef559f34af15cb0e4ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sat, 11 Jan 2020 09:36:50 +0000 Subject: [PATCH 043/393] pushover: improve error when image download fails (#30615) --- homeassistant/components/pushover/notify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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: From 5ffbf55170641657810d9f724d7d38c228999ea2 Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Sat, 11 Jan 2020 10:41:52 +0100 Subject: [PATCH 044/393] Add support for Dyson TP06 fan (#30611) * initial commit * update manifest.json * update CODEOWNERS * remove unnecessary else * add rest of asserts for TP06 state test --- CODEOWNERS | 1 + homeassistant/components/dyson/fan.py | 2 + homeassistant/components/dyson/manifest.json | 2 +- tests/components/dyson/test_fan.py | 61 ++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 344adf9b8fb..38a233d4a19 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,6 +82,7 @@ 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 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..4fc49b4ca60 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": ["libpurecool==0.6.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@etheralm"] } 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", From e793ed9ab0b8857ba0aa88a7e7bfb8accd7761b2 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 11 Jan 2020 12:20:00 +0100 Subject: [PATCH 045/393] Refactor Netatmo integration (#29851) * Refactor to use ids in data class * Use station_id * Refactor Netatmo to use oauth * Remove old code * Clean up * Clean up * Clean up * Refactor binary sensor * Add initial light implementation * Add discovery * Add set schedule service back in * Add discovery via homekit * More work on the light * Fix set schedule service * Clean up * Remove unnecessary code * Add support for multiple entities/accounts * Fix MANUFACTURER typo * Remove multiline inline if statement * Only add tags when camera type is welcome * Remove on/off as it's currently broken * Fix camera turn_on/off * Fix debug message * Refactor some camera code * Refactor camera methods * Remove old code * Rename method * Update persons regularly * Remove unused code * Refactor method * Fix isort * Add english strings * Catch NoDevice exception * Fix unique id and only add sensors for tags if present * Address comments * Remove ToDo comment * Add set_light_auto back in * Add debug info * Fix multiple camera issue * Move camera light service to camera * Only allow camera entities * Make test pass * Upgrade pyatmo module to 3.2.0 * Update requirements * Remove list comprehension * Remove guideline violating code * Remove stale code * Rename devices to entities * Remove light platform * Remove commented code * Exclude files from coverage * Remove unused code * Fix unique id * Address comments * Fix comments * Exclude sensor as well * Add another test * Use core interfaces --- .coveragerc | 9 +- CODEOWNERS | 1 + .../components/netatmo/.translations/en.json | 18 + homeassistant/components/netatmo/__init__.py | 312 +++------------ homeassistant/components/netatmo/api.py | 35 ++ .../components/netatmo/binary_sensor.py | 259 +++++-------- homeassistant/components/netatmo/camera.py | 365 ++++++------------ homeassistant/components/netatmo/climate.py | 131 ++++--- .../components/netatmo/config_flow.py | 56 +++ homeassistant/components/netatmo/const.py | 58 ++- .../components/netatmo/manifest.json | 22 +- homeassistant/components/netatmo/sensor.py | 260 ++++--------- .../components/netatmo/services.yaml | 43 +-- homeassistant/components/netatmo/strings.json | 18 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/netatmo/test_config_flow.py | 93 +++++ 19 files changed, 762 insertions(+), 927 deletions(-) create mode 100644 homeassistant/components/netatmo/.translations/en.json create mode 100644 homeassistant/components/netatmo/api.py create mode 100644 homeassistant/components/netatmo/config_flow.py create mode 100644 homeassistant/components/netatmo/strings.json create mode 100644 tests/components/netatmo/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 05682c79744..be11fa5998c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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/* diff --git a/CODEOWNERS b/CODEOWNERS index 38a233d4a19..fa805e6f6ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,6 +222,7 @@ homeassistant/components/neato/* @dshokouhi @Santobert 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 diff --git a/homeassistant/components/netatmo/.translations/en.json b/homeassistant/components/netatmo/.translations/en.json new file mode 100644 index 00000000000..8cd4f51aee2 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/en.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/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..d420fbb1783 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,80 @@ 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) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the access to Netatmo binary sensor.""" + pass 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 +116,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 +138,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..75824a9ebda 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.0" + ], + "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..64a203c47a2 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.""" @@ -470,7 +432,7 @@ class NetatmoPublicSensor(Entity): 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 +484,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 +510,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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 76e10becfb2..f6154e1929d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -57,6 +57,7 @@ FLOWS = [ "mqtt", "neato", "nest", + "netatmo", "notion", "opentherm_gw", "openuv", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 306b3850a1b..eceb2ee3fd5 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -32,6 +32,9 @@ ZEROCONF = { HOMEKIT = { "BSB002": "hue", "LIFX": "lifx", + "Netatmo Relay": "netatmo", + "Presence": "netatmo", "TRADFRI": "tradfri", + "Welcome": "netatmo", "Wemo": "wemo" } diff --git a/requirements_all.txt b/requirements_all.txt index 0265ce132e0..e288c2a29bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.1.0 +pyatmo==3.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f23c944370b..10bd39fe6a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -398,6 +398,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.netatmo +pyatmo==3.2.0 + # homeassistant.components.blackbird pyblackbird==0.5 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 From 7d506bc38b73f5a81649a0fe629f054ea5dbdae6 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 11 Jan 2020 11:37:57 -0500 Subject: [PATCH 046/393] Use storage collections for input_select entity management (#30604) * Refactor input_select to use _config dict. * Use collections for input_select. * Add tests. * Move async_setup to top. * Cleanup. * async_write_ha_state() * Update homeassistant/components/input_select/__init__.py Co-Authored-By: Paulus Schoutsen --- .../components/input_select/__init__.py | 167 +++++++++++---- tests/components/input_select/test_init.py | 196 +++++++++++++++++- .../input_select/test_reproduce_state.py | 2 +- 3 files changed, 322 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index b2b4b2083e8..937af76ed4f 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, entity_registry 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): @@ -65,20 +87,57 @@ 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 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() - if conf is None: + 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[DOMAIN].items()] + ) + 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: typing.Optional[typing.Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + + 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) + + 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: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf[DOMAIN].items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -95,11 +154,11 @@ 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, {}, 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, {}, lambda entity, call: entity.async_offset_index(-1) ) component.async_register_entity_service( @@ -112,35 +171,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 +232,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,7 +252,12 @@ 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} + + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id for the entity.""" + return self._config[CONF_ID] async def async_select_option(self, option): """Select new option.""" @@ -189,17 +269,22 @@ 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): """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): """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/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 8fda80cd3d2..13669ea507f 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", @@ -400,5 +440,159 @@ 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" 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 From 62f53b656d68d4aecf0300bf430459247f51bf2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 11 Jan 2020 23:25:59 +0200 Subject: [PATCH 047/393] Fix test_scenes_with_entity (#30684) Don't assume any particular order for returned scenes; it's undefined (currently uses dict values()). --- tests/components/homeassistant/test_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 672de5827f1..5df6bd6ad52 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -229,7 +229,7 @@ async def test_scenes_with_entity(hass): }, ) - assert ha_scene.scenes_with_entity(hass, "light.kitchen") == [ + assert sorted(ha_scene.scenes_with_entity(hass, "light.kitchen")) == [ "scene.scene_1", "scene.scene_3", ] From 519c1fa2dad3deef8793d35dcc4da1c4554ab049 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 11 Jan 2020 23:12:55 +0100 Subject: [PATCH 048/393] Update iCloud sensors when service finish its update (#30680) * Update iCloud sensors when needed * Add sensor should_poll --- homeassistant/components/icloud/__init__.py | 4 ++-- homeassistant/components/icloud/const.py | 2 +- .../components/icloud/device_tracker.py | 14 +++++++------- homeassistant/components/icloud/sensor.py | 19 ++++++++++++++++++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index e983f5fac22..d4074db021e 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -46,9 +46,9 @@ from .const import ( DEVICE_STATUS_SET, DOMAIN, ICLOUD_COMPONENTS, + SERVICE_UPDATE, STORAGE_KEY, STORAGE_VERSION, - TRACKER_UPDATE, ) ATTRIBUTION = "Data provided by Apple iCloud" @@ -336,7 +336,7 @@ class IcloudAccount: self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) - dispatcher_send(self.hass, TRACKER_UPDATE) + dispatcher_send(self.hass, SERVICE_UPDATE) self._fetch_interval = self._determine_interval() track_point_in_utc_time( self.hass, diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index ed2fc78fe6d..c2545d911df 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,7 +1,7 @@ """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" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 511ce7f9447..79627eec4aa 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -15,7 +15,7 @@ from .const import ( 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/sensor.py b/homeassistant/components/icloud/sensor.py index 4351d4ffa19..f6c87ed12d0 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 .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() From 4a66eb0a691a4c80be56e8b86a156bc183402028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 12 Jan 2020 00:36:40 +0200 Subject: [PATCH 049/393] Upgrade huawei-lte-api to 1.4.6 (#30683) https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.6 https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.5 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 1f5fa69d341..5b930802c61 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.6", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index e288c2a29bf..d8916c184f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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.6 # homeassistant.components.hydrawise hydrawiser==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10bd39fe6a9..7cf4a9f8e89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ homematicip==0.10.15 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.4 +huawei-lte-api==1.4.6 # homeassistant.components.iaqualink iaqualink==0.3.0 From 4972b249bf9cf9c3d9033e7fb94fed0cfa0272ea Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 11 Jan 2020 17:37:39 -0500 Subject: [PATCH 050/393] Use collection helpers for input_text entities (#30633) * Refactor input_text to use config dict. * Use collections for input_text. * Update homeassistant/components/input_text/__init__.py Better logging names. Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/input_text/__init__.py Correct artifacts. Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/input_text/__init__.py Co-Authored-By: Paulus Schoutsen * Cleanup. Co-authored-by: Paulus Schoutsen --- .../components/input_text/__init__.py | 201 ++++++++++---- tests/components/input_text/test_init.py | 246 +++++++++++++++++- 2 files changed, 385 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2049de7ab27..81099e20418 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, entity_registry 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] ), @@ -83,20 +115,57 @@ CONFIG_SCHEMA = vol.Schema( 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() - if conf is None: + 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[DOMAIN].items()] + ) + 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: typing.Optional[typing.Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: return - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + + 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) + + 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: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **(cfg or {})} for id_, cfg in conf[DOMAIN].items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -110,52 +179,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 +235,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 +260,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 +302,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/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 8835128d672..d6478a5472f 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,14 +275,16 @@ 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}, } }, ): @@ -250,5 +313,174 @@ 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 From 7073b0eb88099d8bd80e3870101fe26975667d19 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 16:04:39 -0800 Subject: [PATCH 051/393] Upgrade Ring to new version (#30666) * Upgrade Ring to new version * Move legacy cleanup down * Fix test --- homeassistant/components/ring/__init__.py | 61 ++++++++----------- .../components/ring/binary_sensor.py | 15 ++++- homeassistant/components/ring/camera.py | 16 +++-- homeassistant/components/ring/config_flow.py | 25 +++----- homeassistant/components/ring/light.py | 13 +++- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ring/sensor.py | 17 ++++-- homeassistant/components/ring/switch.py | 13 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/common.py | 4 +- tests/components/ring/conftest.py | 11 +--- tests/components/ring/test_binary_sensor.py | 15 +---- tests/components/ring/test_config_flow.py | 10 ++- tests/components/ring/test_init.py | 10 +-- tests/components/ring/test_sensor.py | 28 +++------ 16 files changed, 121 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 18c753f4dc9..7addc116b06 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -5,8 +5,7 @@ from functools import partial import logging from pathlib import Path -from requests.exceptions import ConnectTimeout, HTTPError -from ring_doorbell import Ring +from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries @@ -14,6 +13,7 @@ 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_time_interval +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,6 @@ DATA_RING_CHIMES = "ring_chimes" DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" -DEFAULT_CACHEDB = ".ring_cache.pickle" DEFAULT_ENTITY_NAMESPACE = "ring" SIGNAL_UPDATE_RING = "ring_update" @@ -54,6 +53,14 @@ async def async_setup(hass, config): if DOMAIN not in config: return True + def legacy_cleanup(): + """Clean up old tokens.""" + old_cache = Path(hass.config.path(".ring_cache.pickle")) + if old_cache.is_file(): + old_cache.unlink() + + await hass.async_add_executor_job(legacy_cleanup) + hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, @@ -69,30 +76,20 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up a config entry.""" - cache = hass.config.path(DEFAULT_CACHEDB) - try: - ring = await hass.async_add_executor_job( - partial( - Ring, - username=entry.data["username"], - password="invalid-password", - cache_file=cache, - ) - ) - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.async_create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False - if not ring.is_connected: - _LOGGER.error("Unable to connect to Ring service") - return False + def token_updater(token): + """Handle from sync context when token is updated.""" + run_callback_threadsafe( + hass.loop, + partial( + hass.config_entries.async_update_entry, + entry, + data={**entry.data, "token": token}, + ), + ).result() + + auth = Auth(entry.data["token"], token_updater) + ring = Ring(auth) await hass.async_add_executor_job(finish_setup_entry, hass, ring) @@ -106,9 +103,10 @@ async def async_setup_entry(hass, entry): def finish_setup_entry(hass, ring): """Finish setting up entry.""" - hass.data[DATA_RING_CHIMES] = chimes = ring.chimes - hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams + devices = ring.devices + hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] + hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] + hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] ring_devices = chimes + doorbells + stickup_cams @@ -160,8 +158,3 @@ async def async_unload_entry(hass, entry): hass.data.pop(DATA_TRACK_INTERVAL) return unload_ok - - -async def async_remove_entry(hass, entry): - """Act when an entry is removed.""" - await hass.async_add_executor_job(Path(hass.config.path(DEFAULT_CACHEDB)).unlink) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 0706752ffb2..29337f29689 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -5,7 +5,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION -from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS +from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -72,14 +72,23 @@ class RingBinarySensor(BinarySensorDevice): """Return a unique ID.""" return self._unique_id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._data.id)}, + "sw_version": self._data.firmware, + "name": self._data.name, + "model": self._data.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware attrs["timezone"] = self._data.timezone if self._data.alert and self._data.alert_expires_at: diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index a3b34afa056..2b0fe14a1d4 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -18,6 +18,7 @@ from . import ( ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, + DOMAIN, SIGNAL_UPDATE_RING, ) @@ -86,16 +87,23 @@ class RingCam(Camera): """Return a unique ID.""" return self._camera.id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._camera.id)}, + "sw_version": self._camera.firmware, + "name": self._camera.name, + "model": self._camera.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "device_id": self._camera.id, - "firmware": self._camera.firmware, - "kind": self._camera.kind, "timezone": self._camera.timezone, - "type": self._camera.family, "video_url": self._video_url, "last_video_id": self._last_video_id, } diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index bdb60cc26c5..98555277baf 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,21 +1,19 @@ """Config flow for Ring integration.""" -from functools import partial import logging from oauthlib.oauth2 import AccessDeniedError -from ring_doorbell import Ring +from ring_doorbell import Auth import voluptuous as vol from homeassistant import config_entries, core, exceptions -from . import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import +from . import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - cache = hass.config.path(DEFAULT_CACHEDB) def otp_callback(): if "2fa" in data: @@ -23,21 +21,16 @@ async def validate_input(hass: core.HomeAssistant, data): raise Require2FA + auth = Auth() + try: - ring = await hass.async_add_executor_job( - partial( - Ring, - username=data["username"], - password=data["password"], - cache_file=cache, - auth_callback=otp_callback, - ) + token = await hass.async_add_executor_job( + auth.fetch_token, data["username"], data["password"], otp_callback, ) except AccessDeniedError: raise InvalidAuth - if not ring.is_connected: - raise InvalidAuth + return token class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -56,12 +49,12 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - await validate_input(self.hass, user_input) + token = await validate_input(self.hass, user_input) await self.async_set_unique_id(user_input["username"]) return self.async_create_entry( title=user_input["username"], - data={"username": user_input["username"]}, + data={"username": user_input["username"], "token": token}, ) except Require2FA: self.user_pass = user_input diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 1b360f24f1f..b7fa67a391f 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -84,6 +84,17 @@ class RingLight(Light): """If the switch is currently on or off.""" return self._light_on + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.id)}, + "sw_version": self._device.firmware, + "name": self._device.name, + "model": self._device.kind, + "manufacturer": "Ring", + } + def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index b8a3c26bd8b..d6570fad5cb 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.2.9"], + "requirements": ["ring_doorbell==0.4.0"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 532f15f94c1..89b042ba862 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -12,6 +12,7 @@ from . import ( DATA_RING_CHIMES, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, + DOMAIN, SIGNAL_UPDATE_RING, ) @@ -108,6 +109,7 @@ class RingSensor(Entity): self._disp_disconnect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RING, self._update_callback ) + await self.hass.async_add_executor_job(self._data.update) async def async_will_remove_from_hass(self): """Disconnect callbacks.""" @@ -140,17 +142,24 @@ class RingSensor(Entity): """Return a unique ID.""" return self._unique_id + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._data.id)}, + "sw_version": self._data.firmware, + "name": self._data.name, + "model": self._data.kind, + "manufacturer": "Ring", + } + @property def device_state_attributes(self): """Return the state attributes.""" attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware - attrs["kind"] = self._data.kind attrs["timezone"] = self._data.timezone - attrs["type"] = self._data.family attrs["wifi_name"] = self._data.wifi_name if self._extra and self._sensor_type.startswith("last_"): diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 51c9e64377b..e23e757d825 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -76,6 +76,17 @@ class BaseRingSwitch(SwitchDevice): """Update controlled via the hub.""" return False + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.id)}, + "sw_version": self._device.firmware, + "name": self._device.name, + "model": self._device.kind, + "manufacturer": "Ring", + } + class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" diff --git a/requirements_all.txt b/requirements_all.txt index d8916c184f8..0c1159256c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.9 +ring_doorbell==0.4.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cf4a9f8e89..ea60a13b565 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.2.9 +ring_doorbell==0.4.0 # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 1afc597415e..93a6e4f91e0 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -9,7 +9,9 @@ from tests.common import MockConfigEntry async def setup_platform(hass, platform): """Set up the ring platform and prerequisites.""" - MockConfigEntry(domain=DOMAIN, data={"username": "foo"}).add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, data={"username": "foo", "token": {}}).add_to_hass( + hass + ) with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index e4b516496e7..a4cfaf0065d 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,21 +1,12 @@ """Configuration for Ring tests.""" -from asynctest import patch import pytest import requests_mock from tests.common import load_fixture -@pytest.fixture(name="ring_mock") -def ring_save_mock(): - """Fixture to mock a ring.""" - with patch("ring_doorbell._exists_cache", return_value=False): - with patch("ring_doorbell._save_cache", return_value=True) as save_mock: - yield save_mock - - @pytest.fixture(name="requests_mock") -def requests_mock_fixture(ring_mock): +def requests_mock_fixture(): """Fixture to provide a requests mocker.""" with requests_mock.mock() as mock: # Note all devices have an id of 987652, but a different device_id. diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 5a04017f54b..4ca83b2451b 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,6 +1,5 @@ """The tests for the Ring binary sensor platform.""" from asyncio import run_coroutine_threadsafe -import os import unittest from unittest.mock import patch @@ -9,12 +8,7 @@ import requests_mock from homeassistant.components import ring as base_ring from homeassistant.components.ring import binary_sensor as ring -from tests.common import ( - get_test_config_dir, - get_test_home_assistant, - load_fixture, - mock_storage, -) +from tests.common import get_test_home_assistant, load_fixture, mock_storage from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -28,15 +22,9 @@ class TestRingBinarySensorSetup(unittest.TestCase): for device in devices: self.DEVICES.append(device) - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB) self.config = { "username": "foo", "password": "bar", @@ -46,7 +34,6 @@ class TestRingBinarySensorSetup(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_binary_sensor(self, mock): diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index 46925069c31..5712106333f 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -18,8 +18,10 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.ring.config_flow.Ring", - return_value=Mock(is_connected=True), + "homeassistant.components.ring.config_flow.Auth", + return_value=Mock( + fetch_token=Mock(return_value={"access_token": "mock-token"}) + ), ), patch( "homeassistant.components.ring.async_setup", return_value=mock_coro(True) ) as mock_setup, patch( @@ -34,6 +36,7 @@ async def test_form(hass): assert result2["title"] == "hello@home-assistant.io" assert result2["data"] == { "username": "hello@home-assistant.io", + "token": {"access_token": "mock-token"}, } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -47,7 +50,8 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.ring.config_flow.Ring", side_effect=InvalidAuth, + "homeassistant.components.ring.config_flow.Auth.fetch_token", + side_effect=InvalidAuth, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index cfc19da78bf..809c71562c0 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -2,7 +2,6 @@ from asyncio import run_coroutine_threadsafe from copy import deepcopy from datetime import timedelta -import os import unittest import requests_mock @@ -10,7 +9,7 @@ import requests_mock from homeassistant import setup import homeassistant.components.ring as ring -from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.common import get_test_home_assistant, load_fixture ATTRIBUTION = "Data provided by Ring.com" @@ -22,21 +21,14 @@ VALID_CONFIG = { class TestRing(unittest.TestCase): """Tests the Ring component.""" - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this test case class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(ring.DEFAULT_CACHEDB) self.config = VALID_CONFIG def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_setup(self, mock): diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 0102020e3c2..039c9d0625f 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the Ring sensor platform.""" from asyncio import run_coroutine_threadsafe -import os import unittest from unittest.mock import patch @@ -10,12 +9,7 @@ from homeassistant.components import ring as base_ring import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.common import ( - get_test_config_dir, - get_test_home_assistant, - load_fixture, - mock_storage, -) +from tests.common import get_test_home_assistant, load_fixture, mock_storage from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG @@ -29,15 +23,9 @@ class TestRingSensorSetup(unittest.TestCase): for device in devices: self.DEVICES.append(device) - def cleanup(self): - """Cleanup any data created from the tests.""" - if os.path.isfile(self.cache): - os.remove(self.cache) - def setUp(self): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() - self.cache = get_test_config_dir(base_ring.DEFAULT_CACHEDB) self.config = { "username": "foo", "password": "bar", @@ -55,7 +43,6 @@ class TestRingSensorSetup(unittest.TestCase): def tearDown(self): """Stop everything that was started.""" self.hass.stop() - self.cleanup() @requests_mock.Mocker() def test_sensor(self, mock): @@ -97,6 +84,13 @@ class TestRingSensorSetup(unittest.TestCase): ).result() for device in self.DEVICES: + # Mimick add to hass + device.hass = self.hass + run_coroutine_threadsafe( + device.async_added_to_hass(), self.hass.loop, + ).result() + + # Entity update data from ring data device.update() if device.name == "Front Battery": expected_icon = icon_for_battery_level( @@ -104,18 +98,12 @@ class TestRingSensorSetup(unittest.TestCase): ) assert device.icon == expected_icon assert 80 == device.state - assert "hp_cam_v1" == device.device_state_attributes["kind"] - assert "stickup_cams" == device.device_state_attributes["type"] if device.name == "Front Door Battery": assert 100 == device.state - assert "lpd_v1" == device.device_state_attributes["kind"] - assert "chimes" != device.device_state_attributes["type"] if device.name == "Downstairs Volume": assert 2 == device.state - assert "1.2.3" == device.device_state_attributes["firmware"] assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"] assert "mdi:bell-ring" == device.icon - assert "chimes" == device.device_state_attributes["type"] if device.name == "Front Door Last Activity": assert not device.device_state_attributes["answered"] assert "America/New_York" == device.device_state_attributes["timezone"] From 8b46b5591f23c5c97f77c6b4cc221c7d7e684579 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 12 Jan 2020 00:32:04 +0000 Subject: [PATCH 052/393] [ci skip] Translation update --- .../auth/.translations/zh-Hant.json | 4 +-- .../hangouts/.translations/zh-Hant.json | 4 +-- .../components/netatmo/.translations/da.json | 18 +++++++++++ .../components/netatmo/.translations/en.json | 30 +++++++++---------- .../components/netatmo/.translations/no.json | 5 ++++ .../components/netatmo/.translations/ru.json | 18 +++++++++++ .../netatmo/.translations/zh-Hant.json | 18 +++++++++++ .../components/ring/.translations/no.json | 22 ++++++++++++++ .../components/ring/.translations/ru.json | 27 +++++++++++++++++ .../ring/.translations/zh-Hant.json | 27 +++++++++++++++++ .../samsungtv/.translations/zh-Hant.json | 26 ++++++++++++++++ .../starline/.translations/zh-Hant.json | 2 +- 12 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/netatmo/.translations/da.json create mode 100644 homeassistant/components/netatmo/.translations/no.json create mode 100644 homeassistant/components/netatmo/.translations/ru.json create mode 100644 homeassistant/components/netatmo/.translations/zh-Hant.json create mode 100644 homeassistant/components/ring/.translations/no.json create mode 100644 homeassistant/components/ring/.translations/ru.json create mode 100644 homeassistant/components/ring/.translations/zh-Hant.json create mode 100644 homeassistant/components/samsungtv/.translations/zh-Hant.json 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/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/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/en.json b/homeassistant/components/netatmo/.translations/en.json index 8cd4f51aee2..9d69a3ece50 100644 --- a/homeassistant/components/netatmo/.translations/en.json +++ b/homeassistant/components/netatmo/.translations/en.json @@ -1,18 +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." + "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/no.json b/homeassistant/components/netatmo/.translations/no.json new file mode 100644 index 00000000000..a6dd368c8da --- /dev/null +++ b/homeassistant/components/netatmo/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "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..ba213fff2a6 --- /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": "Netatmo" + } + }, + "title": "Netatmo" + } +} \ 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/ring/.translations/no.json b/homeassistant/components/ring/.translations/no.json new file mode 100644 index 00000000000..63af6bdcba4 --- /dev/null +++ b/homeassistant/components/ring/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "unknown": "Uventet feil" + }, + "step": { + "2fa": { + "title": "To-faktor autentisering" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + }, + "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/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/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/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": { From 672db9cfe9852108ae2bd1a946c287bfec5345d4 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sat, 11 Jan 2020 17:58:06 -0800 Subject: [PATCH 053/393] Fixing unit of measure for withings hydration. Fixes #30570 (#30685) --- homeassistant/components/withings/sensor.py | 6 ++- tests/components/withings/test_common.py | 42 +++++++++------------ 2 files changed, 23 insertions(+), 25 deletions(-) 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/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 From abe727fbbc762bafca51248d95a3c5b1fe31473e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 18:21:57 -0800 Subject: [PATCH 054/393] Log error when integration is missing platform setup (#30690) --- homeassistant/helpers/entity_platform.py | 10 ++++++++++ tests/helpers/test_entity_platform.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index b8fef8deca2..0560cf84fb3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -83,6 +83,16 @@ class EntityPlatform: platform = self.platform hass = self.hass + if not hasattr(platform, "async_setup_platform") and not hasattr( + platform, "setup_platform" + ): + self.logger.error( + "The %s platform for the %s integration does not support platform setup. Please remove it from your config.", + self.platform_name, + self.domain, + ) + return + @callback def async_create_setup_task(): """Get task to set up platform.""" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 97534d7dff7..7797bf5057b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -834,3 +834,17 @@ async def test_override_restored_entities(hass): state = hass.states.get("test_domain.world") assert state.state == "on" + + +async def test_platform_with_no_setup(hass, caplog): + """Test setting up a platform that doesnt' support setup.""" + entity_platform = MockEntityPlatform( + hass, domain="mock-integration", platform_name="mock-platform", platform=None + ) + + await entity_platform.async_setup(None) + + assert ( + "The mock-platform platform for the mock-integration integration does not support platform setup." + in caplog.text + ) From 9266fc0cd763946bd7864da596ffdc619eaa9fe7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 18:22:08 -0800 Subject: [PATCH 055/393] Ring OTP improvement (#30688) * Fix otp flow * Update Ring to 0.5 Co-authored-by: steve-gombos <3118886+steve-gombos@users.noreply.github.com> --- homeassistant/components/ring/config_flow.py | 12 ++++-------- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 98555277baf..6d177a4db49 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Ring integration.""" import logging -from oauthlib.oauth2 import AccessDeniedError +from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol @@ -15,18 +15,14 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - def otp_callback(): - if "2fa" in data: - return data["2fa"] - - raise Require2FA - auth = Auth() try: token = await hass.async_add_executor_job( - auth.fetch_token, data["username"], data["password"], otp_callback, + auth.fetch_token, data["username"], data["password"], data.get("2fa"), ) + except MissingTokenError: + raise Require2FA except AccessDeniedError: raise InvalidAuth diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index d6570fad5cb..fccbf9a5319 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.4.0"], + "requirements": ["ring_doorbell==0.5.0"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 0c1159256c7..e1db0d36940 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1753,7 +1753,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.4.0 +ring_doorbell==0.5.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea60a13b565..7ffc1d21295 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.4.0 +ring_doorbell==0.5.0 # homeassistant.components.yamaha rxv==0.6.0 From a0b0dc0aca204deff542e2d396076ee6ec7113be Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sat, 11 Jan 2020 18:45:01 -0800 Subject: [PATCH 056/393] Optimistically set tplink light states (#30189) * Optimistically handling state changes. Using retries when command fail. * Fixing endless update loop. * Address PR comments. --- homeassistant/components/tplink/light.py | 362 ++++++++++++++++------- tests/components/tplink/test_light.py | 324 +++++++++++++------- 2 files changed, 475 insertions(+), 211 deletions(-) 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/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 From 4d6417295bafc3ec77b1e315714ddd32db772101 Mon Sep 17 00:00:00 2001 From: Bill Durr Date: Sat, 11 Jan 2020 22:42:14 -0500 Subject: [PATCH 057/393] ZHA cover device support (#30639) * ZHA cover device support * flake8 * flake8, black * isort * pylint * more test * use zigpy provided functions * black * handle command errors, better state handling * black * more test * lint * Update ZHA cover tests coverage. Co-authored-by: Alexei Chetroi --- .../components/zha/core/channels/closures.py | 33 +++- homeassistant/components/zha/core/const.py | 4 +- .../components/zha/core/registries.py | 2 + homeassistant/components/zha/cover.py | 176 ++++++++++++++++++ tests/components/zha/test_cover.py | 129 +++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/zha/cover.py create mode 100644 tests/components/zha/test_cover.py 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/const.py b/homeassistant/components/zha/core/const.py index 61be496fa1c..708a123d029 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 @@ -48,6 +49,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" @@ -71,7 +73,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" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 37acffd39d0..e89c0b8189b 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 @@ -63,6 +64,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, diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py new file mode 100644 index 00000000000..ef410308eb1 --- /dev/null +++ b/homeassistant/components/zha/cover.py @@ -0,0 +1,176 @@ +"""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_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up Zigbee Home Automation covers.""" + pass + + +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 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 + + 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() + + 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.get(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/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 + ) From 4dc39492a57c505c1915e679f06198204458763c Mon Sep 17 00:00:00 2001 From: inputd <38230664+inputd@users.noreply.github.com> Date: Sat, 11 Jan 2020 20:55:28 -0700 Subject: [PATCH 058/393] Drop timer component microseconds (#30198) * Update __init__.py --- homeassistant/components/timer/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index ccc04d7f72a..8eb3f8b353a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -184,7 +184,7 @@ 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: @@ -209,7 +209,7 @@ 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}) From 117efb5a0474e6e73645656237d6b07f732243a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 11 Jan 2020 19:59:43 -0800 Subject: [PATCH 059/393] Fix update person validation (#30691) --- homeassistant/components/person/__init__.py | 2 +- tests/components/person/test_init.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index fcf3bee45b9..c34fb89a718 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -214,7 +214,7 @@ class PersonStorageCollection(collection.StorageCollection): user_id = update_data.get(CONF_USER_ID) - if user_id is not None: + if user_id is not None and user_id != data.get(CONF_USER_ID): await self._validate_user_id(user_id) return {**data, **update_data} diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 5eaec6d5bf1..699fb58a539 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -514,6 +514,18 @@ async def test_ws_update(hass, hass_ws_client, storage_setup): "id": 6, "type": "person/update", "person_id": persons[0]["id"], + "user_id": persons[0]["user_id"], + } + ) + resp = await client.receive_json() + + assert resp["success"] + + resp = await client.send_json( + { + "id": 7, + "type": "person/update", + "person_id": persons[0]["id"], "name": "Updated Name", "device_trackers": [DEVICE_TRACKER_2], "user_id": None, From 91f738127d3a9442189e6a8559ce838567bc6cf8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 12 Jan 2020 05:00:02 +0100 Subject: [PATCH 060/393] Make met config flow tests robust (#30689) * Make met config flow tests more robust * Fix test with default form values * Remove not needed test * Clean create entry test * Clean double test * Clean last tests * Fix docstring --- tests/components/met/test_config_flow.py | 160 ++++++++--------------- 1 file changed, 58 insertions(+), 102 deletions(-) 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} From 030a399b0961607489d30e2c050ad77d33b9c44a Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 12 Jan 2020 05:00:49 +0100 Subject: [PATCH 061/393] Make hue config flow tests more robust (#30678) * Make hue config flow tests robust * Fix io on setup * Update last tests --- tests/components/hue/test_config_flow.py | 299 +++++++++++++---------- 1 file changed, 170 insertions(+), 129 deletions(-) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 5193d57ea6d..a5bf143775a 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,52 @@ 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" @@ -174,13 +216,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 +238,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 +260,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 +276,27 @@ 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" @@ -252,17 +305,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 +326,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 +368,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 +382,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,17 +402,15 @@ 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" @@ -382,11 +423,11 @@ async def test_bridge_homekit_already_configured(hass): 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" From 4c5ea54df94038e59332c82df92497c7573ef1b6 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 12 Jan 2020 05:01:13 +0100 Subject: [PATCH 062/393] Fix Error with HomematicIP Cloud Cover (#30667) --- homeassistant/components/homematicip_cloud/cover.py | 8 ++++++-- tests/components/homematicip_cloud/test_cover.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index e3efe9a9508..32f38637e36 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -58,7 +58,9 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @property def current_cover_position(self) -> int: """Return current position of cover.""" - return int((1 - self._device.shutterLevel) * 100) + if self._device.shutterLevel is not None: + return int((1 - self._device.shutterLevel) * 100) + return None async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" @@ -93,7 +95,9 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): @property def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" - return int((1 - self._device.slatsLevel) * 100) + if self._device.slatsLevel is not None: + return int((1 - self._device.slatsLevel) * 100) + return None async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 728d60d5501..5b267628ae3 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -7,7 +7,7 @@ from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, ) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN -from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.const import STATE_CLOSED, STATE_OPEN, STATE_UNKNOWN from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -87,7 +87,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_CLOSED + assert ha_state.state == STATE_UNKNOWN async def test_hmip_cover_slats(hass, default_mock_hap): @@ -154,7 +154,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_OPEN + assert ha_state.state == STATE_UNKNOWN async def test_hmip_garage_door_tormatic(hass, default_mock_hap): From 67299020108cdee1b67f1aa026fd562f1d257296 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 12 Jan 2020 16:37:34 +0100 Subject: [PATCH 063/393] Fix discovery for oauth2 flow implementations (#30700) * Fix discovery for oauth2 flow implementations * Fix user step * Add tests --- .../helpers/config_entry_oauth2_flow.py | 17 +++++- .../helpers/test_config_entry_oauth2_flow.py | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 2fdfea8673f..d29dae735f8 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -259,10 +259,21 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) + 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): + return self.async_abort(reason="already_configured") + + return await self.async_step_pick_implementation() + async_step_user = async_step_pick_implementation - async_step_ssdp = async_step_pick_implementation - async_step_zeroconf = async_step_pick_implementation - async_step_homekit = async_step_pick_implementation + async_step_ssdp = async_step_discovery + async_step_zeroconf = async_step_discovery + async_step_homekit = async_step_discovery @classmethod def async_register_implementation( diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 366c295874d..a72f3f51ee7 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -122,6 +122,64 @@ async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): assert result["reason"] == "authorize_url_timeout" +async def test_step_discovery(hass, flow_handler, local_impl): + """Check flow triggers from discovery.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + +async def test_abort_discovered_multiple(hass, flow_handler, local_impl): + """Test if aborts when discovered multiple times.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_in_progress" + + +async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl): + """Test if abort discovery when entries exists.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + entry = MockConfigEntry(domain=TEST_DOMAIN, data={},) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_SSDP} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_full_flow( hass, flow_handler, local_impl, aiohttp_client, aioclient_mock ): From c798413971248f5ada8eca2a378ca9e573039f5d Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 12 Jan 2020 17:55:11 +0100 Subject: [PATCH 064/393] update aiopylgtv to 0.2.5 (#30702) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index b154f5ef8ec..d842ada3fbb 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.4"], + "requirements": ["aiopylgtv==0.2.5"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index e1db0d36940..b97b106eebc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.4 +aiopylgtv==0.2.5 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ffc1d21295..ff228d57bb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.4 +aiopylgtv==0.2.5 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 5f5e8d81e118957453ce8367d910a783cccc5ba2 Mon Sep 17 00:00:00 2001 From: Bogdan Vlaicu Date: Sun, 12 Jan 2020 11:55:33 -0500 Subject: [PATCH 065/393] Bumped oru to 0.1.11 (adds a timeout to the api request) (#30704) --- homeassistant/components/oru/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index b97b106eebc..bdd79669201 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 From 96bf8bc395f7c6bccc7b2f00e03f170acfa39c4c Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Sun, 12 Jan 2020 20:30:26 +0100 Subject: [PATCH 066/393] Update NSAPI to version 3.0.0 (#30599) * Updated NSAPI to version 3.0.0 * Fixed code style error * Restructured API access * Improved construction of attributes * Fixed handling of API output * Added @YarmoM as code owner * Updated CODEOWNERS * Reverted changes for full backwards compatibility * Fixed bad conditional * Simplify conditional --- CODEOWNERS | 1 + .../nederlandse_spoorwegen/manifest.json | 4 +- .../nederlandse_spoorwegen/sensor.py | 80 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 54 insertions(+), 33 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fa805e6f6ae..3371dc62a5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -219,6 +219,7 @@ 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 diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 322452f5f59..92231bd460c 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.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 0b823962373..5477aaf0e2b 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,18 +37,14 @@ 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 ( @@ -128,40 +124,59 @@ 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, + "departure_time_actual": None, + "departure_delay": False, + "departure_platform_planned": self._trips[0].departure_platform_planned, + "departure_platform_actual": None, "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"), + "arrival_time_actual": None, + "arrival_delay": False, + "arrival_platform_platform": self._trips[0].arrival_platform_planned, + "arrival_platform_actual": None, + "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, } + # 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") + attributes["departure_delay"] = True + attributes["departure_platform_actual"] = self._trips[ + 0 + ].departure_platform_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") + attributes["arrival_delay"] = True + attributes["arrival_platform_actual"] = self._trips[ + 0 + ].arrival_platform_actual + + # 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 +188,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/requirements_all.txt b/requirements_all.txt index bdd79669201..2210acb3359 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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.0 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 89c04c94e069a97a2a4f1f6d05765febef4428d5 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sun, 12 Jan 2020 14:21:07 -0600 Subject: [PATCH 067/393] Revert "Forget auth token when going offline so we can reconnect (#26630)" (#30705) This reverts commit 2d6d6ba90e5e2f179a07c4ecd7de1744e86a8025. --- homeassistant/components/amcrest/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index b934a7e0549..63daeb04731 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -132,8 +132,6 @@ class AmcrestChecker(Http): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - with self._token_lock: - self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) From 0534153ae7ffe322c671815544717902f5cb8ca5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 13 Jan 2020 00:31:54 +0000 Subject: [PATCH 068/393] [ci skip] Translation update --- .../components/adguard/.translations/de.json | 4 +-- .../alarm_control_panel/.translations/de.json | 7 +++++ .../components/almond/.translations/de.json | 4 +-- .../components/brother/.translations/de.json | 24 ++++++++++++++++ .../cert_expiry/.translations/de.json | 2 +- .../coolmaster/.translations/de.json | 4 +-- .../components/daikin/.translations/de.json | 2 +- .../components/deconz/.translations/de.json | 11 ++++++-- .../dialogflow/.translations/de.json | 4 +-- .../components/ecobee/.translations/de.json | 2 +- .../components/elgato/.translations/de.json | 6 ++-- .../components/esphome/.translations/de.json | 8 +++--- .../components/geofency/.translations/de.json | 2 +- .../geonetnz_quakes/.translations/de.json | 2 +- .../geonetnz_volcano/.translations/de.json | 2 +- .../components/gios/.translations/de.json | 7 ++++- .../components/glances/.translations/de.json | 6 ++-- .../gpslogger/.translations/de.json | 2 +- .../components/hangouts/.translations/de.json | 2 +- .../hisense_aehw4a1/.translations/de.json | 2 +- .../homekit_controller/.translations/de.json | 4 +-- .../huawei_lte/.translations/de.json | 6 ++-- .../iaqualink/.translations/de.json | 2 +- .../components/icloud/.translations/de.json | 10 +++---- .../components/izone/.translations/de.json | 2 +- .../components/locative/.translations/de.json | 2 +- .../components/mailgun/.translations/de.json | 4 +-- .../components/netatmo/.translations/de.json | 18 ++++++++++++ .../owntracks/.translations/de.json | 4 +-- .../components/plex/.translations/de.json | 4 +-- .../components/point/.translations/de.json | 10 +++---- .../components/ps4/.translations/de.json | 8 +++--- .../components/ring/.translations/de.json | 27 ++++++++++++++++++ .../samsungtv/.translations/de.json | 26 +++++++++++++++++ .../components/sensor/.translations/de.json | 28 +++++++++---------- .../components/sentry/.translations/de.json | 2 +- .../smartthings/.translations/de.json | 2 +- .../components/solarlog/.translations/de.json | 4 +-- .../components/starline/.translations/de.json | 2 +- .../tellduslive/.translations/de.json | 6 ++-- .../components/tesla/.translations/de.json | 4 +-- .../components/tplink/.translations/de.json | 2 +- .../components/traccar/.translations/de.json | 2 +- .../transmission/.translations/de.json | 2 +- .../twentemilieu/.translations/de.json | 2 +- .../components/twilio/.translations/de.json | 4 +-- .../components/upnp/.translations/de.json | 2 +- .../components/withings/.translations/de.json | 4 +-- .../components/wled/.translations/de.json | 6 ++-- 49 files changed, 208 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/brother/.translations/de.json create mode 100644 homeassistant/components/netatmo/.translations/de.json create mode 100644 homeassistant/components/ring/.translations/de.json create mode 100644 homeassistant/components/samsungtv/.translations/de.json 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/icloud/.translations/de.json b/homeassistant/components/icloud/.translations/de.json index ac9a401e70d..a9be5a16dce 100644 --- a/homeassistant/components/icloud/.translations/de.json +++ b/homeassistant/components/icloud/.translations/de.json @@ -4,17 +4,17 @@ "username_exists": "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 +22,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" } }, From 123bef4f1ea53fd6387f2c6148d66b420aa9a8a6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 13 Jan 2020 04:42:37 +0100 Subject: [PATCH 069/393] Fix SynologyDSM sensor if network_sensors is None (#30718) --- homeassistant/components/synologydsm/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index d415d009252..3f459af9887 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -231,6 +231,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: From b585feb10999779688a5e7194e51acdf240df416 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 12 Jan 2020 21:32:08 -0700 Subject: [PATCH 070/393] Consolidate SimpliSafe property services (#30567) * Consolidate SimpliSafe property services * Code review comments * Code review comments * Ensure all services are admin services * Code review comments --- .../components/simplisafe/__init__.py | 138 +++++++----------- .../simplisafe/alarm_control_panel.py | 16 +- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/services.yaml | 75 ++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 96 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 63ac0ca973c..52517b7d25f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -5,7 +5,7 @@ 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 @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, - STATE_HOME, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -30,7 +29,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 +43,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 +65,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, } ) @@ -246,47 +250,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 +264,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 diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 05dad43955c..2cb6c4b41c5 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, @@ -48,6 +49,13 @@ ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" +VOLUME_STRING_MAP = { + VOLUME_HIGH: "high", + VOLUME_LOW: "low", + VOLUME_MEDIUM: "medium", + VOLUME_OFF: "off", +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a SimpliSafe alarm control panel based on existing config.""" @@ -82,9 +90,9 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): 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 +100,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, } diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index ccb822e7f45..f7f6fce0c74 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.0.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/requirements_all.txt b/requirements_all.txt index 2210acb3359..073737f6fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1819,7 +1819,7 @@ shodan==1.21.2 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.3.6 +simplisafe-python==6.0.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff228d57bb3..62b887eb900 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -582,7 +582,7 @@ samsungctl[websocket]==0.7.1 sentry-sdk==0.13.5 # homeassistant.components.simplisafe -simplisafe-python==5.3.6 +simplisafe-python==6.0.0 # homeassistant.components.sleepiq sleepyq==0.7 From 96c11bc6d7ca21b2eb6372725ab41b2aaa95a0ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 05:47:07 +0100 Subject: [PATCH 071/393] Unhide SPC binary sensors by default (#30699) --- homeassistant/components/spc/binary_sensor.py | 7 ------- 1 file changed, 7 deletions(-) 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.""" From ce13fb8d73fe90ebb1a847af5543db1e78193953 Mon Sep 17 00:00:00 2001 From: zewelor Date: Mon, 13 Jan 2020 06:03:48 +0100 Subject: [PATCH 072/393] Support yeelight color light with nightlight (#30194) * Support color light with nightlight * Better nightlight mode support check * Lint fixes * Remove brightness control for color light with nightlight mode --- homeassistant/components/yeelight/__init__.py | 31 +++++-- homeassistant/components/yeelight/light.py | 85 ++++++++++++++++--- 2 files changed, 94 insertions(+), 22 deletions(-) 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) From 15645ab0c9a8dfa9538b172ee4d3a993cdab2614 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 13 Jan 2020 13:28:07 +0100 Subject: [PATCH 073/393] Add unique ID to Airly config entries (#30681) * Add unique ID to Airly config entries * Update tests * Update tests * Fix typo * Remove unnecesary and undo changes in first test * Suggested change --- homeassistant/components/airly/__init__.py | 6 +++ homeassistant/components/airly/air_quality.py | 7 +--- homeassistant/components/airly/config_flow.py | 21 ++++------ homeassistant/components/airly/sensor.py | 6 +-- homeassistant/components/airly/strings.json | 4 +- tests/components/airly/test_config_flow.py | 42 ++++++++++--------- 6 files changed, 43 insertions(+), 43 deletions(-) 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/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] From 56dc5298fcad0c8429fadb10874b58ece6cc7b7b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 13:50:01 +0100 Subject: [PATCH 074/393] Remove hidden property from xiaomi_miio.remote integration (#30727) --- .../components/xiaomi_miio/remote.py | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) 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( From 3df329b8ec47d2a7a9e4dd027a41fcc3a7686575 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:20:09 +0100 Subject: [PATCH 075/393] Bump apprise to 0.8.3 (#30731) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 6e8a0567d06..9d79c2554ec 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/components/apprise", - "requirements": ["apprise==0.8.2"], + "requirements": ["apprise==0.8.3"], "dependencies": [], "codeowners": ["@caronc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 073737f6fb9..aaf2b76701d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62b887eb900..a4aa26bdccf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,7 +93,7 @@ androidtv==0.0.38 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.2 +apprise==0.8.3 # homeassistant.components.aprs aprslib==0.6.46 From c8a52d45668eb559d215b59d2308e379bb6e6b70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:22:06 +0100 Subject: [PATCH 076/393] Remove hidden property from egardia integration (#30728) --- homeassistant/components/egardia/binary_sensor.py | 6 ------ 1 file changed, 6 deletions(-) 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.""" From 0c14d4e067293d68ebad164488fb8b7194ccbda6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:22:48 +0100 Subject: [PATCH 077/393] Remove hidden property from emby integration (#30729) --- homeassistant/components/emby/media_player.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 57f781deceb..2fd1014dcdf 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.""" From 7143ed7ceb3011ada6f8c8b850dc753f6ca74df5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:23:08 +0100 Subject: [PATCH 078/393] Remove hidden property from fibaro integration (#30730) --- homeassistant/components/fibaro/__init__.py | 5 ----- 1 file changed, 5 deletions(-) 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.""" From 3c2b57afaf4290929bf6a03acd8feb5d05ae75a0 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Mon, 13 Jan 2020 08:27:53 -0500 Subject: [PATCH 079/393] Bump env_canada to 0.0.34 (#30713) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index aaf2b76701d..15aedf18a45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -486,7 +486,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 From 1b739f9555706abf108085dbbb97e2966bcc7e29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:29:19 +0100 Subject: [PATCH 080/393] Removes unneeded abort if unique id in Oauth2 discovery flow (#30733) --- homeassistant/helpers/config_entry_oauth2_flow.py | 1 - 1 file changed, 1 deletion(-) 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): From 1f9d6ba541200e63074d45bb6efa99e763b2f420 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 05:30:07 -0800 Subject: [PATCH 081/393] Update Hue SSDP discovery (#30695) --- homeassistant/components/hue/manifest.json | 7 ++++++- homeassistant/generated/ssdp.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index f6b14ec69d4..f8d7295a173 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -6,7 +6,12 @@ "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" } ], "homekit": { diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 01e0726ce54..5e09a241a9e 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -24,7 +24,12 @@ SSDP = { ], "hue": [ { - "manufacturer": "Royal Philips Electronics" + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2012" + }, + { + "manufacturer": "Royal Philips Electronics", + "modelName": "Philips hue bridge 2015" } ], "samsungtv": [ From 040b283a14cb5b58b052155029d2a34a86dedc0c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 14:36:47 +0100 Subject: [PATCH 082/393] Fix hassfest allowing omitting discovery methods when using OAuth2Flow (#30732) --- script/hassfest/ssdp.py | 1 + script/hassfest/zeroconf.py | 3 +++ 2 files changed, 4 insertions(+) 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( From f50714d7e99abe771a1cb634c01d5cb7950f861b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 06:20:25 -0800 Subject: [PATCH 083/393] Update config flow test scaffolding (#30694) * Update config flow test scaffolding * asyncpatch -> patch * Update classname --- .../config_flow/integration/config_flow.py | 31 +++++++++++++++++-- .../config_flow/tests/test_config_flow.py | 19 +++++------- 2 files changed, 37 insertions(+), 13 deletions(-) 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( From 10e89bbc8cc6c6d60724df02749effe725b41a02 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 09:13:46 -0800 Subject: [PATCH 084/393] Fix Ring wifi sensors (#30735) * Fix Ring wifi sensors * Update before adding --- homeassistant/components/ring/sensor.py | 27 +++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 89b042ba862..874c056ec7d 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -69,18 +69,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for device in ring_chimes: for sensor_type in SENSOR_TYPES: - if "chime" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "chime" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_doorbells: for sensor_type in SENSOR_TYPES: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "doorbell" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) for device in ring_stickup_cams: for sensor_type in SENSOR_TYPES: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if "stickup_cams" not in SENSOR_TYPES[sensor_type][1]: + continue + + if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): + await hass.async_add_executor_job(device.update) + + sensors.append(RingSensor(hass, device, sensor_type)) async_add_entities(sensors, True) From 1b730b3055ab6b0e8ee68d169808ec74eb02fd66 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 13 Jan 2020 13:10:22 -0500 Subject: [PATCH 085/393] Bump ZHA quirks to 0.0.31 (#30740) * Bump ZHA quirks version * update requirements --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3beca6fd3c5..e3d0eda3e02 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.12.0", - "zha-quirks==0.0.30", + "zha-quirks==0.0.31", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.12.0", "zigpy-xbee-homeassistant==0.8.0", diff --git a/requirements_all.txt b/requirements_all.txt index 15aedf18a45..cde951a37b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2112,7 +2112,7 @@ zengge==0.2 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.30 +zha-quirks==0.0.31 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4aa26bdccf..bacb75105ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,7 +675,7 @@ yahooweather==0.10 zeroconf==0.24.4 # homeassistant.components.zha -zha-quirks==0.0.30 +zha-quirks==0.0.31 # homeassistant.components.zha zigpy-deconz==0.7.0 From f639a8fbaabe145fdbf6295660b4b65bb1437dc3 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Mon, 13 Jan 2020 19:11:06 +0100 Subject: [PATCH 086/393] update aiopylgtv to 0.2.6 (#30739) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index d842ada3fbb..ddd7be6f3da 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.5"], + "requirements": ["aiopylgtv==0.2.6"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index cde951a37b1..353345e2a60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.5 +aiopylgtv==0.2.6 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bacb75105ef..f6352040949 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.5 +aiopylgtv==0.2.6 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 658d33805828d0de0a02f3f2962c15526291c248 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jan 2020 19:23:26 +0100 Subject: [PATCH 087/393] Removes Cisco Spark integration (#30738) --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/ciscospark/__init__.py | 1 - .../components/ciscospark/manifest.json | 8 --- homeassistant/components/ciscospark/notify.py | 52 ------------------- requirements_all.txt | 3 -- 6 files changed, 66 deletions(-) delete mode 100644 homeassistant/components/ciscospark/__init__.py delete mode 100644 homeassistant/components/ciscospark/manifest.json delete mode 100644 homeassistant/components/ciscospark/notify.py diff --git a/.coveragerc b/.coveragerc index be11fa5998c..3afc6b4fd3e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -116,7 +116,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 diff --git a/CODEOWNERS b/CODEOWNERS index 3371dc62a5e..c06f9c07d76 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -59,7 +59,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 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/requirements_all.txt b/requirements_all.txt index 353345e2a60..6684969490e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 From 75f0fedd68b5bf6defc98abe70e204535f9e0902 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 13 Jan 2020 23:52:49 +0100 Subject: [PATCH 088/393] Fix translation from HA playlist to UPnP playlistItem (#30743) --- homeassistant/components/dlna_dmr/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = { From f9fe91ee7479c43d836369bce16bda0c9c0f5269 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 14 Jan 2020 00:31:46 +0000 Subject: [PATCH 089/393] [ci skip] Translation update --- .../components/airly/.translations/da.json | 3 +++ .../components/airly/.translations/de.json | 3 +++ .../components/airly/.translations/en.json | 3 +++ .../components/airly/.translations/it.json | 3 +++ .../components/airly/.translations/no.json | 3 +++ .../airly/.translations/zh-Hant.json | 3 +++ .../components/deconz/.translations/ko.json | 4 ++- .../components/netatmo/.translations/es.json | 18 +++++++++++++ .../components/netatmo/.translations/it.json | 18 +++++++++++++ .../components/netatmo/.translations/lb.json | 18 +++++++++++++ .../components/netatmo/.translations/no.json | 13 +++++++++ .../components/ring/.translations/es.json | 27 +++++++++++++++++++ .../components/ring/.translations/it.json | 27 +++++++++++++++++++ .../components/ring/.translations/lb.json | 27 +++++++++++++++++++ .../components/ring/.translations/no.json | 7 ++++- .../samsungtv/.translations/pl.json | 26 ++++++++++++++++++ 16 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/netatmo/.translations/es.json create mode 100644 homeassistant/components/netatmo/.translations/it.json create mode 100644 homeassistant/components/netatmo/.translations/lb.json create mode 100644 homeassistant/components/ring/.translations/es.json create mode 100644 homeassistant/components/ring/.translations/it.json create mode 100644 homeassistant/components/ring/.translations/lb.json create mode 100644 homeassistant/components/samsungtv/.translations/pl.json 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/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/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/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/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 5cf1cb32ca2..e50dca926cc 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -85,7 +85,9 @@ "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/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/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/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/no.json b/homeassistant/components/netatmo/.translations/no.json index a6dd368c8da..68a91633642 100644 --- a/homeassistant/components/netatmo/.translations/no.json +++ b/homeassistant/components/netatmo/.translations/no.json @@ -1,5 +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/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/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/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/no.json b/homeassistant/components/ring/.translations/no.json index 63af6bdcba4..27dd7438f4a 100644 --- a/homeassistant/components/ring/.translations/no.json +++ b/homeassistant/components/ring/.translations/no.json @@ -4,17 +4,22 @@ "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" 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 From 1e3b3ecbe6934a36ef351b4d71d3668f72a46c9c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 13 Jan 2020 23:33:45 -0800 Subject: [PATCH 090/393] Set default locale for cloud Alexa config (#30749) --- homeassistant/components/alexa/config.py | 6 ++++-- homeassistant/components/cloud/alexa_config.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index a6e45c61953..bd579dc4dad 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,10 +1,12 @@ """Config helpers for Alexa.""" +from abc import ABC, abstractmethod + from homeassistant.core import callback from .state_report import async_enable_proactive_mode -class AbstractConfig: +class AbstractConfig(ABC): """Hold the configuration for Alexa.""" _unsub_proactive_report = None @@ -29,9 +31,9 @@ class AbstractConfig: return None @property + @abstractmethod def locale(self): """Return config locale.""" - return None @property def entity_config(self): diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 45e1fab1101..8d1527b1930 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -78,6 +78,12 @@ class AlexaConfig(alexa_config.AbstractConfig): return self._endpoint + @property + def locale(self): + """Return config locale.""" + # Not clear how to determine locale atm. + return "en-US" + @property def entity_config(self): """Return entity config.""" From 0d688faa56e062e9d7952471e077004082baa5c4 Mon Sep 17 00:00:00 2001 From: Bas Delfos Date: Tue, 14 Jan 2020 09:46:16 +0100 Subject: [PATCH 091/393] Fix 'NewIPAddress' error in component fritz (#30210) * Fix 'NewIPAddress' error in component fritzbox * Upgrade to fritzconnection 1.2.0 --- homeassistant/components/fritz/device_tracker.py | 4 ++-- homeassistant/components/fritz/manifest.json | 2 +- .../components/fritzbox_callmonitor/manifest.json | 2 +- .../components/fritzbox_callmonitor/sensor.py | 4 ++-- .../components/fritzbox_netmonitor/manifest.json | 2 +- .../components/fritzbox_netmonitor/sensor.py | 11 +++-------- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 - 8 files changed, 11 insertions(+), 17 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 6684969490e..4b98d7a460f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e64427baf71..fc539a97f9f 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", From ced6df158bd66acace2e711c73a15760a95e8a02 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 11:26:59 +0100 Subject: [PATCH 092/393] Refactor HomeMatic / Fix issue with 0.104/dev (#30752) * Refactor HomeMatic / Fix issue with 0.104/dev * Fix lock --- .../components/homematic/__init__.py | 552 ++---------------- .../components/homematic/binary_sensor.py | 6 +- homeassistant/components/homematic/climate.py | 5 +- homeassistant/components/homematic/const.py | 212 +++++++ homeassistant/components/homematic/cover.py | 10 +- homeassistant/components/homematic/entity.py | 297 ++++++++++ homeassistant/components/homematic/light.py | 5 +- homeassistant/components/homematic/lock.py | 8 +- homeassistant/components/homematic/notify.py | 3 +- homeassistant/components/homematic/sensor.py | 8 +- homeassistant/components/homematic/switch.py | 10 +- 11 files changed, 595 insertions(+), 521 deletions(-) create mode 100644 homeassistant/components/homematic/const.py create mode 100644 homeassistant/components/homematic/entity.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 01bc94ce58f..24c9e37a3be 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,5 +1,5 @@ """Support for HomeMatic devices.""" -from datetime import datetime, timedelta +from datetime import datetime from functools import partial import logging @@ -18,232 +18,68 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVERY_TYPE, + ATTR_ERRORCODE, + ATTR_INTERFACE, + ATTR_LOW_BAT, + ATTR_LOWBAT, + ATTR_MESSAGE, + ATTR_PARAM, + ATTR_PARAMSET, + ATTR_PARAMSET_KEY, + ATTR_TIME, + ATTR_UNIQUE_ID, + ATTR_VALUE, + ATTR_VALUE_TYPE, + CONF_CALLBACK_IP, + CONF_CALLBACK_PORT, + CONF_INTERFACES, + CONF_JSONPORT, + CONF_LOCAL_IP, + CONF_LOCAL_PORT, + CONF_PATH, + CONF_PORT, + CONF_RESOLVENAMES, + CONF_RESOLVENAMES_OPTIONS, + DATA_CONF, + DATA_HOMEMATIC, + DATA_STORE, + DISCOVER_BATTERY, + DISCOVER_BINARY_SENSORS, + DISCOVER_CLIMATE, + DISCOVER_COVER, + DISCOVER_LIGHTS, + DISCOVER_LOCKS, + DISCOVER_SENSORS, + DISCOVER_SWITCHES, + DOMAIN, + EVENT_ERROR, + EVENT_IMPULSE, + EVENT_KEYPRESS, + HM_DEVICE_TYPES, + HM_IGNORE_DISCOVERY_NODE, + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS, + HM_IMPULSE_EVENTS, + HM_PRESS_EVENTS, + SERVICE_PUT_PARAMSET, + SERVICE_RECONNECT, + SERVICE_SET_DEVICE_VALUE, + SERVICE_SET_INSTALL_MODE, + SERVICE_SET_VARIABLE_VALUE, + SERVICE_VIRTUALKEY, +) +from .entity import HMHub _LOGGER = logging.getLogger(__name__) -DOMAIN = "homematic" - -SCAN_INTERVAL_HUB = timedelta(seconds=300) -SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) - -DISCOVER_SWITCHES = "homematic.switch" -DISCOVER_LIGHTS = "homematic.light" -DISCOVER_SENSORS = "homematic.sensor" -DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" -DISCOVER_COVER = "homematic.cover" -DISCOVER_CLIMATE = "homematic.climate" -DISCOVER_LOCKS = "homematic.locks" -DISCOVER_BATTERY = "homematic.battery" - -ATTR_DISCOVER_DEVICES = "devices" -ATTR_PARAM = "param" -ATTR_CHANNEL = "channel" -ATTR_ADDRESS = "address" -ATTR_VALUE = "value" -ATTR_VALUE_TYPE = "value_type" -ATTR_INTERFACE = "interface" -ATTR_ERRORCODE = "error" -ATTR_MESSAGE = "message" -ATTR_TIME = "time" -ATTR_UNIQUE_ID = "unique_id" -ATTR_PARAMSET_KEY = "paramset_key" -ATTR_PARAMSET = "paramset" -ATTR_DISCOVERY_TYPE = "discovery_type" -ATTR_LOW_BAT = "LOW_BAT" -ATTR_LOWBAT = "LOWBAT" - - -EVENT_KEYPRESS = "homematic.keypress" -EVENT_IMPULSE = "homematic.impulse" -EVENT_ERROR = "homematic.error" - -SERVICE_VIRTUALKEY = "virtualkey" -SERVICE_RECONNECT = "reconnect" -SERVICE_SET_VARIABLE_VALUE = "set_variable_value" -SERVICE_SET_DEVICE_VALUE = "set_device_value" -SERVICE_SET_INSTALL_MODE = "set_install_mode" -SERVICE_PUT_PARAMSET = "put_paramset" - -HM_DEVICE_TYPES = { - DISCOVER_SWITCHES: [ - "Switch", - "SwitchPowermeter", - "IOSwitch", - "IPSwitch", - "RFSiren", - "IPSwitchPowermeter", - "HMWIOSwitch", - "Rain", - "EcoLogic", - "IPKeySwitchPowermeter", - "IPGarage", - "IPKeySwitch", - "IPKeySwitchLevel", - "IPMultiIO", - ], - DISCOVER_LIGHTS: [ - "Dimmer", - "KeyDimmer", - "IPKeyDimmer", - "IPDimmer", - "ColorEffectLight", - "IPKeySwitchLevel", - ], - DISCOVER_SENSORS: [ - "SwitchPowermeter", - "Motion", - "MotionV2", - "RemoteMotion", - "MotionIP", - "ThermostatWall", - "AreaThermostat", - "RotaryHandleSensor", - "WaterSensor", - "PowermeterGas", - "LuxSensor", - "WeatherSensor", - "WeatherStation", - "ThermostatWall2", - "TemperatureDiffSensor", - "TemperatureSensor", - "CO2Sensor", - "IPSwitchPowermeter", - "HMWIOSwitch", - "FillingLevel", - "ValveDrive", - "EcoLogic", - "IPThermostatWall", - "IPSmoke", - "RFSiren", - "PresenceIP", - "IPAreaThermostat", - "IPWeatherSensor", - "RotaryHandleSensorIP", - "IPPassageSensor", - "IPKeySwitchPowermeter", - "IPThermostatWall230V", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPBrightnessSensor", - "IPGarage", - "UniversalSensor", - "MotionIPV2", - "IPMultiIO", - "IPThermostatWall2", - ], - DISCOVER_CLIMATE: [ - "Thermostat", - "ThermostatWall", - "MAXThermostat", - "ThermostatWall2", - "MAXWallThermostat", - "IPThermostat", - "IPThermostatWall", - "ThermostatGroup", - "IPThermostatWall230V", - "IPThermostatWall2", - ], - DISCOVER_BINARY_SENSORS: [ - "ShutterContact", - "Smoke", - "SmokeV2", - "Motion", - "MotionV2", - "MotionIP", - "RemoteMotion", - "WeatherSensor", - "TiltSensor", - "IPShutterContact", - "HMWIOSwitch", - "MaxShutterContact", - "Rain", - "WiredSensor", - "PresenceIP", - "IPWeatherSensor", - "IPPassageSensor", - "SmartwareMotion", - "IPWeatherSensorPlus", - "MotionIPV2", - "WaterIP", - "IPMultiIO", - "TiltIP", - "IPShutterContactSabotage", - "IPContact", - ], - DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], - DISCOVER_LOCKS: ["KeyMatic"], -} - -HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] - -HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - "ACTUAL_TEMPERATURE": [ - "IPAreaThermostat", - "IPWeatherSensor", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPThermostatWall", - "IPThermostatWall2", - ] -} - -HM_ATTRIBUTE_SUPPORT = { - "LOWBAT": ["battery", {0: "High", 1: "Low"}], - "LOW_BAT": ["battery", {0: "High", 1: "Low"}], - "ERROR": ["error", {0: "No"}], - "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "RSSI_PEER": ["rssi_peer", {}], - "RSSI_DEVICE": ["rssi_device", {}], - "VALVE_STATE": ["valve", {}], - "LEVEL": ["level", {}], - "BATTERY_STATE": ["battery", {}], - "CONTROL_MODE": [ - "mode", - {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, - ], - "POWER": ["power", {}], - "CURRENT": ["current", {}], - "VOLTAGE": ["voltage", {}], - "OPERATING_VOLTAGE": ["voltage", {}], - "WORKING": ["working", {0: "No", 1: "Yes"}], - "STATE_UNCERTAIN": ["state_uncertain", {}], -} - -HM_PRESS_EVENTS = [ - "PRESS_SHORT", - "PRESS_LONG", - "PRESS_CONT", - "PRESS_LONG_RELEASE", - "PRESS", -] - -HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] - -CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] - -DATA_HOMEMATIC = "homematic" -DATA_STORE = "homematic_store" -DATA_CONF = "homematic_conf" - -CONF_INTERFACES = "interfaces" -CONF_LOCAL_IP = "local_ip" -CONF_LOCAL_PORT = "local_port" -CONF_PORT = "port" -CONF_PATH = "path" -CONF_CALLBACK_IP = "callback_ip" -CONF_CALLBACK_PORT = "callback_port" -CONF_RESOLVENAMES = "resolvenames" -CONF_JSONPORT = "jsonport" -CONF_VARIABLES = "variables" -CONF_DEVICES = "devices" -CONF_PRIMARY = "primary" - DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False @@ -776,277 +612,3 @@ def _device_from_servicecall(hass, service): for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] - - -class HMHub(Entity): - """The HomeMatic hub. (CCU2/HomeGear).""" - - def __init__(self, hass, homematic, name): - """Initialize HomeMatic hub.""" - self.hass = hass - self.entity_id = "{}.{}".format(DOMAIN, name.lower()) - self._homematic = homematic - self._variables = {} - self._name = name - self._state = None - - # Load data - self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) - self.hass.add_job(self._update_hub, None) - - self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES - ) - self.hass.add_job(self._update_variables, None) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return false. HomeMatic Hub object updates variables.""" - return False - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def state_attributes(self): - """Return the state attributes.""" - attr = self._variables.copy() - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:gradient" - - def _update_hub(self, now): - """Retrieve latest state.""" - service_message = self._homematic.getServiceMessages(self._name) - state = None if service_message is None else len(service_message) - - # state have change? - if self._state != state: - self._state = state - self.schedule_update_ha_state() - - def _update_variables(self, now): - """Retrieve all variable data and update hmvariable states.""" - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - state_change = False - for key, value in variables.items(): - if key in self._variables and value == self._variables[key]: - continue - - state_change = True - self._variables.update({key: value}) - - if state_change: - self.schedule_update_ha_state() - - def hm_set_variable(self, name, value): - """Set variable value on CCU/Homegear.""" - if name not in self._variables: - _LOGGER.error("Variable %s not found on %s", name, self.name) - return - old_value = self._variables.get(name) - if isinstance(old_value, bool): - value = cv.boolean(value) - else: - value = float(value) - self._homematic.setSystemVariable(self.name, name, value) - - self._variables.update({name: value}) - self.schedule_update_ha_state() - - -class HMDevice(Entity): - """The HomeMatic device base object.""" - - def __init__(self, config): - """Initialize a generic HomeMatic device.""" - self._name = config.get(ATTR_NAME) - self._address = config.get(ATTR_ADDRESS) - self._interface = config.get(ATTR_INTERFACE) - self._channel = config.get(ATTR_CHANNEL) - self._state = config.get(ATTR_PARAM) - self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None - self._connected = False - self._available = False - - # Set parameter to uppercase - if self._state: - self._state = self._state.upper() - - async def async_added_to_hass(self): - """Load data init callbacks.""" - await self.hass.async_add_job(self.link_homematic) - - @property - def unique_id(self): - """Return unique ID. HomeMatic entity IDs are unique by default.""" - return self._unique_id.replace(" ", "_") - - @property - def should_poll(self): - """Return false. HomeMatic states are pushed by the XML-RPC Server.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = {} - - # Generate a dictionary with attributes - for node, data in HM_ATTRIBUTE_SUPPORT.items(): - # Is an attribute and exists for this object - if node in self._data: - value = data[1].get(self._data[node], self._data[node]) - attr[data[0]] = value - - # Static attributes - attr["id"] = self._hmdevice.ADDRESS - attr["interface"] = self._interface - - return attr - - def link_homematic(self): - """Connect to HomeMatic.""" - if self._connected: - return True - - # Initialize - self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = self._homematic.devices[self._interface][self._address] - self._connected = True - - try: - # Initialize datapoints of this object - self._init_data() - self._load_data_from_hm() - - # Link events from pyhomematic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH - except Exception as err: # pylint: disable=broad-except - self._connected = False - _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) - - 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 - - # Availability has changed - if self.available != (not self._hmdevice.UNREACH): - self._available = not self._hmdevice.UNREACH - has_changed = True - - # If it has changed data point, update Home Assistant - if has_changed: - self.schedule_update_ha_state() - - 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, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, - self._hmdevice.EVENTNODE, - self._hmdevice.ACTIONNODE, - ): - for node, channels in metadata.items(): - # Data is needed for this instance - if node in self._data: - # chan is current channel - if len(channels) == 1: - 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) - - # 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 - ) - - def _load_data_from_hm(self): - """Load first value from pyhomematic.""" - if not self._connected: - return False - - # Read data from pyhomematic - for metadata, funct in ( - (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), - (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), - (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), - (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), - ): - for node in metadata: - if metadata[node] and node in self._data: - self._data[node] = funct(name=node, channel=self._channel) - - return True - - def _hm_set_state(self, value): - """Set data to main datapoint.""" - if self._state in self._data: - self._data[self._state] = value - - def _hm_get_state(self): - """Get data from main datapoint.""" - if self._state in self._data: - return self._data[self._state] - return None - - def _init_data(self): - """Generate a data dict (self._data) from the HomeMatic metadata.""" - # Add all attributes to data dictionary - for data_note in self._hmdevice.ATTRIBUTENODE: - self._data.update({data_note: STATE_UNKNOWN}) - - # Initialize device specific data - self._init_data_struct() - - def _init_data_struct(self): - """Generate a data dictionary from the HomeMatic device metadata.""" - raise NotImplementedError diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 1832652406d..731525c8460 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -9,9 +9,9 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorDevice, ) -from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: devices.append(HMBinarySensor(conf)) - add_entities(devices) + add_entities(devices, True) class HMBinarySensor(HMDevice, BinarySensorDevice): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 935ebb9b497..b4ab277a75b 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -14,7 +14,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice +from .const import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMThermostat(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMThermostat(HMDevice, ClimateDevice): diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py new file mode 100644 index 00000000000..cd2d528044a --- /dev/null +++ b/homeassistant/components/homematic/const.py @@ -0,0 +1,212 @@ +"""Constants for the homematic component.""" + +DOMAIN = "homematic" + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_COVER = "homematic.cover" +DISCOVER_CLIMATE = "homematic.climate" +DISCOVER_LOCKS = "homematic.locks" +DISCOVER_BATTERY = "homematic.battery" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_ADDRESS = "address" +ATTR_VALUE = "value" +ATTR_VALUE_TYPE = "value_type" +ATTR_INTERFACE = "interface" +ATTR_ERRORCODE = "error" +ATTR_MESSAGE = "message" +ATTR_TIME = "time" +ATTR_UNIQUE_ID = "unique_id" +ATTR_PARAMSET_KEY = "paramset_key" +ATTR_PARAMSET = "paramset" +ATTR_DISCOVERY_TYPE = "discovery_type" +ATTR_LOW_BAT = "LOW_BAT" +ATTR_LOWBAT = "LOWBAT" + +EVENT_KEYPRESS = "homematic.keypress" +EVENT_IMPULSE = "homematic.impulse" +EVENT_ERROR = "homematic.error" + +SERVICE_VIRTUALKEY = "virtualkey" +SERVICE_RECONNECT = "reconnect" +SERVICE_SET_VARIABLE_VALUE = "set_variable_value" +SERVICE_SET_DEVICE_VALUE = "set_device_value" +SERVICE_SET_INSTALL_MODE = "set_install_mode" +SERVICE_PUT_PARAMSET = "put_paramset" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: [ + "Switch", + "SwitchPowermeter", + "IOSwitch", + "IPSwitch", + "RFSiren", + "IPSwitchPowermeter", + "HMWIOSwitch", + "Rain", + "EcoLogic", + "IPKeySwitchPowermeter", + "IPGarage", + "IPKeySwitch", + "IPKeySwitchLevel", + "IPMultiIO", + ], + DISCOVER_LIGHTS: [ + "Dimmer", + "KeyDimmer", + "IPKeyDimmer", + "IPDimmer", + "ColorEffectLight", + "IPKeySwitchLevel", + ], + DISCOVER_SENSORS: [ + "SwitchPowermeter", + "Motion", + "MotionV2", + "RemoteMotion", + "MotionIP", + "ThermostatWall", + "AreaThermostat", + "RotaryHandleSensor", + "WaterSensor", + "PowermeterGas", + "LuxSensor", + "WeatherSensor", + "WeatherStation", + "ThermostatWall2", + "TemperatureDiffSensor", + "TemperatureSensor", + "CO2Sensor", + "IPSwitchPowermeter", + "HMWIOSwitch", + "FillingLevel", + "ValveDrive", + "EcoLogic", + "IPThermostatWall", + "IPSmoke", + "RFSiren", + "PresenceIP", + "IPAreaThermostat", + "IPWeatherSensor", + "RotaryHandleSensorIP", + "IPPassageSensor", + "IPKeySwitchPowermeter", + "IPThermostatWall230V", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPBrightnessSensor", + "IPGarage", + "UniversalSensor", + "MotionIPV2", + "IPMultiIO", + "IPThermostatWall2", + ], + DISCOVER_CLIMATE: [ + "Thermostat", + "ThermostatWall", + "MAXThermostat", + "ThermostatWall2", + "MAXWallThermostat", + "IPThermostat", + "IPThermostatWall", + "ThermostatGroup", + "IPThermostatWall230V", + "IPThermostatWall2", + ], + DISCOVER_BINARY_SENSORS: [ + "ShutterContact", + "Smoke", + "SmokeV2", + "Motion", + "MotionV2", + "MotionIP", + "RemoteMotion", + "WeatherSensor", + "TiltSensor", + "IPShutterContact", + "HMWIOSwitch", + "MaxShutterContact", + "Rain", + "WiredSensor", + "PresenceIP", + "IPWeatherSensor", + "IPPassageSensor", + "SmartwareMotion", + "IPWeatherSensorPlus", + "MotionIPV2", + "WaterIP", + "IPMultiIO", + "TiltIP", + "IPShutterContactSabotage", + "IPContact", + ], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_LOCKS: ["KeyMatic"], +} + +HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] + +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + "ACTUAL_TEMPERATURE": [ + "IPAreaThermostat", + "IPWeatherSensor", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPThermostatWall", + "IPThermostatWall2", + ] +} + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["battery", {0: "High", 1: "Low"}], + "LOW_BAT": ["battery", {0: "High", 1: "Low"}], + "ERROR": ["error", {0: "No"}], + "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "RSSI_PEER": ["rssi_peer", {}], + "RSSI_DEVICE": ["rssi_device", {}], + "VALVE_STATE": ["valve", {}], + "LEVEL": ["level", {}], + "BATTERY_STATE": ["battery", {}], + "CONTROL_MODE": [ + "mode", + {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, + ], + "POWER": ["power", {}], + "CURRENT": ["current", {}], + "VOLTAGE": ["voltage", {}], + "OPERATING_VOLTAGE": ["voltage", {}], + "WORKING": ["working", {0: "No", 1: "Yes"}], + "STATE_UNCERTAIN": ["state_uncertain", {}], +} + +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE", + "PRESS", +] + +HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] + +CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] + +DATA_HOMEMATIC = "homematic" +DATA_STORE = "homematic_store" +DATA_CONF = "homematic_conf" + +CONF_INTERFACES = "interfaces" +CONF_LOCAL_IP = "local_ip" +CONF_LOCAL_PORT = "local_port" +CONF_PORT = "port" +CONF_PATH = "path" +CONF_CALLBACK_IP = "callback_ip" +CONF_CALLBACK_PORT = "callback_port" +CONF_RESOLVENAMES = "resolvenames" +CONF_JSONPORT = "jsonport" diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 893b3ce8921..0dea1181d73 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -6,9 +6,9 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDevice, ) -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMCover(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMCover(HMDevice, CoverDevice): @@ -68,9 +68,9 @@ class HMCover(HMDevice, CoverDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) if "LEVEL_2" in self._hmdevice.WRITENODE: - self._data.update({"LEVEL_2": STATE_UNKNOWN}) + self._data.update({"LEVEL_2": None}) @property def current_cover_tilt_position(self): diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py new file mode 100644 index 00000000000..4ed893bbf14 --- /dev/null +++ b/homeassistant/components/homematic/entity.py @@ -0,0 +1,297 @@ +"""Homematic base entity.""" +from abc import abstractmethod +from datetime import timedelta +import logging + +from homeassistant.const import ATTR_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_INTERFACE, + ATTR_PARAM, + ATTR_UNIQUE_ID, + DATA_HOMEMATIC, + DOMAIN, + HM_ATTRIBUTE_SUPPORT, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL_HUB = timedelta(seconds=300) +SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) + + +class HMDevice(Entity): + """The HomeMatic device base object.""" + + def __init__(self, config): + """Initialize a generic HomeMatic device.""" + self._name = config.get(ATTR_NAME) + self._address = config.get(ATTR_ADDRESS) + self._interface = config.get(ATTR_INTERFACE) + self._channel = config.get(ATTR_CHANNEL) + self._state = config.get(ATTR_PARAM) + self._unique_id = config.get(ATTR_UNIQUE_ID) + self._data = {} + self._homematic = None + self._hmdevice = None + self._connected = False + self._available = False + + # Set parameter to uppercase + if self._state: + self._state = self._state.upper() + + async def async_added_to_hass(self): + """Load data init callbacks.""" + await self.hass.async_add_job(self._subscribe_homematic_events) + + @property + def unique_id(self): + """Return unique ID. HomeMatic entity IDs are unique by default.""" + return self._unique_id.replace(" ", "_") + + @property + def should_poll(self): + """Return false. HomeMatic states are pushed by the XML-RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate a dictionary with attributes + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attribute and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + # Static attributes + attr["id"] = self._hmdevice.ADDRESS + attr["interface"] = self._interface + + return attr + + def update(self): + """Connect to HomeMatic init values.""" + if self._connected: + return True + + # Initialize + self._homematic = self.hass.data[DATA_HOMEMATIC] + self._hmdevice = self._homematic.devices[self._interface][self._address] + self._connected = True + + try: + # Initialize datapoints of this object + self._init_data() + self._load_data_from_hm() + + # Link events from pyhomematic + self._available = not self._hmdevice.UNREACH + except Exception as err: # pylint: disable=broad-except + self._connected = False + _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) + + 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 + + # Availability has changed + if self.available != (not self._hmdevice.UNREACH): + self._available = not self._hmdevice.UNREACH + has_changed = True + + # If it has changed data point, update Home Assistant + if has_changed: + self.schedule_update_ha_state() + + 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, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, + self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE, + ): + for node, channels in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if len(channels) == 1: + 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) + + # 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 + ) + + def _load_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), + ): + for node in metadata: + if metadata[node] and node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + return True + + def _hm_set_state(self, value): + """Set data to main datapoint.""" + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + """Get data from main datapoint.""" + if self._state in self._data: + return self._data[self._state] + return None + + def _init_data(self): + """Generate a data dict (self._data) from the HomeMatic metadata.""" + # Add all attributes to data dictionary + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: None}) + + # Initialize device specific data + self._init_data_struct() + + @abstractmethod + def _init_data_struct(self): + """Generate a data dictionary from the HomeMatic device metadata.""" + + +class HMHub(Entity): + """The HomeMatic hub. (CCU2/HomeGear).""" + + def __init__(self, hass, homematic, name): + """Initialize HomeMatic hub.""" + self.hass = hass + self.entity_id = "{}.{}".format(DOMAIN, name.lower()) + self._homematic = homematic + self._variables = {} + self._name = name + self._state = None + + # Load data + self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) + self.hass.add_job(self._update_hub, None) + + self.hass.helpers.event.track_time_interval( + self._update_variables, SCAN_INTERVAL_VARIABLES + ) + self.hass.add_job(self._update_variables, None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return false. HomeMatic Hub object updates variables.""" + return False + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes.""" + attr = self._variables.copy() + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:gradient" + + def _update_hub(self, now): + """Retrieve latest state.""" + service_message = self._homematic.getServiceMessages(self._name) + state = None if service_message is None else len(service_message) + + # state have change? + if self._state != state: + self._state = state + self.schedule_update_ha_state() + + def _update_variables(self, now): + """Retrieve all variable data and update hmvariable states.""" + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + state_change = False + for key, value in variables.items(): + if key in self._variables and value == self._variables[key]: + continue + + state_change = True + self._variables.update({key: value}) + + if state_change: + self.schedule_update_ha_state() + + def hm_set_variable(self, name, value): + """Set variable value on CCU/Homegear.""" + if name not in self._variables: + _LOGGER.error("Variable %s not found on %s", name, self.name) + return + old_value = self._variables.get(name) + if isinstance(old_value, bool): + value = cv.boolean(value) + else: + value = float(value) + self._homematic.setSystemVariable(self.name, name, value) + + self._variables.update({name: value}) + self.schedule_update_ha_state() diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 29992bccef3..52b2f9a7996 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -12,7 +12,8 @@ from homeassistant.components.light import ( Light, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMLight(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMLight(HMDevice, Light): diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 7f796b32885..0094ecd2e81 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for conf in discovery_info[ATTR_DISCOVER_DEVICES]: devices.append(HMLock(conf)) - add_entities(devices) + add_entities(devices, True) class HMLock(HMDevice, LockDevice): @@ -44,7 +44,7 @@ class HMLock(HMDevice, LockDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) @property def supported_features(self): diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 9fd94b9832c..3d48adc6df2 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from . import ( +from .const import ( ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, @@ -22,6 +22,7 @@ from . import ( ) _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 10c402a0dd4..bba8325650d 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,10 +8,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, POWER_WATT, - STATE_UNKNOWN, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSensor(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSensor(HMDevice): @@ -117,6 +117,6 @@ class HMSensor(HMDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) else: _LOGGER.critical("Unable to initialize sensor: %s", self._name) diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b77b3a1f700..53679818083 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSwitch(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSwitch(HMDevice, SwitchDevice): @@ -55,8 +55,8 @@ class HMSwitch(HMDevice, SwitchDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) # Need sensor values for SwitchPowermeter for node in self._hmdevice.SENSORNODE: - self._data.update({node: STATE_UNKNOWN}) + self._data.update({node: None}) From aeb789ddcc237515989b7fc27cf6feebed56510c Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 14 Jan 2020 02:27:49 -0800 Subject: [PATCH 093/393] Bump teslajsonpy to 0.2.3 (#30750) --- homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 81a6b8ea0db..e3392074679 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.2"], + "requirements": ["teslajsonpy==0.2.3"], "dependencies": [], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4b98d7a460f..88769dd8647 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1954,7 +1954,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.2.2 +teslajsonpy==0.2.3 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6352040949..6adde9f718b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -619,7 +619,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.tesla -teslajsonpy==0.2.2 +teslajsonpy==0.2.3 # homeassistant.components.toon toonapilib==3.2.4 From 6b49bea6c45c4d8a0d7c47794936f7f2f853e879 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Jan 2020 16:09:35 +0100 Subject: [PATCH 094/393] Fix HomeKit behavior with lights supporting color and temperature (#30756) --- .../components/homekit/type_lights.py | 12 ++++++++++-- tests/components/homekit/test_type_lights.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 7f195b276d6..3fc6a0628ff 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -66,15 +66,20 @@ class Light(HomeAccessory): self._features = self.hass.states.get(self.entity_id).attributes.get( ATTR_SUPPORTED_FEATURES ) + if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_COLOR_TEMP: - self.chars.append(CHAR_COLOR_TEMPERATURE) + if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) self._hue = None self._saturation = None + elif self._features & SUPPORT_COLOR_TEMP: + # ColorTemperature and Hue characteristic should not be + # exposed both. Both states are tracked separately in HomeKit, + # causing "source of truth" problems. + self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char( @@ -88,6 +93,7 @@ class Light(HomeAccessory): self.char_brightness = serv_light.configure_char( CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) + if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( ATTR_MIN_MIREDS, 153 @@ -101,10 +107,12 @@ class Light(HomeAccessory): properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature, ) + if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( CHAR_HUE, value=0, setter_callback=self.set_hue ) + if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char( CHAR_SATURATION, value=75, setter_callback=self.set_saturation diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index c1811a2e2fc..f357702040b 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -177,6 +177,25 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "color temperature at 250" +async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, events): + """Test light with color temperature and rgb color not exposing temperature.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP | SUPPORT_COLOR, + ATTR_COLOR_TEMP: 190, + ATTR_HS_COLOR: (260, 90), + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + + assert not hasattr(acc, "char_color_temperature") + + async def test_light_rgb_color(hass, hk_driver, cls, events): """Test light with rgb_color.""" entity_id = "light.demo" From a26fef38a201d2b195656ef4c98c7cb601996772 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 14 Jan 2020 17:41:52 +0100 Subject: [PATCH 095/393] bump aiokef to 0.2.5 which uses locks (#30753) --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 30335c409ee..b950b144cf9 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.2", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.5", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index f0c2de2a86a..177b2fccd13 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -40,7 +40,6 @@ DEFAULT_INVERSE_SPEAKER_MODE = False DOMAIN = "kef" SCAN_INTERVAL = timedelta(seconds=30) -PARALLEL_UPDATES = 0 SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] diff --git a/requirements_all.txt b/requirements_all.txt index 88769dd8647..793b9c81aea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,7 +172,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.2 +aiokef==0.2.5 # homeassistant.components.lifx aiolifx==0.6.7 From 0b8a269b238fae877bbbd9a530bf0991c49820fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ak=C4=B1n=20=C3=96mero=C4=9Flu?= Date: Tue, 14 Jan 2020 19:47:59 +0300 Subject: [PATCH 096/393] Fix small typo in alarmdotcom component (#30758) --- homeassistant/components/alarmdotcom/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 297c360d04245515103716ea71d866f8bca298ca Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 14 Jan 2020 18:17:44 +0100 Subject: [PATCH 097/393] Fix supported_features in MQTT fan (#28680) * Added custom validator function for speed list * Replace CONF_SPEED_STATE_TOPIC with CONF_SPEED_COMMAND_TOPIC to determine SUPPORT_SET_SPEED * Revert "Added custom validator function for speed list" This reverts commit f000396fa642c64bde40513ea70d9915dbd71ead. * Replace CONF_OSCILLATION_STATE_TOPIC with CONF_OSCILLATION_COMMAND_TOPIC to determine SUPPORT_OSCILLATE --- homeassistant/components/mqtt/fan.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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): From ba3c3057da87f6e34e6a4cb142e04ca302b8c4dd Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Tue, 14 Jan 2020 12:46:04 -0700 Subject: [PATCH 098/393] Add support for the voltage sensor on the greeneye GEM (#30484) * Add support for the voltage sensor on the greeneye GEM * Changed per suggestion @springstan --- .../components/greeneye_monitor/__init__.py | 21 +++++++++- .../components/greeneye_monitor/sensor.py | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) 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/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" From c4673ddee1853be155ba36308c7cd7d7f88242c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 12:54:45 -0800 Subject: [PATCH 099/393] Update Ring to 0.6.0 (#30748) * Update Ring to 0.6.0 * Update sensor tests * update -> async_update * Delete temp files * Address comments * Final tweaks * Remove stale print --- CODEOWNERS | 1 + homeassistant/components/ring/__init__.py | 169 +++++++++++++----- .../components/ring/binary_sensor.py | 90 ++++++---- homeassistant/components/ring/camera.py | 70 ++++---- homeassistant/components/ring/config_flow.py | 4 +- homeassistant/components/ring/light.py | 25 +-- homeassistant/components/ring/manifest.json | 4 +- homeassistant/components/ring/sensor.py | 163 ++++++++++------- homeassistant/components/ring/switch.py | 27 +-- homeassistant/helpers/entity_platform.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/conftest.py | 10 +- tests/components/ring/test_binary_sensor.py | 95 ++-------- tests/components/ring/test_light.py | 6 +- tests/components/ring/test_sensor.py | 142 ++++----------- tests/components/ring/test_switch.py | 6 +- tests/fixtures/ring_devices.json | 8 +- tests/fixtures/ring_devices_updated.json | 8 +- 19 files changed, 417 insertions(+), 418 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c06f9c07d76..7ef5986a4da 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -275,6 +275,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen +homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 7addc116b06..b35ff630310 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -4,15 +4,17 @@ from datetime import timedelta from functools import partial import logging from pathlib import Path +from time import time from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -22,14 +24,14 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_RING_DOORBELLS = "ring_doorbells" -DATA_RING_STICKUP_CAMS = "ring_stickup_cams" -DATA_RING_CHIMES = "ring_chimes" +DATA_HISTORY = "ring_history" +DATA_HEALTH_DATA_TRACKER = "ring_health_data" DATA_TRACK_INTERVAL = "ring_track_interval" DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" SIGNAL_UPDATE_RING = "ring_update" +SIGNAL_UPDATE_HEALTH_RING = "ring_health_update" SCAN_INTERVAL = timedelta(seconds=10) @@ -88,51 +90,42 @@ async def async_setup_entry(hass, entry): ), ).result() - auth = Auth(entry.data["token"], token_updater) + auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) ring = Ring(auth) - await hass.async_add_executor_job(finish_setup_entry, hass, ring) + await hass.async_add_executor_job(ring.update_data) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - return True + if hass.services.has_service(DOMAIN, "update"): + return True - -def finish_setup_entry(hass, ring): - """Finish setting up entry.""" - devices = ring.devices - hass.data[DATA_RING_CHIMES] = chimes = devices["chimes"] - hass.data[DATA_RING_DOORBELLS] = doorbells = devices["doorbells"] - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = devices["stickup_cams"] - - ring_devices = chimes + doorbells + stickup_cams - - def service_hub_refresh(service): - hub_refresh() - - def timer_hub_refresh(event_time): - hub_refresh() - - def hub_refresh(): - """Call ring to refresh information.""" - _LOGGER.debug("Updating Ring Hub component") - - for camera in ring_devices: - _LOGGER.debug("Updating camera %s", camera.name) - camera.update() - - dispatcher_send(hass, SIGNAL_UPDATE_RING) + async def refresh_all(_): + """Refresh all ring accounts.""" + await asyncio.gather( + *[ + hass.async_add_executor_job(api.update_data) + for api in hass.data[DOMAIN].values() + ] + ) + async_dispatcher_send(hass, SIGNAL_UPDATE_RING) # register service - hass.services.register(DOMAIN, "update", service_hub_refresh) + hass.services.async_register(DOMAIN, "update", refresh_all) # register scan interval for ring - hass.data[DATA_TRACK_INTERVAL] = track_time_interval( - hass, timer_hub_refresh, SCAN_INTERVAL + hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval( + hass, refresh_all, SCAN_INTERVAL ) + hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass) + hass.data[DATA_HISTORY] = HistoryCache(hass) + + return True async def async_unload_entry(hass, entry): @@ -148,13 +141,103 @@ async def async_unload_entry(hass, entry): if not unload_ok: return False - await hass.async_add_executor_job(hass.data[DATA_TRACK_INTERVAL]) + hass.data[DOMAIN].pop(entry.entry_id) + if len(hass.data[DOMAIN]) != 0: + return True + + # Last entry unloaded, clean up + hass.data.pop(DATA_TRACK_INTERVAL)() + hass.data.pop(DATA_HEALTH_DATA_TRACKER) + hass.data.pop(DATA_HISTORY) hass.services.async_remove(DOMAIN, "update") - hass.data.pop(DATA_RING_DOORBELLS) - hass.data.pop(DATA_RING_STICKUP_CAMS) - hass.data.pop(DATA_RING_CHIMES) - hass.data.pop(DATA_TRACK_INTERVAL) + return True - return unload_ok + +class HealthDataUpdater: + """Data storage for health data.""" + + def __init__(self, hass): + """Track devices that need health data updated.""" + self.hass = hass + self.devices = {} + self._unsub_interval = None + + async def track_device(self, config_entry_id, device): + """Track a device.""" + if not self.devices: + self._unsub_interval = async_track_time_interval( + self.hass, self.refresh_all, SCAN_INTERVAL + ) + + key = (config_entry_id, device.device_id) + + if key not in self.devices: + self.devices[key] = { + "device": device, + "count": 1, + } + else: + self.devices[key]["count"] += 1 + + await self.hass.async_add_executor_job(device.update_health_data) + + @callback + def untrack_device(self, config_entry_id, device): + """Untrack a device.""" + key = (config_entry_id, device.device_id) + self.devices[key]["count"] -= 1 + + if self.devices[key]["count"] == 0: + self.devices.pop(key) + + if not self.devices: + self._unsub_interval() + self._unsub_interval = None + + def refresh_all(self, _): + """Refresh all registered devices.""" + for info in self.devices.values(): + info["device"].update_health_data() + + dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING) + + +class HistoryCache: + """Helper to fetch history.""" + + STALE_AFTER = 10 # seconds + + def __init__(self, hass): + """Initialize history cache.""" + self.hass = hass + self.cache = {} + + async def async_get_history(self, config_entry_id, device): + """Get history of a device.""" + key = (config_entry_id, device.device_id) + + if key in self.cache: + info = self.cache[key] + + # We're already fetching data, join that task + if "task" in info: + return await info["task"] + + # We have valid cache info, return that + if time() - info["created_at"] < self.STALE_AFTER: + return info["data"] + + self.cache.pop(key) + + # Fetch data + task = self.hass.async_add_executor_job(partial(device.history, limit=10)) + + self.cache[key] = {"task": task} + + data = await task + + self.cache[key] = {"created_at": time(), "data": data} + + return data diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 29337f29689..2dd3682951f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -4,8 +4,10 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTRIBUTION, DATA_RING_DOORBELLS, DATA_RING_STICKUP_CAMS, DOMAIN +from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -13,26 +15,25 @@ SCAN_INTERVAL = timedelta(seconds=10) # Sensor types: Name, category, device_class SENSOR_TYPES = { - "ding": ["Ding", ["doorbell"], "occupancy"], - "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], + "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], + "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Ring binary sensors from a config entry.""" - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() sensors = [] - for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in SENSOR_TYPES: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) - for device in ring_stickup_cams: # ring.stickup_cams is doing I/O + for device_type in ("doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) + if device_type not in SENSOR_TYPES[sensor_type][1]: + continue + + for device in devices[device_type]: + sensors.append(RingBinarySensor(ring, device, sensor_type)) async_add_entities(sensors, True) @@ -40,17 +41,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingBinarySensor(BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() self._sensor_type = sensor_type - self._data = data + self._ring = ring + self._device = device self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" + self._disp_disconnect = None + + async def async_added_to_hass(self): + """Register callbacks.""" + self._disp_disconnect = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RING, self._update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + if self._disp_disconnect: + self._disp_disconnect() + self._disp_disconnect = None + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + _LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name) + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False @property def name(self): @@ -76,10 +101,9 @@ class RingBinarySensor(BinarySensorDevice): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -89,22 +113,16 @@ class RingBinarySensor(BinarySensorDevice): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["timezone"] = self._data.timezone - - if self._data.alert and self._data.alert_expires_at: - attrs["expires_at"] = self._data.alert_expires_at - attrs["state"] = self._data.alert.get("state") + if self._device.alert and self._device.alert_expires_at: + attrs["expires_at"] = self._device.alert_expires_at + attrs["state"] = self._device.alert.get("state") return attrs - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" - self._data.check_alerts() - - if self._data.alert: - if self._sensor_type == self._data.alert.get( - "kind" - ) and self._data.account_id == self._data.alert.get("doorbot_id"): - self._state = True - else: - self._state = False + self._state = any( + alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + for alert in self._ring.active_alerts() + ) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 2b0fe14a1d4..8ef876e4a00 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,6 +1,7 @@ """This component provides support to the Ring Door Bell camera.""" import asyncio from datetime import timedelta +from itertools import chain import logging from haffmpeg.camera import CameraMjpeg @@ -14,13 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DOMAIN, - SIGNAL_UPDATE_RING, -) +from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING FORCE_REFRESH_INTERVAL = timedelta(minutes=45) @@ -29,16 +24,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring_doorbell = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() cams = [] - for camera in ring_doorbell + ring_stickup_cams: + for camera in chain( + devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] + ): if not camera.has_subscription: continue - camera = await hass.async_add_executor_job(RingCam, hass, camera) - cams.append(camera) + cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) async_add_entities(cams, True) @@ -46,17 +42,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingCam(Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera): + def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" super().__init__() - self._camera = camera - self._hass = hass - self._name = self._camera.name - self._ffmpeg = hass.data[DATA_FFMPEG] - self._last_video_id = self._camera.last_recording_id - self._video_url = self._camera.recording_url(self._last_video_id) + self._config_entry_id = config_entry_id + self._device = device + self._name = self._device.name + self._ffmpeg = ffmpeg + self._last_video_id = None + self._video_url = None self._utcnow = dt_util.utcnow() - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL self._disp_disconnect = None async def async_added_to_hass(self): @@ -85,16 +81,15 @@ class RingCam(Camera): @property def unique_id(self): """Return a unique ID.""" - return self._camera.id + return self._device.id @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._camera.id)}, - "sw_version": self._camera.firmware, - "name": self._camera.name, - "model": self._camera.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -103,7 +98,6 @@ class RingCam(Camera): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "timezone": self._camera.timezone, "video_url": self._video_url, "last_video_id": self._last_video_id, } @@ -123,7 +117,6 @@ class RingCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - if self._video_url is None: return @@ -141,22 +134,20 @@ class RingCam(Camera): finally: await stream.close() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False - - def update(self): + async def async_update(self): """Update camera entity and refresh attributes.""" _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") self._utcnow = dt_util.utcnow() - try: - last_event = self._camera.history(limit=1)[0] - except (IndexError, TypeError): + data = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device + ) + + if not data: return + last_event = data[0] last_recording_id = last_event["id"] video_status = last_event["recording"]["status"] @@ -164,9 +155,12 @@ class RingCam(Camera): self._last_video_id != last_recording_id or self._utcnow >= self._expires_at ): - video_url = self._camera.recording_url(last_recording_id) + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, last_recording_id + ) + if video_url: - _LOGGER.info("Ring DoorBell properties refreshed") + _LOGGER.debug("Ring DoorBell properties refreshed") # update attributes if new video or if URL has expired self._last_video_id = last_recording_id diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 6d177a4db49..57f873bd1a6 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -5,7 +5,7 @@ from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, const, core, exceptions from . import DOMAIN # pylint: disable=unused-import @@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = Auth() + auth = Auth(f"HomeAssistant/{const.__version__}") try: token = await hass.async_add_executor_job( diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index b7fa67a391f..10572e2e0ae 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -25,10 +25,12 @@ OFF_STATE = "off" async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + + devices = ring.devices() lights = [] - for device in cameras: + for device in devices["stickup_cams"]: if device.has_capability("light"): lights.append(RingLight(device)) @@ -64,6 +66,11 @@ class RingLight(Light): _LOGGER.debug("Updating Ring light %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the light.""" @@ -74,11 +81,6 @@ class RingLight(Light): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def is_on(self): """If the switch is currently on or off.""" @@ -88,10 +90,9 @@ class RingLight(Light): def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -110,7 +111,7 @@ class RingLight(Light): """Turn the light off.""" self._set_light(OFF_STATE) - def update(self): + async def async_update(self): """Update current state of the light.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index fccbf9a5319..d46f12af511 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,8 +2,8 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.5.0"], + "requirements": ["ring_doorbell==0.6.0"], "dependencies": ["ffmpeg"], - "codeowners": [], + "codeowners": ["@balloob"], "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 874c056ec7d..fe909636e83 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,93 +9,98 @@ from homeassistant.helpers.icon import icon_for_battery_level from . import ( ATTRIBUTION, - DATA_RING_CHIMES, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, + DATA_HEALTH_DATA_TRACKER, + DATA_HISTORY, DOMAIN, + SIGNAL_UPDATE_HEALTH_RING, SIGNAL_UPDATE_RING, ) _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind +# Sensor types: Name, category, units, icon, kind, device_class SENSOR_TYPES = { - "battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None], + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + ], "last_activity": [ "Last Activity", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", None, + "timestamp", + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", ], - "last_ding": ["Last Ding", ["doorbell"], None, "history", "ding"], "last_motion": [ "Last Motion", - ["doorbell", "stickup_cams"], + ["doorbots", "authorized_doorbots", "stickup_cams"], None, "history", "motion", + "timestamp", ], "volume": [ "Volume", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "bell-ring", None, + None, ], "wifi_signal_category": [ "WiFi Signal Category", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], None, "wifi", None, + None, ], "wifi_signal_strength": [ "WiFi Signal Strength", - ["chime", "doorbell", "stickup_cams"], + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], "dBm", "wifi", None, + "signal_strength", ], } async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring_chimes = hass.data[DATA_RING_CHIMES] - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() + # Makes a ton of requests. We will make this a config entry option in the future + wifi_enabled = False sensors = [] - for device in ring_chimes: + + for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): for sensor_type in SENSOR_TYPES: - if "chime" not in SENSOR_TYPES[sensor_type][1]: + if device_type not in SENSOR_TYPES[sensor_type][1]: continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_doorbells: - for sensor_type in SENSOR_TYPES: - if "doorbell" not in SENSOR_TYPES[sensor_type][1]: + if not wifi_enabled and sensor_type.startswith("wifi_"): continue - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) + for device in devices[device_type]: + if device_type == "battery" and device.battery_life is None: + continue - sensors.append(RingSensor(hass, device, sensor_type)) - - for device in ring_stickup_cams: - for sensor_type in SENSOR_TYPES: - if "stickup_cams" not in SENSOR_TYPES[sensor_type][1]: - continue - - if sensor_type in ("wifi_signal_category", "wifi_signal_strength"): - await hass.async_add_executor_job(device.update) - - sensors.append(RingSensor(hass, device, sensor_type)) + sensors.append(RingSensor(config_entry.entry_id, device, sensor_type)) async_add_entities(sensors, True) @@ -103,28 +108,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class RingSensor(Entity): """A sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() + self._config_entry_id = config_entry_id self._sensor_type = sensor_type - self._data = data + self._device = device self._extra = None self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) self._kind = SENSOR_TYPES.get(self._sensor_type)[4] self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] ) self._state = None - self._tz = str(hass.config.time_zone) - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{self._device.id}-{self._sensor_type}" self._disp_disconnect = None + self._disp_disconnect_health = None async def async_added_to_hass(self): """Register callbacks.""" self._disp_disconnect = async_dispatcher_connect( self.hass, SIGNAL_UPDATE_RING, self._update_callback ) - await self.hass.async_add_executor_job(self._data.update) + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self._disp_disconnect_health = async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HEALTH_RING, self._update_callback + ) + await self.hass.data[DATA_HEALTH_DATA_TRACKER].track_device( + self._config_entry_id, self._device + ) + # Write the state, it was not available when doing initial update. + if self._sensor_type == "wifi_signal_category": + self._state = self._device.wifi_signal_category + + if self._sensor_type == "wifi_signal_strength": + self._state = self._device.wifi_signal_strength async def async_will_remove_from_hass(self): """Disconnect callbacks.""" @@ -132,6 +151,17 @@ class RingSensor(Entity): self._disp_disconnect() self._disp_disconnect = None + if self._disp_disconnect_health: + self._disp_disconnect_health() + self._disp_disconnect_health = None + + if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): + return + + self.hass.data[DATA_HEALTH_DATA_TRACKER].untrack_device( + self._config_entry_id, self._device + ) + @callback def _update_callback(self): """Call update method.""" @@ -157,14 +187,18 @@ class RingSensor(Entity): """Return a unique ID.""" return self._unique_id + @property + def device_class(self): + """Return sensor device class.""" + return SENSOR_TYPES[self._sensor_type][5] + @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._data.id)}, - "sw_version": self._data.firmware, - "name": self._data.name, - "model": self._data.kind, + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, "manufacturer": "Ring", } @@ -174,8 +208,6 @@ class RingSensor(Entity): attrs = {} attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["timezone"] = self._data.timezone - attrs["wifi_name"] = self._data.wifi_name if self._extra and self._sensor_type.startswith("last_"): attrs["created_at"] = self._extra["created_at"] @@ -199,29 +231,34 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - def update(self): + async def async_update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Updating data from %s sensor", self._name) if self._sensor_type == "volume": - self._state = self._data.volume + self._state = self._device.volume if self._sensor_type == "battery": - self._state = self._data.battery_life + self._state = self._device.battery_life if self._sensor_type.startswith("last_"): - history = self._data.history( - limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True + history = await self.hass.data[DATA_HISTORY].async_get_history( + self._config_entry_id, self._device ) - if history: - self._extra = history[0] - created_at = self._extra["created_at"] - self._state = "{0:0>2}:{1:0>2}".format( - created_at.hour, created_at.minute - ) + + found = None + for entry in history: + if entry["kind"] == self._kind: + found = entry + break + + if found: + self._extra = found + created_at = found["created_at"] + self._state = created_at.isoformat() if self._sensor_type == "wifi_signal_category": - self._state = self._data.wifi_signal_category + self._state = self._device.wifi_signal_category if self._sensor_type == "wifi_signal_strength": - self._state = self._data.wifi_signal_strength + self._state = self._device.wifi_signal_strength diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index e23e757d825..06f81732784 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -7,7 +7,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN, SIGNAL_UPDATE_RING _LOGGER = logging.getLogger(__name__) @@ -24,9 +24,11 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + ring = hass.data[DOMAIN][config_entry.entry_id] + devices = ring.devices() switches = [] - for device in cameras: + + for device in devices["stickup_cams"]: if device.has_capability("siren"): switches.append(SirenSwitch(device)) @@ -58,9 +60,14 @@ class BaseRingSwitch(SwitchDevice): @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring sensor %s (callback)", self.name) + _LOGGER.debug("Updating Ring switch %s (callback)", self.name) self.async_schedule_update_ha_state(True) + @property + def should_poll(self): + """Update controlled via the hub.""" + return False + @property def name(self): """Name of the device.""" @@ -71,19 +78,13 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def device_info(self): """Return device info.""" return { - "identifiers": {(DOMAIN, self._device.id)}, - "sw_version": self._device.firmware, + "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "model": self._device.kind, + "model": self._device.model, "manufacturer": "Ring", } @@ -122,7 +123,7 @@ class SirenSwitch(BaseRingSwitch): """Return the icon.""" return SIREN_ICON - def update(self): + async def async_update(self): """Update state of the siren.""" if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0560cf84fb3..53ad54c5ed1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -146,7 +146,8 @@ class EntityPlatform: warn_task = hass.loop.call_later( SLOW_SETUP_WARNING, logger.warning, - "Setup of platform %s is taking over %s seconds.", + "Setup of %s platform %s is taking over %s seconds.", + self.domain, self.platform_name, SLOW_SETUP_WARNING, ) diff --git a/requirements_all.txt b/requirements_all.txt index 793b9c81aea..fa90fff639a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1750,7 +1750,7 @@ rfk101py==0.0.1 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6adde9f718b..dc3b1d18199 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -570,7 +570,7 @@ restrictedpython==5.0 rflink==0.0.50 # homeassistant.components.ring -ring_doorbell==0.5.0 +ring_doorbell==0.6.0 # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index a4cfaf0065d..5df85662ac8 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,4 +1,6 @@ """Configuration for Ring tests.""" +import re + import pytest import requests_mock @@ -33,17 +35,19 @@ def requests_mock_fixture(): ) # Mocks the response for getting the history of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/history", + re.compile( + r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/history" + ), text=load_fixture("ring_doorbots.json"), ) # Mocks the response for getting the health of a device mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/doorbots\/\d+\/health"), text=load_fixture("ring_doorboot_health_attrs.json"), ) # Mocks the response for getting a chimes health mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", + re.compile(r"https:\/\/api\.ring\.com\/clients_api\/chimes\/\d+\/health"), text=load_fixture("ring_chime_health_attrs.json"), ) diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 4ca83b2451b..8615138d56e 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,87 +1,22 @@ """The tests for the Ring binary sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest from unittest.mock import patch -import requests_mock - -from homeassistant.components import ring as base_ring -from homeassistant.components.ring import binary_sensor as ring - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +from .common import setup_platform -class TestRingBinarySensorSetup(unittest.TestCase): - """Test the Ring Binary Sensor platform.""" +async def test_binary_sensor(hass, requests_mock): + """Test the Ring binary sensors.""" + with patch( + "ring_doorbell.Ring.active_alerts", + return_value=[{"kind": "motion", "doorbot_id": 987654}], + ): + await setup_platform(hass, "binary_sensor") - DEVICES = [] + motion_state = hass.states.get("binary_sensor.front_door_motion") + assert motion_state is not None + assert motion_state.state == "on" + assert motion_state.attributes["device_class"] == "motion" - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) - - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = { - "username": "foo", - "password": "bar", - "monitored_conditions": ["ding", "motion"], - } - - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - @requests_mock.Mocker() - def test_binary_sensor(self, mock): - """Test the Ring sensor class and methods.""" - mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") - ) - mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), - ) - mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), - ) - mock.get( - "https://api.ring.com/clients_api/dings/active", - text=load_fixture("ring_ding_active.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("ring_doorboot_health_attrs.json"), - ) - mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("ring_chime_health_attrs.json"), - ) - - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() - - for device in self.DEVICES: - device.update() - if device.name == "Front Door Ding": - assert "on" == device.state - assert "America/New_York" == device.device_state_attributes["timezone"] - elif device.name == "Front Door Motion": - assert "off" == device.state - assert "motion" == device.device_class - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] + ding_state = hass.states.get("binary_sensor.front_door_ding") + assert ding_state is not None + assert ding_state.state == "off" diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index 56d39173d63..6cc727b1a1c 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("light.front_light") - assert entry.unique_id == "aacdef123" + assert entry.unique_id == 765432 entry = entity_registry.async_get("light.internal_light") - assert entry.unique_id == "aacdef124" + assert entry.unique_id == 345678 async def test_light_off_reports_correctly(hass, requests_mock): @@ -42,7 +42,7 @@ async def test_light_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a light on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", + "https://api.ring.com/clients_api/doorbots/765432/floodlight_light_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 039c9d0625f..f86e6b25959 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,122 +1,46 @@ """The tests for the Ring sensor platform.""" -from asyncio import run_coroutine_threadsafe -import unittest -from unittest.mock import patch +from .common import setup_platform -import requests_mock - -from homeassistant.components import ring as base_ring -import homeassistant.components.ring.sensor as ring -from homeassistant.helpers.icon import icon_for_battery_level - -from tests.common import get_test_home_assistant, load_fixture, mock_storage -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG +WIFI_ENABLED = False -class TestRingSensorSetup(unittest.TestCase): - """Test the Ring platform.""" +async def test_sensor(hass, requests_mock): + """Test the Ring sensors.""" + await setup_platform(hass, "sensor") - DEVICES = [] + front_battery_state = hass.states.get("sensor.front_battery") + assert front_battery_state is not None + assert front_battery_state.state == "80" - def add_entities(self, devices, action): - """Mock add devices.""" - for device in devices: - self.DEVICES.append(device) + front_door_battery_state = hass.states.get("sensor.front_door_battery") + assert front_door_battery_state is not None + assert front_door_battery_state.state == "100" - def setUp(self): - """Initialize values for this testcase class.""" - self.hass = get_test_home_assistant() - self.config = { - "username": "foo", - "password": "bar", - "monitored_conditions": [ - "battery", - "last_activity", - "last_ding", - "last_motion", - "volume", - "wifi_signal_category", - "wifi_signal_strength", - ], - } + downstairs_volume_state = hass.states.get("sensor.downstairs_volume") + assert downstairs_volume_state is not None + assert downstairs_volume_state.state == "2" - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + front_door_last_activity_state = hass.states.get("sensor.front_door_last_activity") + assert front_door_last_activity_state is not None - @requests_mock.Mocker() - def test_sensor(self, mock): - """Test the Ring sensor class and methods.""" - mock.post( - "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") - ) - mock.post( - "https://api.ring.com/clients_api/session", - text=load_fixture("ring_session.json"), - ) - mock.get( - "https://api.ring.com/clients_api/ring_devices", - text=load_fixture("ring_devices.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/history", - text=load_fixture("ring_doorbots.json"), - ) - mock.get( - "https://api.ring.com/clients_api/doorbots/987652/health", - text=load_fixture("ring_doorboot_health_attrs.json"), - ) - mock.get( - "https://api.ring.com/clients_api/chimes/999999/health", - text=load_fixture("ring_chime_health_attrs.json"), - ) + downstairs_wifi_signal_strength_state = hass.states.get( + "sensor.downstairs_wifi_signal_strength" + ) - with mock_storage(), patch("homeassistant.components.ring.PLATFORMS", []): - run_coroutine_threadsafe( - base_ring.async_setup(self.hass, VALID_CONFIG), self.hass.loop - ).result() - run_coroutine_threadsafe( - self.hass.async_block_till_done(), self.hass.loop - ).result() - run_coroutine_threadsafe( - ring.async_setup_entry(self.hass, None, self.add_entities), - self.hass.loop, - ).result() + if not WIFI_ENABLED: + return - for device in self.DEVICES: - # Mimick add to hass - device.hass = self.hass - run_coroutine_threadsafe( - device.async_added_to_hass(), self.hass.loop, - ).result() + assert downstairs_wifi_signal_strength_state is not None + assert downstairs_wifi_signal_strength_state.state == "-39" - # Entity update data from ring data - device.update() - if device.name == "Front Battery": - expected_icon = icon_for_battery_level( - battery_level=int(device.state), charging=False - ) - assert device.icon == expected_icon - assert 80 == device.state - if device.name == "Front Door Battery": - assert 100 == device.state - if device.name == "Downstairs Volume": - assert 2 == device.state - assert "ring_mock_wifi" == device.device_state_attributes["wifi_name"] - assert "mdi:bell-ring" == device.icon - if device.name == "Front Door Last Activity": - assert not device.device_state_attributes["answered"] - assert "America/New_York" == device.device_state_attributes["timezone"] + front_door_wifi_signal_category_state = hass.states.get( + "sensor.front_door_wifi_signal_category" + ) + assert front_door_wifi_signal_category_state is not None + assert front_door_wifi_signal_category_state.state == "good" - if device.name == "Downstairs WiFi Signal Strength": - assert -39 == device.state - - if device.name == "Front Door WiFi Signal Category": - assert "good" == device.state - - if device.name == "Front Door WiFi Signal Strength": - assert -58 == device.state - - assert device.entity_picture is None - assert ATTRIBUTION == device.device_state_attributes["attribution"] - assert not device.should_poll + front_door_wifi_signal_strength_state = hass.states.get( + "sensor.front_door_wifi_signal_strength" + ) + assert front_door_wifi_signal_strength_state is not None + assert front_door_wifi_signal_strength_state.state == "-58" diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 15f4dd86a39..e2a86014f1c 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -12,10 +12,10 @@ async def test_entity_registry(hass, requests_mock): entity_registry = await hass.helpers.entity_registry.async_get_registry() entry = entity_registry.async_get("switch.front_siren") - assert entry.unique_id == "aacdef123-siren" + assert entry.unique_id == "765432-siren" entry = entity_registry.async_get("switch.internal_siren") - assert entry.unique_id == "aacdef124-siren" + assert entry.unique_id == "345678-siren" async def test_siren_off_reports_correctly(hass, requests_mock): @@ -43,7 +43,7 @@ async def test_siren_can_be_turned_on(hass, requests_mock): # Mocks the response for turning a siren on requests_mock.put( - "https://api.ring.com/clients_api/doorbots/987652/siren_on", + "https://api.ring.com/clients_api/doorbots/765432/siren_on", text=load_fixture("ring_doorbot_siren_on_response.json"), ) diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 557aef3535c..2d2ec893a74 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json index fa3c0586101..3668a2b13dc 100644 --- a/tests/fixtures/ring_devices_updated.json +++ b/tests/fixtures/ring_devices_updated.json @@ -9,7 +9,7 @@ "do_not_disturb": {"seconds_left": 0}, "features": {"ringtones_enabled": true}, "firmware_version": "1.2.3", - "id": 999999, + "id": 123456, "kind": "chime", "latitude": 12.000000, "longitude": -70.12345, @@ -42,7 +42,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.4.26", - "id": 987652, + "id": 987654, "kind": "lpd_v1", "latitude": 12.000000, "longitude": -70.12345, @@ -93,7 +93,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 765432, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "on", @@ -231,7 +231,7 @@ "shadow_correction_enabled": false, "show_recordings": true}, "firmware_version": "1.9.3", - "id": 987652, + "id": 345678, "kind": "hp_cam_v1", "latitude": 12.000000, "led_status": "off", From 5fdc60e067318b8fb605eaeb9858f4e1cc21237b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 13:03:02 -0800 Subject: [PATCH 100/393] Add Safe Mode (#30723) * Store last working HTTP settings * Add safe mode * Fix tests * Add cloud to safe mode * Update logging text * Fix camera tests leaving files behind * Make emulated_hue tests not leave files behind * Make logbook tests not leave files behind * Make tts tests not leave files behind * Make image_processing tests not leave files behind * Make manual_mqtt tests not leave files behind --- CODEOWNERS | 1 + homeassistant/__main__.py | 60 ++--- homeassistant/bootstrap.py | 132 +++++------ homeassistant/components/frontend/__init__.py | 9 +- homeassistant/components/http/__init__.py | 19 +- .../components/safe_mode/__init__.py | 15 ++ .../components/safe_mode/manifest.json | 12 + homeassistant/config.py | 63 ++--- homeassistant/helpers/check_config.py | 8 +- homeassistant/scripts/ensure_config.py | 7 +- homeassistant/setup.py | 2 +- tests/components/automation/test_init.py | 23 +- tests/components/camera/test_init.py | 132 ++++------- tests/components/emulated_hue/test_upnp.py | 9 +- tests/components/frontend/test_init.py | 4 +- tests/components/group/test_init.py | 5 +- tests/components/homeassistant/test_scene.py | 4 +- tests/components/http/test_init.py | 13 + .../components/image_processing/test_init.py | 2 - tests/components/input_boolean/test_init.py | 15 +- tests/components/input_datetime/test_init.py | 19 +- tests/components/input_number/test_init.py | 19 +- tests/components/input_select/test_init.py | 19 +- tests/components/input_text/test_init.py | 19 +- tests/components/logbook/test_init.py | 3 +- .../manual_mqtt/test_alarm_control_panel.py | 6 - tests/components/person/test_init.py | 2 +- tests/components/safe_mode/__init__.py | 1 + tests/components/safe_mode/test_init.py | 9 + tests/components/script/test_init.py | 9 +- tests/components/timer/test_init.py | 19 +- tests/components/tts/test_init.py | 4 + tests/scripts/test_check_config.py | 2 +- tests/test_bootstrap.py | 222 ++++++++++++++---- tests/test_config.py | 22 +- 35 files changed, 480 insertions(+), 430 deletions(-) create mode 100644 homeassistant/components/safe_mode/__init__.py create mode 100644 homeassistant/components/safe_mode/manifest.json create mode 100644 tests/components/safe_mode/__init__.py create mode 100644 tests/components/safe_mode/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 7ef5986a4da..9d2f4eb390b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -278,6 +278,7 @@ 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 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/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index efb1c34653b..8039b9947e7 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 @@ -362,11 +362,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/http/__init__.py b/homeassistant/components/http/__init__.py index 0d93461f90f..58cfb4b9cc1 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,10 @@ 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) + await store.async_save(conf) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server) hass.http = server 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/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/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/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..2b96bb3ea9d 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", diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index b0196fdfe60..49707ac66b0 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -496,14 +496,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 +550,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 +588,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 diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index de48a1d48f3..7f5b2bd20b9 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): 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/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/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/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 5df6bd6ad52..c423f66c7b8 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -18,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() @@ -28,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() diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 212ae7499ab..43a39302f4f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -240,3 +240,16 @@ 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()} + + await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) + + await hass.async_start() + + assert await hass.components.http.async_get_last_config() == http.HTTP_SCHEMA( + config + ) 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..c9f894656ea 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()) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 6908c4fc5f1..fdf6e9296be 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -345,21 +345,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()) diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index f9763168354..4005268c5ba 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -338,21 +338,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()) diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 13669ea507f..a3856277704 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -415,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()) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index d6478a5472f..41f94b51732 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -288,21 +288,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()) 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/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/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/script/test_init.py b/tests/components/script/test_init.py index e008984f47c..cb66c26b6a3 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) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 56675c9d893..bfb1f8fdd30 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -259,21 +259,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()) 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/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..8b6d8addb30 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() From 9f62b5892936da9f826d7b697535e8c96a37d2da Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 23:02:49 +0100 Subject: [PATCH 101/393] Revert #29701 (#30766) * Revert #29701 * fix format * Fix lint --- homeassistant/components/zwave/__init__.py | 5 - homeassistant/components/zwave/cover.py | 144 --------------------- 2 files changed, 149 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 414e7f0017c..9b9236de1c2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -79,14 +79,12 @@ CONF_REFRESH_DELAY = "delay" CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" -CONF_TILT_OPEN_POSITION = "tilt_open_position" DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 -DEFAULT_CONF_TILT_OPEN_POSITION = 50 SUPPORTED_PLATFORMS = [ "binary_sensor", @@ -216,9 +214,6 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( vol.Optional( CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY ): cv.positive_int, - vol.Optional( - CONF_TILT_OPEN_POSITION, default=DEFAULT_CONF_TILT_OPEN_POSITION - ): cv.positive_int, } ) diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index b6f54fc7d07..95cc994e4ff 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -3,7 +3,6 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, - ATTR_TILT_POSITION, DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -15,13 +14,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, - CONF_TILT_OPEN_POSITION, ZWaveDeviceEntity, workaround, ) from .const import ( COMMAND_CLASS_BARRIER_OPERATOR, - COMMAND_CLASS_MANUFACTURER_PROPRIETARY, COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, @@ -32,23 +29,6 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -def _to_hex_str(id_in_bytes): - """Convert a two byte value to a hex string. - - Example: 0x1234 --> '0x1234' - """ - return f"0x{id_in_bytes:04x}" - - -# For some reason node.manufacturer_id is of type string. So we need to convert -# the values. -FIBARO = _to_hex_str(workaround.FIBARO) -FIBARO222_SHUTTERS = [ - _to_hex_str(workaround.FGR222_SHUTTER2), - _to_hex_str(workaround.FGRM222_SHUTTER2), -] - - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave covers.""" pass @@ -73,17 +53,6 @@ def get_device(hass, values, node_config, **kwargs): values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0 ): - if ( - values.primary.node.manufacturer_id == FIBARO - and values.primary.node.product_type in FIBARO222_SHUTTERS - ): - return FibaroFGRM222( - hass, - values, - invert_buttons, - invert_percent, - node_config.get(CONF_TILT_OPEN_POSITION), - ) return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: return ZwaveGarageDoorSwitch(values) @@ -243,116 +212,3 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase): def open_cover(self, **kwargs): """Open the garage door.""" self.values.primary.data = "Opened" - - -class FibaroFGRM222(ZwaveRollershutter): - """Implementation of proprietary features for Fibaro FGR-222 / FGRM-222. - - This adds support for the tilt feature for the ventian blind mode. - To enable this you need to configure the devices to use the venetian blind - mode and to enable the proprietary command class: - * Set "3: Reports type to Blind position reports sent" - to value "the main controller using Fibaro Command Class" - * Set "10: Roller Shutter operating modes" - to value "2 - Venetian Blind Mode, with positioning" - """ - - def __init__( - self, hass, values, invert_buttons, invert_percent, open_tilt_position: int - ): - """Initialize the FGRM-222.""" - self._value_blinds = None - self._value_tilt = None - self._has_tilt_mode = False # type: bool - self._open_tilt_position = 50 # type: int - if open_tilt_position is not None: - self._open_tilt_position = open_tilt_position - super().__init__(hass, values, invert_buttons, invert_percent) - - @property - def current_cover_tilt_position(self) -> int: - """Get the tilt of the blinds. - - Saturate values <5 and >94 so that it's easier to detect the end - positions in automations. - """ - if not self._has_tilt_mode: - return None - if self._value_tilt.data <= 5: - return 0 - if self._value_tilt.data >= 95: - return 100 - return self._value_tilt.data - - def set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - if not self._has_tilt_mode: - _LOGGER.error("Can't set cover tilt as device is not yet set up.") - else: - # Limit the range to [0-99], as this what that the ZWave command - # accepts. - tilt_position = max(0, min(99, kwargs.get(ATTR_TILT_POSITION))) - _LOGGER.debug("setting tilt to %d", tilt_position) - self._value_tilt.data = tilt_position - - def open_cover_tilt(self, **kwargs): - """Set slats to horizontal position.""" - self.set_cover_tilt_position(tilt_position=self._open_tilt_position) - - def close_cover_tilt(self, **kwargs): - """Close the slats.""" - self.set_cover_tilt_position(tilt_position=0) - - def set_cover_position(self, **kwargs): - """Move the roller shutter to a specific position. - - If the venetian blinds mode is not activated, fall back to - the behavior of the parent class. - """ - if not self._has_tilt_mode: - super().set_cover_position(**kwargs) - else: - _LOGGER.debug("Setting cover position to %s", kwargs.get(ATTR_POSITION)) - self._value_blinds.data = kwargs.get(ATTR_POSITION) - - def _configure_values(self): - """Get the value objects from the node.""" - for value in self.node.get_values( - class_id=COMMAND_CLASS_MANUFACTURER_PROPRIETARY - ).values(): - if value is None: - continue - if value.index == 0: - self._value_blinds = value - elif value.index == 1: - self._value_tilt = value - else: - _LOGGER.warning( - "Undefined index %d for this command class", value.index - ) - - if self._value_tilt is not None: - # We reached here because the user has configured the Fibaro to - # report using the MANUFACTURER_PROPRIETARY command class. The only - # reason for the user to configure this way is if tilt support is - # needed (aka venetian blind mode). Therefore, turn it on. - # - # Note: This is safe to do even if the user has accidentally set - # this configuration parameter, or configuration parameter 10 to - # something other than venetian blind mode. The controller will just - # ignore potential tilt settings sent from Home Assistant in this - # case. - self._has_tilt_mode = True - _LOGGER.info( - "Zwave node %s is a Fibaro FGR-222/FGRM-222 with tilt support.", - self.node_id, - ) - - def update_properties(self): - """React on properties being updated.""" - if not self._has_tilt_mode: - self._configure_values() - if self._value_blinds is not None: - self._current_position = self._value_blinds.data - else: - super().update_properties() From 4731f7c721ecd53e00e03c4ff81d847cc7cee29f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 23:49:56 +0100 Subject: [PATCH 102/393] Hass.io allow to reset password with CLI (#30755) * Hass.io allow to reset passwort with CLI * Add improvments * fix comments * fix lint * Fix tests * more tests * Address comments * sort imports * fix test python37 --- homeassistant/components/hassio/__init__.py | 4 +- homeassistant/components/hassio/auth.py | 89 +++++++++++--- homeassistant/components/hassio/handler.py | 2 +- tests/components/hassio/conftest.py | 13 ++- tests/components/hassio/test_auth.py | 122 ++++++++++++++++++-- 5 files changed, 197 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 28e06cc5d6a..f70e44cfa55 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -201,7 +201,7 @@ async def async_setup(hass, config): 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/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 From fbf5e320f753ca1b155f3d2856164e6d22db4ef4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jan 2020 15:06:30 -0800 Subject: [PATCH 103/393] Whitelist Frenck for release --- azure-pipelines-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index ddf95d354c3..135057f2ae4 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -43,7 +43,7 @@ stages: release="$(Build.SourceBranchName)" created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then exit 0 fi From 6f84723fec13acd42fe07e937c7513b3fc117fc7 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 15 Jan 2020 00:31:52 +0100 Subject: [PATCH 104/393] Upgrade youtube_dl to version 2020.01.15 (#30767) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4781b2c7693..9b02e8266ab 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.15"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index fa90fff639a..ed6bba9174c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2100,7 +2100,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.01.01 +youtube_dl==2020.01.15 # homeassistant.components.zengge zengge==0.2 From 0a7feba855e4530cc32386b2c24907a7b224e7c6 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 14 Jan 2020 18:32:48 -0500 Subject: [PATCH 105/393] Use storage based collections for Timer platform (#30765) * Use config dict for timer entity. * Manage timer entities using collection helpers. * Add tests. * Make Timer duration JSON serializable. --- homeassistant/components/timer/__init__.py | 182 +++++++++++++++------ tests/components/timer/test_init.py | 177 +++++++++++++++++++- 2 files changed, 309 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 8eb3f8b353a..575099d1a4a 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, entity_registry 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,55 @@ 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() - if conf is None: + 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[DOMAIN].items()] + ) + 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: typing.Optional[typing.Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: return - new_entities = await _async_process_config(hass, conf) - if new_entities: - await component.async_add_entities(new_entities) + + 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) + + 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: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf[DOMAIN].items()] + ) homeassistant.helpers.service.async_register_admin_service( hass, @@ -96,43 +157,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 +214,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 +230,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. @@ -189,18 +268,18 @@ class Timer(RestoreEntity): 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.""" @@ -212,8 +291,8 @@ class Timer(RestoreEntity): 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 +302,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 +313,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 +324,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/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index bfb1f8fdd30..99084239c76 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 @@ -283,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) @@ -359,3 +404,133 @@ 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 From 8093da6a0cebde7d7df75c659ba8198829cfeb18 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 15 Jan 2020 00:31:56 +0000 Subject: [PATCH 106/393] [ci skip] Translation update --- .../components/airly/.translations/lb.json | 3 +++ .../components/airly/.translations/pl.json | 3 +++ .../components/airly/.translations/ru.json | 3 +++ .../components/brother/.translations/nl.json | 18 +++++++++++++ .../components/local_ip/.translations/nl.json | 6 +++-- .../components/netatmo/.translations/ca.json | 17 ++++++++++++ .../components/netatmo/.translations/nl.json | 17 ++++++++++++ .../components/netatmo/.translations/pl.json | 18 +++++++++++++ .../components/ring/.translations/ca.json | 16 +++++++++++ .../components/ring/.translations/nl.json | 24 +++++++++++++++++ .../components/ring/.translations/pl.json | 27 +++++++++++++++++++ .../samsungtv/.translations/ca.json | 17 ++++++++++++ .../samsungtv/.translations/nl.json | 22 +++++++++++++++ .../components/sentry/.translations/nl.json | 7 +++++ 14 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/brother/.translations/nl.json create mode 100644 homeassistant/components/netatmo/.translations/ca.json create mode 100644 homeassistant/components/netatmo/.translations/nl.json create mode 100644 homeassistant/components/netatmo/.translations/pl.json create mode 100644 homeassistant/components/ring/.translations/ca.json create mode 100644 homeassistant/components/ring/.translations/nl.json create mode 100644 homeassistant/components/ring/.translations/pl.json create mode 100644 homeassistant/components/samsungtv/.translations/ca.json create mode 100644 homeassistant/components/samsungtv/.translations/nl.json create mode 100644 homeassistant/components/sentry/.translations/nl.json 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/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/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/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/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/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/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/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/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/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/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/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/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 From 8af946fba597aa7caf171c96ad6bbd23b9d45024 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 15 Jan 2020 03:36:21 +0100 Subject: [PATCH 107/393] Search: Add search to default config and don't resolve area (#30762) * Add search to default config and don't resolve area * Move to frontend * Minor update Co-authored-by: Jason Hu --- homeassistant/components/frontend/manifest.json | 11 ++++------- homeassistant/components/search/__init__.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1bc1900ee94..26ee4ce52b5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,20 +2,17 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": [ - "home-assistant-frontend==20200108.0" - ], + "requirements": ["home-assistant-frontend==20200108.0"], "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/search/__init__.py b/homeassistant/components/search/__init__.py index 574fc5ee773..51de916f456 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -58,7 +58,7 @@ class Searcher: """ # These types won't be further explored. Config entries + Output types. - DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry"} + DONT_RESOLVE = {"scene", "automation", "script", "group", "config_entry", "area"} def __init__( self, From 5fa7d6f22a4a3c7f3ae99b91375080181b099b23 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 14 Jan 2020 22:15:59 -0500 Subject: [PATCH 108/393] Allow input_* and timer component setup without config (#30772) * Allow input_boolean setup without config. * Allow input_number setup without config. * Allow input_select setup without config. * Allow input_text setup without config. * Allow timer setup without config. --- .../components/input_boolean/__init__.py | 7 +++++-- .../components/input_number/__init__.py | 5 ++--- .../components/input_select/__init__.py | 5 ++--- .../components/input_text/__init__.py | 5 ++--- homeassistant/components/timer/__init__.py | 4 ++-- tests/components/input_boolean/test_init.py | 19 +++++++++++++++++++ tests/components/input_number/test_init.py | 19 +++++++++++++++++++ tests/components/input_select/test_init.py | 19 +++++++++++++++++++ tests/components/input_text/test_init.py | 19 +++++++++++++++++++ tests/components/timer/test_init.py | 19 +++++++++++++++++++ 10 files changed, 108 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index a12c6552399..c805af0a758 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -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() @@ -132,7 +132,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_number/__init__.py b/homeassistant/components/input_number/__init__.py index deedfdab2de..4205389d9b2 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -105,7 +105,6 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -135,7 +134,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() @@ -162,7 +161,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if conf is None: conf = {DOMAIN: {}} await yaml_collection.async_load( - [{CONF_ID: id_, **conf} for id_, conf in conf[DOMAIN].items()] + [{CONF_ID: id_, **conf} for id_, conf in conf.get(DOMAIN, {}).items()] ) homeassistant.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 937af76ed4f..8d86904e5f9 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -81,7 +81,6 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -109,7 +108,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await yaml_collection.async_load( - [{CONF_ID: id_, **cfg} for id_, cfg in config[DOMAIN].items()] + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] ) await storage_collection.async_load() @@ -136,7 +135,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if conf is None: conf = {DOMAIN: {}} await yaml_collection.async_load( - [{CONF_ID: id_, **cfg} for id_, cfg in conf[DOMAIN].items()] + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] ) homeassistant.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 81099e20418..c439d177224 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -109,7 +109,6 @@ CONFIG_SCHEMA = vol.Schema( ) ) }, - required=True, extra=vol.ALLOW_EXTRA, ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -137,7 +136,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() @@ -164,7 +163,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if conf is None: conf = {DOMAIN: {}} await yaml_collection.async_load( - [{CONF_ID: id_, **(cfg or {})} for id_, cfg in conf[DOMAIN].items()] + [{CONF_ID: id_, **(cfg or {})} for id_, cfg in conf.get(DOMAIN, {}).items()] ) homeassistant.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 575099d1a4a..1216bc8a239 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -111,7 +111,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: collection.attach_entity_component_collection(component, storage_collection, Timer) await yaml_collection.async_load( - [{CONF_ID: id_, **cfg} for id_, cfg in config[DOMAIN].items()] + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] ) await storage_collection.async_load() @@ -138,7 +138,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if conf is None: conf = {DOMAIN: {}} await yaml_collection.async_load( - [{CONF_ID: id_, **cfg} for id_, cfg in conf[DOMAIN].items()] + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] ) homeassistant.helpers.service.async_register_admin_service( diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index c9f894656ea..c0bdad3eacd 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -333,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_number/test_init.py b/tests/components/input_number/test_init.py index 4005268c5ba..8331e1374c8 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -543,3 +543,22 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): 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 a3856277704..5c470ca5bfc 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -595,3 +595,22 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): 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_text/test_init.py b/tests/components/input_text/test_init.py index 41f94b51732..304f7e09495 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -483,3 +483,22 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): 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/timer/test_init.py b/tests/components/timer/test_init.py index 99084239c76..dcf9c36474f 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -534,3 +534,22 @@ async def test_ws_create(hass, hass_ws_client, storage_setup): 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()) From ac771addc16c85d7b6329924eda1832d4e945449 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 15 Jan 2020 05:43:55 -0500 Subject: [PATCH 109/393] Add Config Flow support, Device Registry support, available property to vizio component (#30653) * add config flow support, device registry support, available property * raise PlatformNotReady if HA cant connect to device * remove test logging statement and fix integration title * store import and last user input values so user can see errors next to value that caused error * add PARALLEL_UPDATES * add missing type hints * add missing type hints to tests * fix options config flow title * changes based on review * better key name for message when cant connect * fix missed update to key name * fix comments * remove logger from test which was used to debug and update test function names and docstrings to be more accurate * add __init__.py to vizio tests module * readded options flow and updated main component to handle options updates, set unique ID to serial, fixes based on review * pop hass.data in media_player unload instead of in __init__ since it is set in media_player * update requirements_all and requirements_test_all * make unique_id key name a constant * remove additional line breaks after docstrings * unload entries during test_user_flow and test_import_flow tests to hopefully reduce teardown time * try to speed up tests * remove unnecessary code, use event bus to track options updates, move patches to pytest fixtures and fix patch scoping * fix comment * remove translations from commit * suppress API error logging when checking for device availability as it can spam logs * update requirements_all and requirements_test_all * dont pass hass to entity since it is passed to entity anyway, remove entity unload from tests, other misc changes from review * fix clearing listeners * use config_entry unique ID for unique ID and use config_entry entry ID as update signal * update config flow based on suggested changes * update volume step on config import if it doesn't match config_entry volume step * update config_entry data and options with new volume step value * copy entry.data and entry.options before updating when updating config_entry * fix test_import_entity_already_configured --- homeassistant/components/vizio/__init__.py | 43 ++- homeassistant/components/vizio/config_flow.py | 171 +++++++++++ homeassistant/components/vizio/const.py | 1 - homeassistant/components/vizio/manifest.json | 5 +- .../components/vizio/media_player.py | 185 +++++++---- homeassistant/components/vizio/strings.json | 40 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/vizio/__init__.py | 1 + tests/components/vizio/test_config_flow.py | 289 ++++++++++++++++++ 11 files changed, 669 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/vizio/config_flow.py create mode 100644 homeassistant/components/vizio/strings.json create mode 100644 tests/components/vizio/__init__.py create mode 100644 tests/components/vizio/test_config_flow.py diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index aa6f724bc59..c890af2700d 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,7 +1,7 @@ """The vizio component.""" - import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS, @@ -9,13 +9,14 @@ from homeassistant.const import ( CONF_NAME, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( CONF_VOLUME_STEP, DEFAULT_DEVICE_CLASS, DEFAULT_NAME, DEFAULT_VOLUME_STEP, + DOMAIN, ) @@ -42,3 +43,41 @@ VIZIO_SCHEMA = { 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, +) + + +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, entry: ConfigEntry) -> bool: + """Load the saved entities.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "media_player") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(entry, "media_player") + + return True diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py new file mode 100644 index 00000000000..5863d89c972 --- /dev/null +++ b/homeassistant/components/vizio/config_flow.py @@ -0,0 +1,171 @@ +"""Config flow for Vizio.""" +import logging +from typing import Any, Dict + +from pyvizio import VizioAsync +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, +) +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 update_schema_defaults(input_dict: Dict[str, Any]) -> vol.Schema: + """Update schema defaults based on user input/config dict. Retains info already provided for future form views.""" + 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(["tv", "soundbar"])), + vol.Optional( + CONF_ACCESS_TOKEN, default=input_dict.get(CONF_ACCESS_TOKEN, "") + ): str, + }, + extra=vol.REMOVE_EXTRA, + ) + + +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): + """Get the options flow for this handler.""" + return VizioOptionsConfigFlow(config_entry) + + def __init__(self) -> None: + """Initialize config flow.""" + self.import_schema = None + self.user_schema = 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 = update_schema_defaults(user_input) + + # Check if new config entry matches any existing config entries + for entry in self.hass.config_entries.async_entries(DOMAIN): + if 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: + unique_id = await VizioAsync.get_unique_id( + user_input[CONF_HOST], + user_input.get(CONF_ACCESS_TOKEN), + user_input[CONF_DEVICE_CLASS], + ) + + # Abort flow if existing component with same unique ID matches new config entry + if await self.async_set_unique_id( + unique_id=unique_id, raise_on_progress=True + ): + return self.async_abort(reason="already_setup") + + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + schema = self.user_schema or self.import_schema or update_schema_defaults({}) + + 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 entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[ + CONF_NAME + ] == import_config.get(CONF_NAME): + if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: + new_volume_step = { + CONF_VOLUME_STEP: import_config[CONF_VOLUME_STEP] + } + self.hass.config_entries.async_update_entry( + entry=entry, + data=entry.data.copy().update(new_volume_step), + options=entry.options.copy().update(new_volume_step), + ) + return self.async_abort(reason="updated_volume_step") + return self.async_abort(reason="already_setup") + + # Store import values in case setup fails so user can see error + self.import_schema = update_schema_defaults(import_config) + + return await self.async_step_user(user_input=import_config) + + +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)) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index c4c1ba3199b..b87e40d3b46 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,5 +1,4 @@ """Constants used by vizio component.""" - CONF_VOLUME_STEP = "volume_step" DEFAULT_NAME = "Vizio SmartCast" diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 5a4c0f4a4cc..0a44a638d44 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,8 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.0.15"], + "requirements": ["pyvizio==0.0.20"], "dependencies": [], - "codeowners": ["@raman325"] + "codeowners": ["@raman325"], + "config_flow": true } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 84b745baf92..44f44c0c48e 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,14 +1,12 @@ """Vizio SmartCast Device support.""" - from datetime import timedelta import logging -from typing import Any, Callable, Dict, List +from typing import Callable, List from pyvizio import VizioAsync -import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -19,6 +17,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS, @@ -27,18 +26,24 @@ 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 ConfigType, HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType -from . import VIZIO_SCHEMA, validate_auth -from .const import CONF_VOLUME_STEP, DEVICE_ID, ICON +from .const import CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, ICON _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) +PARALLEL_UPDATES = 0 + COMMON_SUPPORTED_COMMANDS = ( SUPPORT_SELECT_SOURCE | SUPPORT_TURN_ON @@ -54,26 +59,35 @@ SUPPORTED_COMMANDS = { } -PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth) - - -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistantType, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: Callable[[List[Entity], bool], None], - discovery_info: Dict[str, Any] = None, -): - """Set up the Vizio media player platform.""" +) -> bool: + """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_type = config_entry.data[CONF_DEVICE_CLASS] - 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] + # If config entry options not set up, set them up, otherwise assign values managed in options + if CONF_VOLUME_STEP not in 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] device = VizioAsync( - DEVICE_ID, host, name, token, device_type, async_get_clientsession(hass, False) + DEVICE_ID, + host, + name, + token, + device_type, + session=async_get_clientsession(hass, False), ) + if not await device.can_connect(): fail_auth_msg = "" if token: @@ -83,18 +97,27 @@ async def async_setup_platform( "is valid and available, device type is correct%s", fail_auth_msg, ) - return + raise PlatformNotReady - async_add_entities([VizioDevice(device, name, volume_step, device_type)], True) + entity = VizioDevice(config_entry, device, name, volume_step, device_type) + + async_add_entities([entity], True) class VizioDevice(MediaPlayerDevice): """Media Player implementation which performs REST requests to device.""" def __init__( - self, device: VizioAsync, name: str, volume_step: int, device_type: str + self, + config_entry: ConfigEntry, + device: VizioAsync, + name: str, + volume_step: int, + device_type: str, ) -> None: """Initialize Vizio device.""" + self._config_entry = config_entry + self._async_unsub_listeners = [] self._name = name self._state = None @@ -106,104 +129,140 @@ class VizioDevice(MediaPlayerDevice): self._supported_commands = SUPPORTED_COMMANDS[device_type] self._device = device self._max_volume = float(self._device.get_max_volume()) - self._unique_id = None self._icon = ICON[device_type] + self._available = True @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update(self) -> None: """Retrieve latest state of the device.""" + is_on = await self._device.get_power_state(False) - if not self._unique_id: - self._unique_id = await self._device.get_esn() + if is_on is None: + self._available = False + return - is_on = await self._device.get_power_state() - - if is_on: - self._state = STATE_ON - - volume = await self._device.get_current_volume() - if volume is not None: - self._volume_level = float(volume) / self._max_volume - - input_ = await self._device.get_current_input() - if input_ is not None: - self._current_input = input_.meta_name - - inputs = await 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(False) + if volume is not None: + self._volume_level = float(volume) / self._max_volume + + input_ = await self._device.get_current_input(False) + if input_ is not None: + self._current_input = input_.meta_name + + inputs = await self._device.get_inputs(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 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) -> str: """Return the name of the device.""" - return self._name @property def icon(self) -> str: """Return the icon of the device.""" - return self._icon @property def volume_level(self) -> float: """Return the volume level of the device.""" - return self._volume_level @property def source(self) -> str: """Return current input of the device.""" - return self._current_input @property def source_list(self) -> List: """Return list of available inputs of the device.""" - return self._available_inputs @property def supported_features(self) -> int: """Flag device features that are supported.""" - return self._supported_commands @property def unique_id(self) -> str: """Return the unique id of the device.""" + return self._config_entry.unique_id - return self._unique_id + @property + def device_info(self): + """Return device registry information.""" + return { + "identifiers": {(DOMAIN, self._config_entry.unique_id)}, + "name": self.name, + "manufacturer": "VIZIO", + } async def async_turn_on(self) -> None: """Turn the device on.""" - await self._device.pow_on() async def async_turn_off(self) -> None: """Turn the device off.""" - await self._device.pow_off() async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" - if mute: await self._device.mute_on() else: @@ -211,22 +270,18 @@ class VizioDevice(MediaPlayerDevice): async def async_media_previous_track(self) -> None: """Send previous channel command.""" - await self._device.ch_down() async def async_media_next_track(self) -> None: """Send next channel command.""" - await self._device.ch_up() async def async_select_source(self, source: str) -> None: """Select input source.""" - await self._device.input_switch(source) async def async_volume_up(self) -> None: """Increasing volume of the device.""" - await self._device.vol_up(self._volume_step) if self._volume_level is not None: @@ -236,7 +291,6 @@ class VizioDevice(MediaPlayerDevice): async def async_volume_down(self) -> None: """Decreasing volume of the device.""" - await self._device.vol_down(self._volume_step) if self._volume_level is not None: @@ -246,7 +300,6 @@ class VizioDevice(MediaPlayerDevice): 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)) diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json new file mode 100644 index 00000000000..029643ab578 --- /dev/null +++ b/homeassistant/components/vizio/strings.json @@ -0,0 +1,40 @@ +{ + "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": "Host already configured.", + "name_exists": "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_in_progress": "Config flow for vizio component already in progress.", + "already_setup": "This entry has already been setup.", + "host_exists": "Vizio component with host already configured.", + "name_exists": "Vizio component with name already configured.", + "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." + } + }, + "options": { + "title": "Update Vizo SmartCast Options", + "step": { + "init": { + "title": "Update Vizo SmartCast Options", + "data": { + "volume_step": "Volume Step Size" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f6154e1929d..3886dfd2f20 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -92,6 +92,7 @@ FLOWS = [ "upnp", "velbus", "vesync", + "vizio", "wemo", "withings", "wled", diff --git a/requirements_all.txt b/requirements_all.txt index ed6bba9174c..586a0530fc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.0.15 +pyvizio==0.0.20 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc3b1d18199..1287711f512 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -557,6 +557,9 @@ pyvera==0.3.7 # homeassistant.components.vesync pyvesync==1.1.0 +# homeassistant.components.vizio +pyvizio==0.0.20 + # homeassistant.components.html5 pywebpush==1.9.2 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..9e657cf926d --- /dev/null +++ b/tests/components/vizio/test_config_flow.py @@ -0,0 +1,289 @@ +"""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.vizio import VIZIO_SCHEMA +from homeassistant.components.vizio.const import ( + CONF_VOLUME_STEP, + DEFAULT_NAME, + DEFAULT_VOLUME_STEP, + DOMAIN, +) +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + +NAME = "Vizio" +HOST = "192.168.1.1:9000" +DEVICE_CLASS_TV = "tv" +DEVICE_CLASS_SOUNDBAR = "soundbar" +ACCESS_TOKEN = "deadbeef" +VOLUME_STEP = 2 +UNIQUE_ID = "testid" + +MOCK_USER_VALID_TV_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, + CONF_ACCESS_TOKEN: ACCESS_TOKEN, +} + +MOCK_IMPORT_VALID_TV_ENTRY = { + 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_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_TV, +} + +MOCK_SOUNDBAR_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR, +} + + +@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, + ), patch( + "homeassistant.components.vizio.async_setup_entry", return_value=True + ): + 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) -> None: + """Test user config flow with minimum fields.""" + # test form shows + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR, + }, + ) + + 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_SOUNDBAR + + +async def test_user_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None: + """Test user config flow with all fields.""" + # test form shows + 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" + + 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_user_host_already_configured( + hass: HomeAssistantType, vizio_connect +) -> None: + """Test host is already configured during user setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_SOUNDBAR_ENTRY, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + ) + entry.add_to_hass(hass) + fail_entry = MOCK_SOUNDBAR_ENTRY.copy() + fail_entry[CONF_NAME] = "newtestname" + + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=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 +) -> None: + """Test name is already configured during user setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_SOUNDBAR_ENTRY, + options={CONF_VOLUME_STEP: VOLUME_STEP}, + ) + entry.add_to_hass(hass) + + fail_entry = MOCK_SOUNDBAR_ENTRY.copy() + fail_entry[CONF_HOST] = "0.0.0.0" + + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_user_error_on_could_not_connect( + hass: HomeAssistantType, vizio_cant_connect +) -> None: + """Test with could_not_connect during user_setup.""" + 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" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_USER_VALID_TV_ENTRY + ) + 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 +) -> 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": "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"], MOCK_INVALID_TV_ENTRY + ) + + 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 +) -> None: + """Test import config flow with minimum fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=vol.Schema(VIZIO_SCHEMA)( + {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR} + ), + ) + + 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_SOUNDBAR + assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP + + +async def test_import_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None: + """Test import config flow with all fields.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "import"}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_ENTRY), + ) + + 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 +) -> None: + """Test entity is already configured during import setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_SOUNDBAR_ENTRY), + options={CONF_VOLUME_STEP: VOLUME_STEP}, + ) + entry.add_to_hass(hass) + fail_entry = vol.Schema(VIZIO_SCHEMA)(MOCK_SOUNDBAR_ENTRY.copy()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "import"}, data=fail_entry + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" From 41469cec6cf2f681a83f6fbfe5ff02b498907b2f Mon Sep 17 00:00:00 2001 From: Jan De Luyck <5451787+jdeluyck@users.noreply.github.com> Date: Wed, 15 Jan 2020 16:21:55 +0100 Subject: [PATCH 110/393] Update emulated_roku to 0.1.9 (#30791) * Update emulated_roku to 0.1.9 * Update requirements_all --- homeassistant/components/emulated_roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 05cf72019d8..62d51d7d910 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.1.8"], + "requirements": ["emulated_roku==0.1.9"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 586a0530fc0..e73a1144394 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -474,7 +474,7 @@ eliqonline==1.2.2 elkm1-lib==0.7.15 # homeassistant.components.emulated_roku -emulated_roku==0.1.8 +emulated_roku==0.1.9 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1287711f512..e6942a3cfcd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.emulated_roku -emulated_roku==0.1.8 +emulated_roku==0.1.9 # homeassistant.components.season ephem==3.7.7.0 From 36796ef64974eea1fe365a7d38601aa745fd841f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=BDdrale?= Date: Wed, 15 Jan 2020 16:24:06 +0100 Subject: [PATCH 111/393] Update pyubee to 0.8 (#30785) --- CODEOWNERS | 1 + homeassistant/components/ubee/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9d2f4eb390b..52bd64dffed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -354,6 +354,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 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/requirements_all.txt b/requirements_all.txt index e73a1144394..93bc45470ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1675,7 +1675,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 From de26108b23c6191be7a8c5f2cf8c91f4efb7cb76 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 15 Jan 2020 16:09:05 +0000 Subject: [PATCH 112/393] Restore unit_of_measurement from entity registry (#30780) * Restore unit_of_measurement from entity registry * Lint fix --- homeassistant/helpers/entity_platform.py | 1 + homeassistant/helpers/entity_registry.py | 12 +++++++ tests/common.py | 5 +++ tests/components/homekit/test_type_sensors.py | 35 +++++++++++++++++++ tests/helpers/test_entity_platform.py | 2 ++ 5 files changed, 55 insertions(+) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 53ad54c5ed1..0e4d80ac080 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -355,6 +355,7 @@ class EntityPlatform: capabilities=entity.capability_attributes, supported_features=entity.supported_features, device_class=entity.device_class, + unit_of_measurement=entity.unit_of_measurement, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index acb155ae594..635f7feba13 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -18,6 +18,7 @@ import attr from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, ) @@ -77,6 +78,7 @@ class RegistryEntry: capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) supported_features: int = attr.ib(default=0) device_class: Optional[str] = attr.ib(default=None) + unit_of_measurement: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -164,6 +166,7 @@ class EntityRegistry: capabilities: Optional[Dict[str, Any]] = None, supported_features: Optional[int] = None, device_class: Optional[str] = None, + unit_of_measurement: Optional[str] = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -180,6 +183,7 @@ class EntityRegistry: capabilities=capabilities or _UNDEF, supported_features=supported_features or _UNDEF, device_class=device_class or _UNDEF, + unit_of_measurement=unit_of_measurement or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -210,6 +214,7 @@ class EntityRegistry: capabilities=capabilities, supported_features=supported_features or 0, device_class=device_class, + unit_of_measurement=unit_of_measurement, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -279,6 +284,7 @@ class EntityRegistry: capabilities=_UNDEF, supported_features=_UNDEF, device_class=_UNDEF, + unit_of_measurement=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -293,6 +299,7 @@ class EntityRegistry: ("capabilities", capabilities), ("supported_features", supported_features), ("device_class", device_class), + ("unit_of_measurement", unit_of_measurement), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -369,6 +376,7 @@ class EntityRegistry: capabilities=entity.get("capabilities") or {}, supported_features=entity.get("supported_features", 0), device_class=entity.get("device_class"), + unit_of_measurement=entity.get("unit_of_measurement"), ) self.entities = entities @@ -395,6 +403,7 @@ class EntityRegistry: "capabilities": entry.capabilities, "supported_features": entry.supported_features, "device_class": entry.device_class, + "unit_of_measurement": entry.unit_of_measurement, } for entry in self.entities.values() ] @@ -511,6 +520,9 @@ def async_setup_entity_restore( if entry.device_class is not None: attrs[ATTR_DEVICE_CLASS] = entry.device_class + if entry.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/common.py b/tests/common.py index fd40b08635f..5a00a2bc7df 100644 --- a/tests/common.py +++ b/tests/common.py @@ -922,6 +922,11 @@ class MockEntity(entity.Entity): """Info how device should be classified.""" return self._handle("device_class") + @property + def unit_of_measurement(self): + """Info on the units the entity state is in.""" + return self._handle("unit_of_measurement") + @property def capability_attributes(self): """Info about capabilities.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 43533840cc6..969ea0bddc8 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,4 +1,5 @@ """Test different accessory types: Sensors.""" +from homeassistant.components.homekit import get_accessory from homeassistant.components.homekit.const import ( PROP_CELSIUS, THRESHOLD_CO, @@ -17,6 +18,7 @@ from homeassistant.components.homekit.type_sensors import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, STATE_HOME, STATE_NOT_HOME, STATE_OFF, @@ -25,6 +27,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import CoreState +from homeassistant.helpers import entity_registry async def test_temperature(hass, hk_driver): @@ -262,3 +266,34 @@ async def test_binary_device_classes(hass, hk_driver): acc = BinarySensor(hass, hk_driver, "Binary Sensor", entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char + + +async def test_sensor_restore(hass, hk_driver, events): + """Test setting up an entity from state in the event registry.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "sensor", + "generic", + "1234", + suggested_object_id="temperature", + device_class="temperature", + ) + registry.async_get_or_create( + "sensor", + "generic", + "12345", + suggested_object_id="humidity", + device_class="humidity", + unit_of_measurement="%", + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + acc = get_accessory(hass, hk_driver, hass.states.get("sensor.temperature"), 2, {}) + assert acc.category == 10 + + acc = get_accessory(hass, hk_driver, hass.states.get("sensor.humidity"), 2, {}) + assert acc.category == 10 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 7797bf5057b..8eea8ad004f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -804,6 +804,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, supported_features=5, device_class="mock-device-class", + unit_of_measurement="%", ) await component.async_add_entities([entity_default]) @@ -815,6 +816,7 @@ async def test_entity_info_added_to_entity_registry(hass): assert entry_default.capabilities == {"max": 100} assert entry_default.supported_features == 5 assert entry_default.device_class == "mock-device-class" + assert entry_default.unit_of_measurement == "%" async def test_override_restored_entities(hass): From 1e82813c3b20141d9c1df81a2bafd18d2899de52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 15 Jan 2020 08:10:42 -0800 Subject: [PATCH 113/393] Refactor Ring data handling (#30777) * Refactor Ring data handling * Add async_ to methods --- homeassistant/components/ring/__init__.py | 241 +++++++----- .../components/ring/binary_sensor.py | 103 +++-- homeassistant/components/ring/camera.py | 94 ++--- homeassistant/components/ring/config_flow.py | 3 - homeassistant/components/ring/entity.py | 53 +++ homeassistant/components/ring/light.py | 64 +--- homeassistant/components/ring/sensor.py | 354 +++++++++--------- homeassistant/components/ring/switch.py | 73 +--- tests/components/ring/test_binary_sensor.py | 11 +- 9 files changed, 514 insertions(+), 482 deletions(-) create mode 100644 homeassistant/components/ring/entity.py diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index b35ff630310..7b4fbb15b30 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -4,16 +4,16 @@ from datetime import timedelta from functools import partial import logging from pathlib import Path -from time import time +from typing import Optional +from oauthlib.oauth2 import AccessDeniedError from ring_doorbell import Auth, Ring import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.async_ import run_callback_threadsafe @@ -24,16 +24,8 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_HISTORY = "ring_history" -DATA_HEALTH_DATA_TRACKER = "ring_health_data" -DATA_TRACK_INTERVAL = "ring_track_interval" - DOMAIN = "ring" DEFAULT_ENTITY_NAMESPACE = "ring" -SIGNAL_UPDATE_RING = "ring_update" -SIGNAL_UPDATE_HEALTH_RING = "ring_health_update" - -SCAN_INTERVAL = timedelta(seconds=10) PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") @@ -93,9 +85,36 @@ async def async_setup_entry(hass, entry): auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) ring = Ring(auth) - await hass.async_add_executor_job(ring.update_data) + try: + await hass.async_add_executor_job(ring.update_data) + except AccessDeniedError: + _LOGGER.error("Access token is no longer valid. Please set up Ring again") + return False - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ring + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": ring, + "devices": ring.devices(), + "device_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1) + ), + "dings_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10) + ), + "history_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.history(limit=10), + timedelta(minutes=1), + ), + "health_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.update_health_data(), + timedelta(minutes=1), + ), + } for component in PLATFORMS: hass.async_create_task( @@ -105,25 +124,16 @@ async def async_setup_entry(hass, entry): if hass.services.has_service(DOMAIN, "update"): return True - async def refresh_all(_): - """Refresh all ring accounts.""" - await asyncio.gather( - *[ - hass.async_add_executor_job(api.update_data) - for api in hass.data[DOMAIN].values() - ] - ) - async_dispatcher_send(hass, SIGNAL_UPDATE_RING) + async def async_refresh_all(_): + """Refresh all ring data.""" + for info in hass.data[DOMAIN].values(): + await info["device_data"].async_refresh_all() + await info["dings_data"].async_refresh_all() + await hass.async_add_executor_job(info["history_data"].refresh_all) + await hass.async_add_executor_job(info["health_data"].refresh_all) # register service - hass.services.async_register(DOMAIN, "update", refresh_all) - - # register scan interval for ring - hass.data[DATA_TRACK_INTERVAL] = async_track_time_interval( - hass, refresh_all, SCAN_INTERVAL - ) - hass.data[DATA_HEALTH_DATA_TRACKER] = HealthDataUpdater(hass) - hass.data[DATA_HISTORY] = HistoryCache(hass) + hass.services.async_register(DOMAIN, "update", async_refresh_all) return True @@ -146,98 +156,141 @@ async def async_unload_entry(hass, entry): if len(hass.data[DOMAIN]) != 0: return True - # Last entry unloaded, clean up - hass.data.pop(DATA_TRACK_INTERVAL)() - hass.data.pop(DATA_HEALTH_DATA_TRACKER) - hass.data.pop(DATA_HISTORY) + # Last entry unloaded, clean up service hass.services.async_remove(DOMAIN, "update") return True -class HealthDataUpdater: - """Data storage for health data.""" +class GlobalDataUpdater: + """Data storage for single API endpoint.""" - def __init__(self, hass): - """Track devices that need health data updated.""" + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize global data updater.""" self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval + self.listeners = [] + self._unsub_interval = None + + @callback + def async_add_listener(self, update_callback): + """Listen for data updates.""" + # This is the first listener, set up interval. + if not self.listeners: + self._unsub_interval = async_track_time_interval( + self.hass, self.async_refresh_all, self.update_interval + ) + + self.listeners.append(update_callback) + + @callback + def async_remove_listener(self, update_callback): + """Remove data update.""" + self.listeners.remove(update_callback) + + if not self.listeners: + self._unsub_interval() + self._unsub_interval = None + + async def async_refresh_all(self, _now: Optional[int] = None) -> None: + """Time to update.""" + if not self.listeners: + return + + try: + await self.hass.async_add_executor_job( + getattr(self.ring, self.update_method) + ) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + await self.hass.config_entries.async_unload(self.config_entry_id) + return + + for update_callback in self.listeners: + update_callback() + + +class DeviceDataUpdater: + """Data storage for device data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize device data updater.""" + self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval self.devices = {} self._unsub_interval = None - async def track_device(self, config_entry_id, device): + async def async_track_device(self, device, update_callback): """Track a device.""" if not self.devices: self._unsub_interval = async_track_time_interval( - self.hass, self.refresh_all, SCAN_INTERVAL + self.hass, self.refresh_all, self.update_interval ) - key = (config_entry_id, device.device_id) - - if key not in self.devices: - self.devices[key] = { + if device.device_id not in self.devices: + self.devices[device.device_id] = { "device": device, - "count": 1, + "update_callbacks": [update_callback], + "data": None, } + # Store task so that other concurrent requests can wait for us to finish and + # data be available. + self.devices[device.device_id]["task"] = asyncio.current_task() + self.devices[device.device_id][ + "data" + ] = await self.hass.async_add_executor_job(self.update_method, device) + self.devices[device.device_id].pop("task") else: - self.devices[key]["count"] += 1 + self.devices[device.device_id]["update_callbacks"].append(update_callback) + # If someone is currently fetching data as part of the initialization, wait for them + if "task" in self.devices[device.device_id]: + await self.devices[device.device_id]["task"] - await self.hass.async_add_executor_job(device.update_health_data) + update_callback(self.devices[device.device_id]["data"]) @callback - def untrack_device(self, config_entry_id, device): + def async_untrack_device(self, device, update_callback): """Untrack a device.""" - key = (config_entry_id, device.device_id) - self.devices[key]["count"] -= 1 + self.devices[device.device_id]["update_callbacks"].remove(update_callback) - if self.devices[key]["count"] == 0: - self.devices.pop(key) + if not self.devices[device.device_id]["update_callbacks"]: + self.devices.pop(device.device_id) if not self.devices: self._unsub_interval() self._unsub_interval = None - def refresh_all(self, _): + def refresh_all(self, _=None): """Refresh all registered devices.""" for info in self.devices.values(): - info["device"].update_health_data() + try: + data = info["data"] = self.update_method(info["device"]) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + self.hass.add_job( + self.hass.config_entries.async_unload(self.config_entry_id) + ) + return - dispatcher_send(self.hass, SIGNAL_UPDATE_HEALTH_RING) - - -class HistoryCache: - """Helper to fetch history.""" - - STALE_AFTER = 10 # seconds - - def __init__(self, hass): - """Initialize history cache.""" - self.hass = hass - self.cache = {} - - async def async_get_history(self, config_entry_id, device): - """Get history of a device.""" - key = (config_entry_id, device.device_id) - - if key in self.cache: - info = self.cache[key] - - # We're already fetching data, join that task - if "task" in info: - return await info["task"] - - # We have valid cache info, return that - if time() - info["created_at"] < self.STALE_AFTER: - return info["data"] - - self.cache.pop(key) - - # Fetch data - task = self.hass.async_add_executor_job(partial(device.history, limit=10)) - - self.cache[key] = {"task": task} - - data = await task - - self.cache[key] = {"created_at": time(), "data": data} - - return data + for update_callback in info["update_callbacks"]: + self.hass.loop.call_soon_threadsafe(update_callback, data) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 2dd3682951f..7b20ff948d1 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -1,13 +1,12 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTRIBUTION, DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -22,8 +21,8 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Ring binary sensors from a config entry.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + ring = hass.data[DOMAIN][config_entry.entry_id]["api"] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] sensors = [] @@ -33,49 +32,62 @@ async def async_setup_entry(hass, config_entry, async_add_entities): continue for device in devices[device_type]: - sensors.append(RingBinarySensor(ring, device, sensor_type)) + sensors.append( + RingBinarySensor(config_entry.entry_id, ring, device, sensor_type) + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class RingBinarySensor(BinarySensorDevice): +class RingBinarySensor(RingEntityMixin, BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, ring, device, sensor_type): + _active_alert = None + + def __init__(self, config_entry_id, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - self._sensor_type = sensor_type + super().__init__(config_entry_id, device) self._ring = ring - self._device = device + self._sensor_type = sensor_type self._name = "{0} {1}".format( - self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] + self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None - self._unique_id = f"{self._device.id}-{self._sensor_type}" - self._disp_disconnect = None + self._unique_id = f"{device.id}-{sensor_type}" + self._update_alert() async def async_added_to_hass(self): """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) + await super().async_added_to_hass() + self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback) + self._dings_update_callback() async def async_will_remove_from_hass(self): """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None + await super().async_will_remove_from_hass() + self.ring_objects["dings_data"].async_remove_listener( + self._dings_update_callback + ) @callback - def _update_callback(self): + def _dings_update_callback(self): """Call update method.""" - self.async_schedule_update_ha_state(True) - _LOGGER.debug("Updating Ring binary sensor %s (callback)", self.name) + self._update_alert() + self.async_write_ha_state() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False + @callback + def _update_alert(self): + """Update active alert.""" + self._active_alert = next( + ( + alert + for alert in self._ring.active_alerts() + if alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + ), + None, + ) @property def name(self): @@ -85,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice): @property def is_on(self): """Return True if the binary sensor is on.""" - return self._state + return self._active_alert is not None @property def device_class(self): @@ -97,32 +109,17 @@ class RingBinarySensor(BinarySensorDevice): """Return a unique ID.""" return self._unique_id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + attrs = super().device_state_attributes - if self._device.alert and self._device.alert_expires_at: - attrs["expires_at"] = self._device.alert_expires_at - attrs["state"] = self._device.alert.get("state") + if self._active_alert is None: + return attrs + + attrs["state"] = self._active_alert["state"] + attrs["expires_at"] = datetime.fromtimestamp( + self._active_alert.get("now") + self._active_alert.get("expires_in") + ).isoformat() return attrs - - async def async_update(self): - """Get the latest data and updates the state.""" - self._state = any( - alert["kind"] == self._sensor_type - and alert["doorbot_id"] == self._device.id - for alert in self._ring.active_alerts() - ) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 8ef876e4a00..07d87c85714 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -12,10 +12,10 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ATTRIBUTION, DATA_HISTORY, DOMAIN, SIGNAL_UPDATE_RING +from . import ATTRIBUTION, DOMAIN +from .entity import RingEntityMixin FORCE_REFRESH_INTERVAL = timedelta(minutes=45) @@ -24,8 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] cams = [] for camera in chain( @@ -36,42 +35,52 @@ async def async_setup_entry(hass, config_entry, async_add_entities): cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) - async_add_entities(cams, True) + async_add_entities(cams) -class RingCam(Camera): +class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" - super().__init__() - self._config_entry_id = config_entry_id - self._device = device + super().__init__(config_entry_id, device) + self._name = self._device.name self._ffmpeg = ffmpeg + self._last_event = None self._last_video_id = None self._video_url = None self._utcnow = dt_util.utcnow() self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL - self._disp_disconnect = None async def async_added_to_hass(self): """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback ) async def async_will_remove_from_hass(self): """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) @callback - def _update_callback(self): + def _history_update_callback(self, history_data): """Call update method.""" - self.async_schedule_update_ha_state(True) - _LOGGER.debug("Updating Ring camera %s (callback)", self.name) + if history_data: + self._last_event = history_data[0] + self.async_schedule_update_ha_state(True) + else: + self._last_event = None + self._last_video_id = None + self._video_url = None + self._expires_at = self._utcnow + self.async_write_ha_state() @property def name(self): @@ -83,16 +92,6 @@ class RingCam(Camera): """Return a unique ID.""" return self._device.id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - @property def device_state_attributes(self): """Return the state attributes.""" @@ -104,7 +103,6 @@ class RingCam(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) if self._video_url is None: @@ -136,33 +134,23 @@ class RingCam(Camera): async def async_update(self): """Update camera entity and refresh attributes.""" - _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") - - self._utcnow = dt_util.utcnow() - - data = await self.hass.data[DATA_HISTORY].async_get_history( - self._config_entry_id, self._device - ) - - if not data: + if self._last_event is None: return - last_event = data[0] - last_recording_id = last_event["id"] - video_status = last_event["recording"]["status"] + if self._last_event["recording"]["status"] != "ready": + return - if video_status == "ready" and ( - self._last_video_id != last_recording_id or self._utcnow >= self._expires_at + if ( + self._last_video_id == self._last_event["id"] + and self._utcnow <= self._expires_at ): + return - video_url = await self.hass.async_add_executor_job( - self._device.recording_url, last_recording_id - ) + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, self._last_event["id"] + ) - if video_url: - _LOGGER.debug("Ring DoorBell properties refreshed") - - # update attributes if new video or if URL has expired - self._last_video_id = last_recording_id - self._video_url = video_url - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + if video_url: + self._last_video_id = self._last_event["id"] + self._video_url = video_url + self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index 57f873bd1a6..a25e0283753 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -39,9 +39,6 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - errors = {} if user_input is not None: try: diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py new file mode 100644 index 00000000000..6eb87cb8f9b --- /dev/null +++ b/homeassistant/components/ring/entity.py @@ -0,0 +1,53 @@ +"""Base class for Ring entity.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback + +from . import ATTRIBUTION, DOMAIN + + +class RingEntityMixin: + """Base implementation for Ring device.""" + + def __init__(self, config_entry_id, device): + """Initialize a sensor for Ring device.""" + super().__init__() + self._config_entry_id = config_entry_id + self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" + self.ring_objects["device_data"].async_add_listener(self._update_callback) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + self.ring_objects["device_data"].async_remove_listener(self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_write_ha_state() + + @property + def ring_objects(self): + """Return the Ring API objects.""" + return self.hass.data[DOMAIN][self._config_entry_id] + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, + "manufacturer": "Ring", + } diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 10572e2e0ae..86ef55af16d 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.light import Light from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -25,51 +25,35 @@ OFF_STATE = "off" async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - ring = hass.data[DOMAIN][config_entry.entry_id] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] - devices = ring.devices() lights = [] for device in devices["stickup_cams"]: if device.has_capability("light"): - lights.append(RingLight(device)) + lights.append(RingLight(config_entry.entry_id, device)) - async_add_entities(lights, True) + async_add_entities(lights) -class RingLight(Light): +class RingLight(RingEntityMixin, Light): """Creates a switch to turn the ring cameras light on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the light.""" - self._device = device - self._unique_id = self._device.id - self._light_on = False + super().__init__(config_entry_id, device) + self._unique_id = device.id + self._light_on = device.lights == ON_STATE self._no_updates_until = dt_util.utcnow() - self._disp_disconnect = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring light %s (callback)", self.name) - self.async_schedule_update_ha_state(True) + if self._no_updates_until > dt_util.utcnow(): + return - @property - def should_poll(self): - """Update controlled via the hub.""" - return False + self._light_on = self._device.lights == ON_STATE + self.async_write_ha_state() @property def name(self): @@ -86,22 +70,12 @@ class RingLight(Light): """If the switch is currently on or off.""" return self._light_on - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state self._light_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state(True) + self.async_schedule_update_ha_state() def turn_on(self, **kwargs): """Turn the light on for 30 seconds.""" @@ -110,11 +84,3 @@ class RingLight(Light): def turn_off(self, **kwargs): """Turn the light off.""" self._set_light(OFF_STATE) - - async def async_update(self): - """Update current state of the light.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - - self._light_on = self._device.lights == ON_STATE diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index fe909636e83..2b921dddd2f 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,88 +1,20 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from . import ( - ATTRIBUTION, - DATA_HEALTH_DATA_TRACKER, - DATA_HISTORY, - DOMAIN, - SIGNAL_UPDATE_HEALTH_RING, - SIGNAL_UPDATE_RING, -) +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind, device_class -SENSOR_TYPES = { - "battery": [ - "Battery", - ["doorbots", "authorized_doorbots", "stickup_cams"], - "%", - None, - None, - "battery", - ], - "last_activity": [ - "Last Activity", - ["doorbots", "authorized_doorbots", "stickup_cams"], - None, - "history", - None, - "timestamp", - ], - "last_ding": [ - "Last Ding", - ["doorbots", "authorized_doorbots"], - None, - "history", - "ding", - "timestamp", - ], - "last_motion": [ - "Last Motion", - ["doorbots", "authorized_doorbots", "stickup_cams"], - None, - "history", - "motion", - "timestamp", - ], - "volume": [ - "Volume", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - None, - "bell-ring", - None, - None, - ], - "wifi_signal_category": [ - "WiFi Signal Category", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - None, - "wifi", - None, - None, - ], - "wifi_signal_strength": [ - "WiFi Signal Strength", - ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], - "dBm", - "wifi", - None, - "signal_strength", - ], -} - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + # Makes a ton of requests. We will make this a config entry option in the future wifi_enabled = False @@ -100,72 +32,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if device_type == "battery" and device.battery_life is None: continue - sensors.append(RingSensor(config_entry.entry_id, device, sensor_type)) + sensors.append( + SENSOR_TYPES[sensor_type][6]( + config_entry.entry_id, device, sensor_type + ) + ) - async_add_entities(sensors, True) + async_add_entities(sensors) -class RingSensor(Entity): +class RingSensor(RingEntityMixin, Entity): """A sensor implementation for Ring device.""" def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - self._config_entry_id = config_entry_id + super().__init__(config_entry_id, device) self._sensor_type = sensor_type - self._device = device self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) - self._kind = SENSOR_TYPES.get(self._sensor_type)[4] + self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._kind = SENSOR_TYPES.get(sensor_type)[4] self._name = "{0} {1}".format( - self._device.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._state = None - self._unique_id = f"{self._device.id}-{self._sensor_type}" - self._disp_disconnect = None - self._disp_disconnect_health = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): - return - - self._disp_disconnect_health = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_HEALTH_RING, self._update_callback - ) - await self.hass.data[DATA_HEALTH_DATA_TRACKER].track_device( - self._config_entry_id, self._device - ) - # Write the state, it was not available when doing initial update. - if self._sensor_type == "wifi_signal_category": - self._state = self._device.wifi_signal_category - - if self._sensor_type == "wifi_signal_strength": - self._state = self._device.wifi_signal_strength - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None - - if self._disp_disconnect_health: - self._disp_disconnect_health() - self._disp_disconnect_health = None - - if self._sensor_type not in ("wifi_signal_category", "wifi_signal_strength"): - return - - self.hass.data[DATA_HEALTH_DATA_TRACKER].untrack_device( - self._config_entry_id, self._device - ) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) + self._unique_id = f"{device.id}-{sensor_type}" @property def should_poll(self): @@ -180,7 +69,11 @@ class RingSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + if self._sensor_type == "volume": + return self._device.volume + + if self._sensor_type == "battery": + return self._device.battery_life @property def unique_id(self): @@ -192,37 +85,12 @@ class RingSensor(Entity): """Return sensor device class.""" return SENSOR_TYPES[self._sensor_type][5] - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = {} - - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - - if self._extra and self._sensor_type.startswith("last_"): - attrs["created_at"] = self._extra["created_at"] - attrs["answered"] = self._extra["answered"] - attrs["recording_status"] = self._extra["recording"]["status"] - attrs["category"] = self._extra["kind"] - - return attrs - @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery" and self._state is not None: + if self._sensor_type == "battery" and self._device.battery_life is not None: return icon_for_battery_level( - battery_level=int(self._state), charging=False + battery_level=self._device.battery_life, charging=False ) return self._icon @@ -231,34 +99,168 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - async def async_update(self): - """Get the latest data and updates the state.""" - _LOGGER.debug("Updating data from %s sensor", self._name) - if self._sensor_type == "volume": - self._state = self._device.volume +class HealthDataRingSensor(RingSensor): + """Ring sensor that relies on health data.""" - if self._sensor_type == "battery": - self._state = self._device.battery_life + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() - if self._sensor_type.startswith("last_"): - history = await self.hass.data[DATA_HISTORY].async_get_history( - self._config_entry_id, self._device - ) + await self.ring_objects["health_data"].async_track_device( + self._device, self._health_update_callback + ) - found = None - for entry in history: + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["health_data"].async_untrack_device( + self._device, self._health_update_callback + ) + + @callback + def _health_update_callback(self, _health_data): + """Call update method.""" + self.async_write_ha_state() + + @property + def state(self): + """Return the state of the sensor.""" + if self._sensor_type == "wifi_signal_category": + return self._device.wifi_signal_category + + if self._sensor_type == "wifi_signal_strength": + return self._device.wifi_signal_strength + + +class HistoryRingSensor(RingSensor): + """Ring sensor that relies on history data.""" + + _latest_event = None + + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) + + @callback + def _history_update_callback(self, history_data): + """Call update method.""" + if not history_data: + return + + found = None + if self._kind is None: + found = history_data[0] + else: + for entry in history_data: if entry["kind"] == self._kind: found = entry break - if found: - self._extra = found - created_at = found["created_at"] - self._state = created_at.isoformat() + if not found: + return - if self._sensor_type == "wifi_signal_category": - self._state = self._device.wifi_signal_category + self._latest_event = found + self.async_write_ha_state() - if self._sensor_type == "wifi_signal_strength": - self._state = self._device.wifi_signal_strength + @property + def state(self): + """Return the state of the sensor.""" + if self._latest_event is None: + return None + + return self._latest_event["created_at"].isoformat() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = super().device_state_attributes + + if self._latest_event: + attrs["created_at"] = self._latest_event["created_at"] + attrs["answered"] = self._latest_event["answered"] + attrs["recording_status"] = self._latest_event["recording"]["status"] + attrs["category"] = self._latest_event["kind"] + + return attrs + + +# Sensor types: Name, category, units, icon, kind, device_class, class +SENSOR_TYPES = { + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + RingSensor, + ], + "last_activity": [ + "Last Activity", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + None, + "timestamp", + HistoryRingSensor, + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", + HistoryRingSensor, + ], + "last_motion": [ + "Last Motion", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + "motion", + "timestamp", + HistoryRingSensor, + ], + "volume": [ + "Volume", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "bell-ring", + None, + None, + RingSensor, + ], + "wifi_signal_category": [ + "WiFi Signal Category", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "wifi", + None, + None, + HealthDataRingSensor, + ], + "wifi_signal_strength": [ + "WiFi Signal Strength", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + "dBm", + "wifi", + None, + "signal_strength", + HealthDataRingSensor, + ], +} diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 06f81732784..65eed83d98e 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DOMAIN, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -24,49 +24,24 @@ SKIP_UPDATES_DELAY = timedelta(seconds=5) async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - ring = hass.data[DOMAIN][config_entry.entry_id] - devices = ring.devices() + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] switches = [] for device in devices["stickup_cams"]: if device.has_capability("siren"): - switches.append(SirenSwitch(device)) + switches.append(SirenSwitch(config_entry.entry_id, device)) - async_add_entities(switches, True) + async_add_entities(switches) -class BaseRingSwitch(SwitchDevice): +class BaseRingSwitch(RingEntityMixin, SwitchDevice): """Represents a switch for controlling an aspect of a ring device.""" - def __init__(self, device, device_type): + def __init__(self, config_entry_id, device, device_type): """Initialize the switch.""" - self._device = device + super().__init__(config_entry_id, device) self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" - self._disp_disconnect = None - - async def async_added_to_hass(self): - """Register callbacks.""" - self._disp_disconnect = async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_RING, self._update_callback - ) - - async def async_will_remove_from_hass(self): - """Disconnect callbacks.""" - if self._disp_disconnect: - self._disp_disconnect() - self._disp_disconnect = None - - @callback - def _update_callback(self): - """Call update method.""" - _LOGGER.debug("Updating Ring switch %s (callback)", self.name) - self.async_schedule_update_ha_state(True) - - @property - def should_poll(self): - """Update controlled via the hub.""" - return False @property def name(self): @@ -78,25 +53,24 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def device_info(self): - """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.device_id)}, - "name": self._device.name, - "model": self._device.model, - "manufacturer": "Ring", - } - class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the switch for a device with a siren.""" - super().__init__(device, "siren") + super().__init__(config_entry_id, device, "siren") self._no_updates_until = dt_util.utcnow() - self._siren_on = False + self._siren_on = device.siren > 0 + + @callback + def _update_callback(self): + """Call update method.""" + if self._no_updates_until > dt_util.utcnow(): + return + + self._siren_on = self._device.siren > 0 + self.async_write_ha_state() def _set_switch(self, new_state): """Update switch state, and causes Home Assistant to correctly update.""" @@ -122,10 +96,3 @@ class SirenSwitch(BaseRingSwitch): def icon(self): """Return the icon.""" return SIREN_ICON - - async def async_update(self): - """Update state of the siren.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - self._siren_on = self._device.siren > 0 diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 8615138d56e..0b73c739503 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,4 +1,5 @@ """The tests for the Ring binary sensor platform.""" +from time import time from unittest.mock import patch from .common import setup_platform @@ -8,7 +9,15 @@ async def test_binary_sensor(hass, requests_mock): """Test the Ring binary sensors.""" with patch( "ring_doorbell.Ring.active_alerts", - return_value=[{"kind": "motion", "doorbot_id": 987654}], + return_value=[ + { + "kind": "motion", + "doorbot_id": 987654, + "state": "ringing", + "now": time(), + "expires_in": 180, + } + ], ): await setup_platform(hass, "binary_sensor") From bb42ff93f4aa5c4b8035b981c17cfd84f9181d2d Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 15 Jan 2020 12:15:31 -0500 Subject: [PATCH 114/393] Add support for vacuums to Alexa. (#30764) --- .../components/alexa/capabilities.py | 38 ++- homeassistant/components/alexa/entities.py | 32 +++ homeassistant/components/alexa/handlers.py | 57 ++++- tests/components/alexa/test_smart_home.py | 221 ++++++++++++++++++ 4 files changed, 345 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index d1b7917f263..b13cfd7d370 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,14 @@ """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 import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1291,6 +1298,15 @@ 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[vacuum.ATTR_FAN_SPEED_LIST] + speed = self.entity.attributes[vacuum.ATTR_FAN_SPEED] + speed_index = next( + (i for i, v in enumerate(speed_list) if v == speed), None + ) + return speed_index + return None def configuration(self): @@ -1367,6 +1383,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): diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6d1997589a4..2a326b2e367 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -20,6 +20,7 @@ from homeassistant.components import ( sensor, switch, timer, + vacuum, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -724,3 +725,34 @@ class TimerCapabilities(AlexaEntity): """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 510efe4b610..701b614aaa0 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -11,6 +11,7 @@ from homeassistant.components import ( light, media_player, timer, + vacuum, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -1138,6 +1139,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) @@ -1220,6 +1235,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) @@ -1412,8 +1445,18 @@ async def async_api_hold(hass, config, directive, context): 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, timer.SERVICE_PAUSE, data, blocking=False, context=context + entity.domain, service, data, blocking=False, context=context ) return directive.response() @@ -1425,8 +1468,18 @@ async def async_api_resume(hass, config, directive, context): 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, timer.SERVICE_START, data, blocking=False, context=context + entity.domain, service, data, blocking=False, context=context ) return directive.response() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index dd6faab8e96..ceedebcaec4 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 @@ -3134,3 +3135,223 @@ async def test_timer_resume(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"), ("high", "1"), ("medium", "0"), ("super_sucker", "99")], + "Alexa.RangeController", + "AdjustRangeValue", + "vacuum#test_2", + False, + "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, + ) From 53933000862d6166772c688c4e2fb4e609b2cfcb Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Wed, 15 Jan 2020 19:48:30 +0100 Subject: [PATCH 115/393] update to aiopylgtv 0.2.7 (#30797) --- homeassistant/components/webostv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index ddd7be6f3da..ff254e35159 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.6"], + "requirements": ["aiopylgtv==0.2.7"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 93bc45470ac..55c6f72b6b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.6 +aiopylgtv==0.2.7 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6942a3cfcd..4b508bc2833 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.6 +aiopylgtv==0.2.7 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From 1e2c3cacf949c913e006333b885fe0439340ebdb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Jan 2020 20:53:52 +0100 Subject: [PATCH 116/393] Mark hide_entity deprecated in automation integration (#30799) --- .../components/automation/__init__.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 6175646778f..9f51127cf99 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -93,18 +93,21 @@ _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( From 3899c6ae270b85bbcd7a15f9c483f1217350b646 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 15 Jan 2020 20:54:20 +0100 Subject: [PATCH 117/393] Bump librouteros to 3.0.0 (#30800) --- homeassistant/components/mikrotik/__init__.py | 36 +++++-------------- .../components/mikrotik/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 9b533288d86..c3f21658d21 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,8 +2,9 @@ import logging import ssl -import librouteros -from librouteros.login import login_plain, login_token +from librouteros import connect +from librouteros.exceptions import LibRouterosError +from librouteros.login import plain as login_plain, token as login_token import voluptuous as vol from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER @@ -82,11 +83,9 @@ def setup(hass, config): port = MTK_DEFAULT_API_PORT if login == MTK_LOGIN_PLAIN: - login_method = (login_plain,) - elif login == MTK_LOGIN_TOKEN: - login_method = (login_token,) + login_method = login_plain else: - login_method = (login_plain, login_token) + login_method = login_token try: api = MikrotikClient( @@ -94,11 +93,7 @@ def setup(hass, config): ) 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: + except LibRouterosError as api_error: _LOGGER.error("Mikrotik %s error %s", host, api_error) continue @@ -148,15 +143,9 @@ class MikrotikClient: kwargs["ssl_wrapper"] = self._ssl_wrapper try: - self._client = librouteros.connect( - self._host, self._user, self._password, **kwargs - ) + self._client = connect(self._host, self._user, self._password, **kwargs) self._connected = True - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: + except LibRouterosError as api_error: _LOGGER.error("Mikrotik %s: %s", self._host, api_error) self._client = None return False @@ -184,14 +173,7 @@ class MikrotikClient: 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: + except LibRouterosError as api_error: _LOGGER.error( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", self._host, diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 1f5bcf8163f..932df2edd29 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -2,7 +2,7 @@ "domain": "mikrotik", "name": "MikroTik", "documentation": "https://www.home-assistant.io/integrations/mikrotik", - "requirements": ["librouteros==2.3.0"], + "requirements": ["librouteros==3.0.0"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 55c6f72b6b6..d0bc384d14b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -775,7 +775,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 From b8feaba5cb87d89ab6b0c68dde79933dfa5304a4 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Wed, 15 Jan 2020 15:01:47 -0500 Subject: [PATCH 118/393] Update pynws to v0.10.1 (#30662) * update to pynws 0.10.1 * remove unneeded raw json files * move test helper module to const --- homeassistant/components/nws/manifest.json | 2 +- homeassistant/components/nws/weather.py | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nws/__init__.py | 1 + tests/components/nws/const.py | 114 +++ tests/components/nws/test_weather.py | 327 +++---- tests/fixtures/nws-weather-fore-null.json | 80 -- tests/fixtures/nws-weather-fore-valid.json | 80 -- tests/fixtures/nws-weather-obs-null.json | 161 ---- tests/fixtures/nws-weather-obs-valid.json | 161 ---- tests/fixtures/nws-weather-sta-valid.json | 996 --------------------- 12 files changed, 262 insertions(+), 1679 deletions(-) create mode 100644 tests/components/nws/__init__.py create mode 100644 tests/components/nws/const.py delete mode 100644 tests/fixtures/nws-weather-fore-null.json delete mode 100644 tests/fixtures/nws-weather-fore-valid.json delete mode 100644 tests/fixtures/nws-weather-obs-null.json delete mode 100644 tests/fixtures/nws-weather-obs-valid.json delete mode 100644 tests/fixtures/nws-weather-sta-valid.json 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/requirements_all.txt b/requirements_all.txt index d0bc384d14b..4e832647850 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b508bc2833..e7deeea91e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -481,7 +481,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 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/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 From 01520a32d33679136c59462698f6cd56b1375454 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 16 Jan 2020 00:31:45 +0000 Subject: [PATCH 119/393] [ci skip] Translation update --- .../components/vizio/.translations/da.json | 40 +++++++++++++++++++ .../components/vizio/.translations/en.json | 40 +++++++++++++++++++ .../components/vizio/.translations/ru.json | 37 +++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 homeassistant/components/vizio/.translations/da.json create mode 100644 homeassistant/components/vizio/.translations/en.json create mode 100644 homeassistant/components/vizio/.translations/ru.json diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json new file mode 100644 index 00000000000..fecc07bd9a1 --- /dev/null +++ b/homeassistant/components/vizio/.translations/da.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurationsproces for Vizio-komponenten er allerede i gang.", + "already_setup": "Denne post er allerede blevet konfigureret.", + "host_exists": "Vizio-komponent med v\u00e6rt er allerede konfigureret.", + "name_exists": "Vizio-komponent med navn er allerede konfigureret.", + "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": "V\u00e6rt er allerede konfigureret.", + "name_exists": "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": { + "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/en.json b/homeassistant/components/vizio/.translations/en.json new file mode 100644 index 00000000000..3be97349890 --- /dev/null +++ b/homeassistant/components/vizio/.translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_in_progress": "Config flow for vizio component already in progress.", + "already_setup": "This entry has already been setup.", + "host_exists": "Vizio component with host already configured.", + "name_exists": "Vizio component with name already configured.", + "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": "Host already configured.", + "name_exists": "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": { + "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/ru.json b/homeassistant/components/vizio/.translations/ru.json new file mode 100644 index 00000000000..24e8411b438 --- /dev/null +++ b/homeassistant/components/vizio/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "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.", + "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." + }, + "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": { + "volume_step": "\u0428\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438" + } + } + } + } +} \ No newline at end of file From 1566d963a897099ab304439ae542efb09507fc0e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Thu, 16 Jan 2020 02:16:26 +0100 Subject: [PATCH 120/393] Fixed pyyaml requirement from 5.2.0 to 5.2 (#30808) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2cc4e1c65d6..11ae7df4361 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2.0 +pyyaml==5.2 requests==2.22.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.12 diff --git a/requirements_all.txt b/requirements_all.txt index 4e832647850..31ab60d586c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,7 +12,7 @@ cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2.0 +pyyaml==5.2 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 diff --git a/setup.py b/setup.py index cf84577b558..35594c21507 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.2.0", + "pyyaml==5.2", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", From 6f24fe3970c6cf2ef95772b362dca6ce94935dd6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Jan 2020 01:19:38 -0800 Subject: [PATCH 121/393] Handle no host info in ignored config entries (#30822) --- homeassistant/components/hue/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index cbcb21db7d0..238ea06961d 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -69,7 +69,7 @@ async def async_setup(hass, config): bridges = conf[CONF_BRIDGES] configured_hosts = set( - entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) + entry.data.get("host") for entry in hass.config_entries.async_entries(DOMAIN) ) for bridge_conf in bridges: From 9d5a39191650500761c6da55091b81602be16da9 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 16 Jan 2020 10:48:47 +0100 Subject: [PATCH 122/393] Fix setup error of Mikrotik (#30810) --- homeassistant/components/mikrotik/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index c3f21658d21..8c21b2e1c35 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -156,7 +156,7 @@ class MikrotikClient: def get_hostname(self): """Return device host name.""" - data = self.command(MIKROTIK_SERVICES[IDENTITY]) + data = list(self.command(MIKROTIK_SERVICES[IDENTITY])) return data[0][NAME] if data else None def connected(self): From bef86009729ba9ad7fdda6cb712838c7282494f4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jan 2020 11:20:58 +0100 Subject: [PATCH 123/393] Fix mpd time issue (#30825) * Fix mpd time issue * Update homeassistant/components/mpd/media_player.py Co-Authored-By: Franck Nijhof --- homeassistant/components/mpd/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 6460becbb3e..4b6d63b4240 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -133,8 +133,8 @@ class MpdDevice(MediaPlayerDevice): self._status = self._client.status() self._currentsong = self._client.currentsong() - position = self._status["time"] - if self._media_position != position: + position = self._status.get("time") + if position is not None and self._media_position != position: self._media_position_updated_at = dt_util.utcnow() self._media_position = position From d1da653e623ac8e94ec9a957ca9465e465dbbdb2 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 16 Jan 2020 12:30:55 +0100 Subject: [PATCH 124/393] Fix play_media in webostv (#30828) --- homeassistant/components/webostv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 4652d6385c1..b7c8a416870 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -351,7 +351,7 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): partial_match_channel_id = None perfect_match_channel_id = None - for channel in self._client.get_channels(): + for channel in await self._client.get_channels(): if media_id == channel["channelNumber"]: perfect_match_channel_id = channel["channelId"] continue From d9d5e06baf82ccadfde1ae6462a69799d39d524d Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 16 Jan 2020 09:16:57 -0500 Subject: [PATCH 125/393] Use collection helpers for input_datetime component (#30815) * Refactor input_datetime. Keep the state as datetime, but format accordingly to has_time and has_date. * Use config dict for input_datetime. * Add tests. * Lint Co-authored-by: Paulus Schoutsen --- .../components/input_datetime/__init__.py | 223 +++++++++++++----- tests/components/input_datetime/test_init.py | 219 ++++++++++++++++- 2 files changed, 377 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index da684e03ddc..fdb80591bbe 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, entity_registry 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,57 @@ 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() - if conf is None: + 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) + + 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 - new_entities = await _async_process_config(conf) - if new_entities: - await component.async_add_entities(new_entities) + + 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) + + 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: + 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 +182,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 +264,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 +331,27 @@ class InputDatetime(RestoreEntity): return attrs + @property + def unique_id(self) -> typing.Optional[str]: + """Return unique id of the entity.""" + return self._config[CONF_ID] + 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/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index fdf6e9296be..b2f92a333ea 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.""" @@ -318,11 +364,7 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert await async_setup_component( hass, DOMAIN, - { - DOMAIN: { - "dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}, - } - }, + {DOMAIN: {"dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}}}, ) assert count_start + 1 == len(hass.states.async_entity_ids()) @@ -365,8 +407,169 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") - 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 str(DEFAULT_TIME) == state_1.state assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state + + +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()) From e1205409f3b825bcd2506a12692fd438a9537eb2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Jan 2020 08:31:12 -0800 Subject: [PATCH 126/393] Handle location API being exhausted (#30798) --- homeassistant/util/location.py | 4 ++++ tests/util/test_location.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) 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/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")) From 454466574949fd700c667713b100fc0c3f71e830 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Jan 2020 18:25:39 +0100 Subject: [PATCH 127/393] Deprecate states UI options in group integration (#30831) --- homeassistant/components/group/__init__.py | 56 ++++++++++++++-------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 92d811c06fb..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( @@ -317,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, + } + ), ), ) @@ -343,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 From 235ab64066b1681ce51a4c26d903f6849c4510bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Jan 2020 18:25:49 +0100 Subject: [PATCH 128/393] Deprecate weblink integration (#30834) --- homeassistant/components/weblink/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) 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): From 04b7d9e848819897a15145590164081653f62db9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 16 Jan 2020 18:27:43 +0100 Subject: [PATCH 129/393] Fix iCloud when no family members (issue #30829) (#30836) --- homeassistant/components/icloud/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index d4074db021e..f36ad607634 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -297,10 +297,11 @@ class IcloudAccount: self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" self._family_members_fullname = {} - for prs_id, member in user_info["membersInfo"].items(): - self._family_members_fullname[ - prs_id - ] = f"{member['firstName']} {member['lastName']}" + 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() From 8861c800688444bbb50523bee7092e647f59e223 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Jan 2020 19:31:00 +0100 Subject: [PATCH 130/393] Deprecate hide_if_away from device_tracker (#30833) --- .../components/device_tracker/__init__.py | 13 +++++--- .../components/device_tracker/legacy.py | 33 ++++++++++--------- 2 files changed, 26 insertions(+), 20 deletions(-) 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/legacy.py b/homeassistant/components/device_tracker/legacy.py index b7d529f18ac..04ecad3b13d 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -528,21 +528,24 @@ async def async_load_config( This method is a coroutine. """ - dev_schema = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), - vol.Optional("track", default=False): cv.boolean, - vol.Optional(CONF_MAC, default=None): vol.Any( - None, vol.All(cv.string, vol.Upper) - ), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - vol.Optional("gravatar", default=None): vol.Any(None, cv.string), - vol.Optional("picture", default=None): vol.Any(None, cv.string), - vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta - ), - } + dev_schema = vol.All( + cv.deprecated(CONF_AWAY_HIDE, invalidation_version="0.107.0"), + vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), + vol.Optional("track", default=False): cv.boolean, + vol.Optional(CONF_MAC, default=None): vol.Any( + None, vol.All(cv.string, vol.Upper) + ), + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + vol.Optional("gravatar", default=None): vol.Any(None, cv.string), + vol.Optional("picture", default=None): vol.Any(None, cv.string), + vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( + cv.time_period, cv.positive_timedelta + ), + } + ), ) result = [] try: From 2a65da5db2392bb9e0230413f43017be3914d3a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 16 Jan 2020 19:31:18 +0100 Subject: [PATCH 131/393] Deprecate history_graph integration (#30835) --- homeassistant/components/history_graph/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) 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 = [] From 7da84dca76650581acbc3fbb3c9d98e517f0452a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 16 Jan 2020 11:28:35 -0800 Subject: [PATCH 132/393] Reinstate and deprecate filename option for hue config (#30846) --- homeassistant/components/hue/__init__.py | 6 +++++- tests/components/hue/test_init.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 238ea06961d..b294a811c61 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -44,7 +44,11 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.Schema( { vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, [BRIDGE_CONFIG_SCHEMA] + cv.ensure_list, + [ + cv.deprecated("filename", invalidation_version="0.106.0"), + vol.All(BRIDGE_CONFIG_SCHEMA), + ], ) } ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index b48d66990e8..2e147fd097b 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -33,6 +33,7 @@ async def test_setup_defined_hosts_known_auth(hass): hue.CONF_HOST: "0.0.0.0", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, + "filename": "bla", } } }, @@ -49,6 +50,7 @@ async def test_setup_defined_hosts_known_auth(hass): hue.CONF_HOST: "0.0.0.0", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, + "filename": "bla", } } From 9bfcd04a4f16ef25f1f2c2b0baae81ea2e79ac34 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 16 Jan 2020 12:37:29 -0700 Subject: [PATCH 133/393] Fix sensor type creation with multiple Ambient weather stations (#30850) --- .../components/ambient_station/__init__.py | 16 ++++++++-------- .../components/ambient_station/binary_sensor.py | 10 ++++++++-- .../components/ambient_station/const.py | 1 + .../components/ambient_station/sensor.py | 10 ++++++++-- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 58389dd1831..c61e15dfeb5 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.event import async_call_later from .config_flow import configured_instances from .const import ( ATTR_LAST_DATA, + ATTR_MONITORED_CONDITIONS, CONF_APP_KEY, DATA_CLIENT, DOMAIN, @@ -341,7 +342,6 @@ class AmbientStation: self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client - self.monitored_conditions = [] self.stations = {} async def _attempt_connect(self): @@ -398,19 +398,19 @@ class AmbientStation: _LOGGER.debug("New station subscription: %s", data) - self.monitored_conditions = [ + # Only create entities based on the data coming through the socket. + # If the user is monitoring brightness (in W/m^2), make sure we also + # add a calculated sensor for the same data measured in lx: + monitored_conditions = [ k for k in station["lastData"] if k in SENSOR_TYPES ] - - # If the user is monitoring brightness (in W/m^2), - # make sure we also add a calculated sensor for the - # same data measured in lx: - if TYPE_SOLARRADIATION in self.monitored_conditions: - self.monitored_conditions.append(TYPE_SOLARRADIATION_LX) + if TYPE_SOLARRADIATION in monitored_conditions: + monitored_conditions.append(TYPE_SOLARRADIATION_LX) self.stations[station["macAddress"]] = { ATTR_LAST_DATA: station["lastData"], ATTR_LOCATION: station.get("info", {}).get("location"), + ATTR_MONITORED_CONDITIONS: monitored_conditions, ATTR_NAME: station.get("info", {}).get( "name", station["macAddress"] ), diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 3f02eb9f1e8..1ed6dbd0db4 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -19,7 +19,13 @@ from . import ( TYPE_BATTOUT, AmbientWeatherEntity, ) -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR +from .const import ( + ATTR_LAST_DATA, + ATTR_MONITORED_CONDITIONS, + DATA_CLIENT, + DOMAIN, + TYPE_BINARY_SENSOR, +) _LOGGER = logging.getLogger(__name__) @@ -35,7 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): binary_sensor_list = [] for mac_address, station in ambient.stations.items(): - for condition in ambient.monitored_conditions: + for condition in station[ATTR_MONITORED_CONDITIONS]: name, _, kind, device_class = SENSOR_TYPES[condition] if kind == TYPE_BINARY_SENSOR: binary_sensor_list.append( diff --git a/homeassistant/components/ambient_station/const.py b/homeassistant/components/ambient_station/const.py index b2df34f2f28..21a6e514b30 100644 --- a/homeassistant/components/ambient_station/const.py +++ b/homeassistant/components/ambient_station/const.py @@ -2,6 +2,7 @@ DOMAIN = "ambient_station" ATTR_LAST_DATA = "last_data" +ATTR_MONITORED_CONDITIONS = "monitored_conditions" CONF_APP_KEY = "app_key" diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 56425221e0d..0120799d6f2 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -9,7 +9,13 @@ from . import ( TYPE_SOLARRADIATION_LX, AmbientWeatherEntity, ) -from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR +from .const import ( + ATTR_LAST_DATA, + ATTR_MONITORED_CONDITIONS, + DATA_CLIENT, + DOMAIN, + TYPE_SENSOR, +) _LOGGER = logging.getLogger(__name__) @@ -25,7 +31,7 @@ async def async_setup_entry(hass, entry, async_add_entities): sensor_list = [] for mac_address, station in ambient.stations.items(): - for condition in ambient.monitored_conditions: + for condition in station[ATTR_MONITORED_CONDITIONS]: name, unit, kind, device_class = SENSOR_TYPES[condition] if kind == TYPE_SENSOR: sensor_list.append( From a93088dafb32a20a894bf2d2de021bc68ebad94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per-=C3=98yvind=20Bruun?= Date: Thu, 16 Jan 2020 20:37:53 +0100 Subject: [PATCH 134/393] Fix for issue #29822 (#30849) --- homeassistant/components/msteams/notify.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py index c986f1d2363..eafac85c8c2 100644 --- a/homeassistant/components/msteams/notify.py +++ b/homeassistant/components/msteams/notify.py @@ -39,16 +39,18 @@ class MSTeamsNotificationService(BaseNotificationService): def __init__(self, webhook_url): """Initialize the service.""" self._webhook_url = webhook_url - self.teams_message = pymsteams.connectorcard(self._webhook_url) def send_message(self, message=None, **kwargs): """Send a message to the webhook.""" + + teams_message = pymsteams.connectorcard(self._webhook_url) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) - self.teams_message.title(title) + teams_message.title(title) - self.teams_message.text(message) + teams_message.text(message) if data is not None: file_url = data.get(ATTR_FILE_URL) @@ -60,8 +62,8 @@ class MSTeamsNotificationService(BaseNotificationService): message_section = pymsteams.cardsection() message_section.addImage(file_url) - self.teams_message.addSection(message_section) + teams_message.addSection(message_section) try: - self.teams_message.send() + teams_message.send() except RuntimeError as err: _LOGGER.error("Could not send notification. Error: %s", err) From 6678f8387f61daa0f6991362faaa10426a476792 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 16 Jan 2020 16:26:10 -0800 Subject: [PATCH 135/393] Remove unused import (#30858) --- homeassistant/components/ring/__init__.py | 2 +- homeassistant/components/ring/binary_sensor.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 7b4fbb15b30..57148cc15af 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass, entry): hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1) ), "dings_data": GlobalDataUpdater( - hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10) + hass, entry.entry_id, ring, "update_dings", timedelta(seconds=5) ), "history_data": DeviceDataUpdater( hass, 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"], From 24a381456a4f6ec5bcdd75645ec83e9f09edc16a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 17 Jan 2020 00:31:45 +0000 Subject: [PATCH 136/393] [ci skip] Translation update --- .../components/axis/.translations/fr.json | 3 +- .../components/brother/.translations/fr.json | 24 +++++++++++ .../components/deconz/.translations/fr.json | 7 +++- .../components/elgato/.translations/ru.json | 2 +- .../components/gios/.translations/fr.json | 10 ++++- .../components/local_ip/.translations/ru.json | 2 +- .../components/netatmo/.translations/fr.json | 18 +++++++++ .../components/ring/.translations/fr.json | 27 +++++++++++++ .../samsungtv/.translations/fr.json | 26 ++++++++++++ .../components/sentry/.translations/fr.json | 17 ++++++++ .../components/vizio/.translations/ca.json | 20 ++++++++++ .../components/vizio/.translations/de.json | 40 +++++++++++++++++++ .../components/vizio/.translations/fr.json | 29 ++++++++++++++ .../components/vizio/.translations/it.json | 8 ++++ .../components/vizio/.translations/no.json | 40 +++++++++++++++++++ 15 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/brother/.translations/fr.json create mode 100644 homeassistant/components/netatmo/.translations/fr.json create mode 100644 homeassistant/components/ring/.translations/fr.json create mode 100644 homeassistant/components/samsungtv/.translations/fr.json create mode 100644 homeassistant/components/sentry/.translations/fr.json create mode 100644 homeassistant/components/vizio/.translations/ca.json create mode 100644 homeassistant/components/vizio/.translations/de.json create mode 100644 homeassistant/components/vizio/.translations/fr.json create mode 100644 homeassistant/components/vizio/.translations/it.json create mode 100644 homeassistant/components/vizio/.translations/no.json 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/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json new file mode 100644 index 00000000000..db3c7f48ce7 --- /dev/null +++ b/homeassistant/components/brother/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "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" + } + }, + "title": "Imprimante Brother" + } +} \ No newline at end of file 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/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json index 2b5fb72c507..fd2f6579407 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -22,6 +22,6 @@ "title": "\u041d\u0430\u0439\u0434\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/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/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/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/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/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/sentry/.translations/fr.json b/homeassistant/components/sentry/.translations/fr.json new file mode 100644 index 00000000000..d5537f8632d --- /dev/null +++ b/homeassistant/components/sentry/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "bad_dsn": "DSN invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json new file mode 100644 index 00000000000..ab8d3c017ca --- /dev/null +++ b/homeassistant/components/vizio/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "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" + } +} \ 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..a0eba0a29e1 --- /dev/null +++ b/homeassistant/components/vizio/.translations/de.json @@ -0,0 +1,40 @@ +{ + "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_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": { + "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/fr.json b/homeassistant/components/vizio/.translations/fr.json new file mode 100644 index 00000000000..8e01a7aad96 --- /dev/null +++ b/homeassistant/components/vizio/.translations/fr.json @@ -0,0 +1,29 @@ +{ + "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.", + "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_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", + "name": "Nom" + } + } + } + }, + "options": { + "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..bde1c2d37ff --- /dev/null +++ b/homeassistant/components/vizio/.translations/it.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "host_exists": "Host gi\u00e0 configurato.", + "name_exists": "Nome gi\u00e0 configurato." + } + } +} \ 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..30e6fc9af6f --- /dev/null +++ b/homeassistant/components/vizio/.translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurasjons flyt for Vizio komponent er allerede i gang.", + "already_setup": "Denne oppf\u00f8ringen er allerede konfigurert.", + "host_exists": "Vizio komponent med vert allerede konfigurert.", + "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", + "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": "Vert er allerede konfigurert.", + "name_exists": "Navnet er 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": { + "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" + }, + "title": "Oppdater Vizo SmartCast alternativer" + } + }, + "title": "Oppdater Vizo SmartCast alternativer" + } +} \ No newline at end of file From d63ea198f2a9d65e2be4160f33688534744605df Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 17 Jan 2020 02:48:59 -0500 Subject: [PATCH 137/393] Bump pyvizio version to 0.1.1 (#30867) * bump pyvizio version to resolve setup issue * update requirements*.txt --- homeassistant/components/vizio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 0a44a638d44..b5ea057a33d 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.0.20"], + "requirements": ["pyvizio==0.1.1"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true diff --git a/requirements_all.txt b/requirements_all.txt index 31ab60d586c..e62de58b1f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.0.20 +pyvizio==0.1.1 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7deeea91e3..f19a50bee53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.0.20 +pyvizio==0.1.1 # homeassistant.components.html5 pywebpush==1.9.2 From da368f0cb8783ba2c2df45d8635e68b52fb6b5e1 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 17 Jan 2020 17:08:30 +0100 Subject: [PATCH 138/393] Update pyatmo to 3.2.2 and add available attribute (#30882) * Update pyatmo to 3.2.1 * Update pyatmo to 3.2.2 * Remove accidentally added requirements * Add availability property --- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 75824a9ebda..14ec2e61b9c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==3.2.0" + "pyatmo==3.2.2" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 64a203c47a2..82c3748d19b 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -427,6 +427,11 @@ 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() diff --git a/requirements_all.txt b/requirements_all.txt index e62de58b1f9..92bcb276f6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1137,7 +1137,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.2.0 +pyatmo==3.2.2 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f19a50bee53..cfc0cd072e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.2.0 +pyatmo==3.2.2 # homeassistant.components.blackbird pyblackbird==0.5 From 8a78b65f0d99f1383cfdbcea13e95fbe7e3f2288 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 17 Jan 2020 11:41:46 -0500 Subject: [PATCH 139/393] Extract collection entity registry cleaner into a helper (#30844) * Extract entity_registry_keeper into separate helper * Refactor input_*, timer and person to use new helper. * Make Mypy happy. * Better name. --- .../components/input_boolean/__init__.py | 16 +++--------- .../components/input_datetime/__init__.py | 16 +++--------- .../components/input_number/__init__.py | 16 +++--------- .../components/input_select/__init__.py | 16 +++--------- .../components/input_text/__init__.py | 16 +++--------- homeassistant/components/person/__init__.py | 13 ++-------- homeassistant/components/timer/__init__.py | 16 +++--------- homeassistant/helpers/collection.py | 26 +++++++++++++++++++ tests/components/input_datetime/test_init.py | 21 +++++++++++++-- 9 files changed, 65 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index c805af0a758..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 @@ -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.""" diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index fdb80591bbe..03b468313f8 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) 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_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -117,18 +117,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: """Reload yaml entities.""" diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 4205389d9b2..f78fc485e40 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) 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_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -142,18 +142,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: """Reload yaml entities.""" diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 8d86904e5f9..26a07e600f3 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) 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_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -116,18 +116,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: """Reload yaml entities.""" diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index c439d177224..bdb3e8a4bc9 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) 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_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -144,18 +144,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: """Reload yaml entities.""" 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/timer/__init__.py b/homeassistant/components/timer/__init__.py index 1216bc8a239..abf3a6ab0f7 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) 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_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time @@ -119,18 +119,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: """Reload yaml entities.""" diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index dd0edbd09b9..80790a6d831 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 @@ -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/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index b2f92a333ea..67a23a61d8b 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -360,22 +360,33 @@ 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, DOMAIN, - {DOMAIN: {"dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}}}, + { + 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", @@ -406,12 +417,18 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): state_1 = hass.states.get("input_datetime.dt1") state_2 = hass.states.get("input_datetime.dt2") + state_3 = hass.states.get("input_datetime.dt3") assert state_1 is not None assert state_2 is not None + 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.""" From d913d35fc3e2098bf72ef717c59cd3ebcfdf2648 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 17 Jan 2020 18:37:32 +0100 Subject: [PATCH 140/393] Fix missing switch groups of HomematicIP Cloud (#30903) --- homeassistant/components/homematicip_cloud/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 8f3f6a3a177..6fdb0b8c95c 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -13,7 +13,7 @@ from homematicip.aio.device import ( AsyncPrintedCircuitBoardSwitch2, AsyncPrintedCircuitBoardSwitchBattery, ) -from homematicip.aio.group import AsyncSwitchingGroup +from homematicip.aio.group import AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry @@ -67,7 +67,7 @@ async def async_setup_entry( entities.append(HomematicipMultiSwitch(hap, device, channel)) for group in hap.home.groups: - if isinstance(group, AsyncSwitchingGroup): + if isinstance(group, (AsyncExtendedLinkedSwitchingGroup, AsyncSwitchingGroup)): entities.append(HomematicipGroupSwitch(hap, group)) if entities: From 31996120dd19541499d868f8f97c1ecb0a7dd8aa Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 17 Jan 2020 13:06:10 -0500 Subject: [PATCH 141/393] add multistate back (#30889) --- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/sensor.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 708a123d029..bf778812453 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -58,6 +58,7 @@ CHANNEL_HUMIDITY = "humidity" CHANNEL_IAS_WD = "ias_wd" CHANNEL_ILLUMINANCE = "illuminance" CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_MULTISTATE_INPUT = "multistate_input" CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3b73a9793c9..ce02bf11d9d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -26,6 +26,7 @@ from .core.const import ( CHANNEL_ELECTRICAL_MEASUREMENT, CHANNEL_HUMIDITY, CHANNEL_ILLUMINANCE, + CHANNEL_MULTISTATE_INPUT, CHANNEL_POWER_CONFIGURATION, CHANNEL_PRESSURE, CHANNEL_SMARTENERGY_METERING, @@ -227,6 +228,18 @@ class ElectricalMeasurement(Sensor): return round(value * self._channel.multiplier / self._channel.divisor) +@STRICT_MATCH(channel_names=CHANNEL_MULTISTATE_INPUT) +class Text(Sensor): + """Sensor that displays string values.""" + + _device_class = None + _unit = None + + def formatter(self, value) -> str: + """Return string value.""" + return value + + @STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) @STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) class Humidity(Sensor): From 847196dbe8f92a8f092481b2b64249550db463f4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 17 Jan 2020 13:43:41 -0700 Subject: [PATCH 142/393] Remove option to configure SimpliSafe scan interval (#30909) * Enforce minimum scan interval for SimpliSafe * Remove scan interval all together * Fix tests --- .../components/simplisafe/__init__.py | 13 ++----------- .../components/simplisafe/config_flow.py | 18 +++--------------- .../components/simplisafe/test_config_flow.py | 11 +---------- 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 52517b7d25f..d5538e6a372 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,6 +1,5 @@ """Support for SimpliSafe alarm systems.""" import asyncio -from datetime import timedelta import logging from simplipy import API @@ -9,13 +8,7 @@ from simplipy.system.v3 import VOLUME_HIGH, VOLUME_LOW, VOLUME_MEDIUM, VOLUME_OF 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, -) +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 ( @@ -100,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, } ) @@ -160,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], }, ) ) @@ -202,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: 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/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, } From 196bf2f3a0d6a587ae0cea585ea708dcc8b14b92 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Fri, 17 Jan 2020 21:51:32 +0100 Subject: [PATCH 143/393] remove PostNL component as it is no longer working (#30902) --- .coveragerc | 1 - homeassistant/components/postnl/__init__.py | 1 - homeassistant/components/postnl/manifest.json | 8 -- homeassistant/components/postnl/sensor.py | 100 ------------------ requirements_all.txt | 3 - 5 files changed, 113 deletions(-) delete mode 100644 homeassistant/components/postnl/__init__.py delete mode 100644 homeassistant/components/postnl/manifest.json delete mode 100644 homeassistant/components/postnl/sensor.py diff --git a/.coveragerc b/.coveragerc index 3afc6b4fd3e..87bd0522e81 100644 --- a/.coveragerc +++ b/.coveragerc @@ -539,7 +539,6 @@ omit = 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/* 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/requirements_all.txt b/requirements_all.txt index 92bcb276f6b..c802caca13e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,9 +1021,6 @@ pmsensor==0.4 # homeassistant.components.pocketcasts pocketcasts==0.1 -# homeassistant.components.postnl -postnl_api==1.2.2 - # homeassistant.components.reddit praw==6.5.0 From a5a69bdf71f2f22d1a5f302eb82541fa56c0fccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 17 Jan 2020 22:54:28 +0100 Subject: [PATCH 144/393] Adds icon endpoint to NO_AUTH (#30910) * Adds icon endpoint to NO_AUTH * Add test for no_auth icon get * Blackout --- homeassistant/components/hassio/http.py | 4 +++- tests/components/hassio/test_http.py | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) 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/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") From 8cacef47f34f471810a11316e2903a285b274bf2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jan 2020 14:54:32 -0800 Subject: [PATCH 145/393] camera endpoint likes to timeout, catch it. (#30919) --- homeassistant/components/ring/camera.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 07d87c85714..1526a915482 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -6,6 +6,7 @@ import logging from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import requests from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG @@ -146,9 +147,15 @@ class RingCam(RingEntityMixin, Camera): ): return - video_url = await self.hass.async_add_executor_job( - self._device.recording_url, self._last_event["id"] - ) + try: + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, self._last_event["id"] + ) + except requests.Timeout: + _LOGGER.warning( + "Time out fetching recording url for camera %s", self.entity_id + ) + video_url = None if video_url: self._last_video_id = self._last_event["id"] From e33698b17d3b36734ec06651063d8026a1fdf55b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 17 Jan 2020 23:54:53 +0100 Subject: [PATCH 146/393] Updated frontend to 20200108.2 (#30921) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 26ee4ce52b5..159ee68a53e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200108.0"], + "requirements": [ + "home-assistant-frontend==20200108.2" + ], "dependencies": [ "api", "auth", @@ -13,6 +15,8 @@ "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/package_constraints.txt b/homeassistant/package_constraints.txt index 11ae7df4361..b6e36d330f1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 importlib-metadata==1.3.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index c802caca13e..d726862f290 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfc0cd072e6..c87df849389 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -244,7 +244,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.0 +home-assistant-frontend==20200108.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 From 1d41cf96ca2a7c8216319738787b1fa027f99241 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 17 Jan 2020 18:04:46 -0500 Subject: [PATCH 147/393] Fix Alexa semantics for covers with tilt support. (#30911) * Fix Alexa semantics for covers with tilt support. * Clarify wording. * Korrect grammar. --- .../components/alexa/capabilities.py | 94 +++++++--- homeassistant/components/alexa/entities.py | 4 +- homeassistant/components/alexa/handlers.py | 8 +- homeassistant/components/alexa/resources.py | 15 +- tests/components/alexa/test_smart_home.py | 176 ++++++++++++------ 5 files changed, 207 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b13cfd7d370..6a910b3bb8f 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1091,6 +1091,15 @@ class AlexaSecurityPanelController(AlexaCapability): class AlexaModeController(AlexaCapability): """Implements Alexa.ModeController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html """ @@ -1201,28 +1210,38 @@ class AlexaModeController(AlexaCapability): def semantics(self): """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER], + lower_labels, "SetMode", {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, ) self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE], + raise_labels, "SetMode", {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, ) - self._semantics.add_states_to_value( - [AlexaSemantics.STATES_CLOSED], - f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", - ) - self._semantics.add_states_to_value( - [AlexaSemantics.STATES_OPEN], - f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", - ) + return self._semantics.serialize_semantics() return None @@ -1231,6 +1250,15 @@ class AlexaModeController(AlexaCapability): class AlexaRangeController(AlexaCapability): """Implements Alexa.RangeController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html """ @@ -1290,8 +1318,8 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) - # Cover Tilt Position - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) # Input Number Value @@ -1350,10 +1378,10 @@ class AlexaRangeController(AlexaCapability): ) return self._resource.serialize_capability_resources() - # Cover Tilt Position Resources - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt Resources + if self.instance == f"{cover.DOMAIN}.tilt": self._resource = AlexaPresetResource( - ["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING], + ["Tilt", "Angle", AlexaGlobalCatalog.SETTING_DIRECTION], min_value=0, max_value=100, precision=1, @@ -1407,24 +1435,35 @@ class AlexaRangeController(AlexaCapability): def semantics(self): """Build and return semantics object.""" + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + lower_labels = [AlexaSemantics.ACTION_LOWER] + raise_labels = [AlexaSemantics.ACTION_RAISE] self._semantics = AlexaSemantics() + + # Add open/close semantics if tilt is not supported. + if not supported & cover.SUPPORT_SET_TILT_POSITION: + lower_labels.append(AlexaSemantics.ACTION_CLOSE) + raise_labels.append(AlexaSemantics.ACTION_OPEN) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], value=0 + ) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0} + lower_labels, "SetRangeValue", {"rangeValue": 0} ) self._semantics.add_action_to_directive( - [AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100} - ) - self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) - self._semantics.add_states_to_range( - [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + raise_labels, "SetRangeValue", {"rangeValue": 100} ) return self._semantics.serialize_semantics() - # Cover Tilt Position - if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + if self.instance == f"{cover.DOMAIN}.tilt": self._semantics = AlexaSemantics() self._semantics.add_action_to_directive( [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} @@ -1444,6 +1483,15 @@ class AlexaRangeController(AlexaCapability): class AlexaToggleController(AlexaCapability): """Implements Alexa.ToggleController. + The instance property must be unique across ModeController, RangeController, ToggleController within the same device. + The instance property should be a concatenated string of device domain period and single word. + e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property strings within the same device. + e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + + An instance property string value may be reused for different devices. + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html """ diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 2a326b2e367..6b831986192 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -410,9 +410,7 @@ class CoverCapabilities(AlexaEntity): self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" ) if supported & cover.SUPPORT_SET_TILT_POSITION: - yield AlexaRangeController( - self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}" - ) + yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt") 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 701b614aaa0..fc49266f812 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1120,8 +1120,8 @@ async def async_api_set_range(hass, config, directive, context): service = cover.SERVICE_SET_COVER_POSITION data[cover.ATTR_POSITION] = range_value - # Cover Tilt Position - elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT @@ -1215,8 +1215,8 @@ async def async_api_adjust_range(hass, config, directive, context): 100, max(0, range_delta + current) ) - # Cover Tilt Position - elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + # Cover Tilt + elif instance == f"{cover.DOMAIN}.tilt": range_delta = int(range_delta) service = SERVICE_SET_COVER_TILT_POSITION current = entity.attributes.get(cover.ATTR_TILT_POSITION) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 09927321c36..d2580f3bfea 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -190,7 +190,12 @@ class AlexaGlobalCatalog: class AlexaCapabilityResource: - """Base class for Alexa capabilityResources, ModeResources, and presetResources objects. + """Base class for Alexa capabilityResources, modeResources, and presetResources objects. + + Resources objects labels must be unique across all modeResources and presetResources within the same device. + To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array. + You cannot use any words from the following list as friendly names: + https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources """ @@ -312,6 +317,14 @@ class AlexaSemantics: Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has + multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail. + + You can support semantics actionMappings on different controllers for the same device, however each controller must + support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController, + but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported + for one interface on the same device. + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object """ diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ceedebcaec4..a29df07bc1f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -133,7 +133,7 @@ def get_capability(capabilities, capability_name, instance=None): for capability in capabilities: if instance and capability["instance"] == instance: return capability - if capability["interface"] == capability_name: + if not instance and capability["interface"] == capability_name: return capability return None @@ -1484,6 +1484,36 @@ async def test_cover_position_range(hass): assert supported_range["maximumValue"] == 100 assert supported_range["precision"] == 1 + # Assert for Position Semantics + position_semantics = range_capability["semantics"] + assert position_semantics is not None + + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings + + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in position_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in position_state_mappings + call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", @@ -2511,16 +2541,37 @@ async def test_cover_position_mode(hass): }, } in supported_modes - semantics = mode_capability["semantics"] - assert semantics is not None + # Assert for Position Semantics + position_semantics = mode_capability["semantics"] + assert position_semantics is not None - action_mappings = semantics["actionMappings"] - assert action_mappings is not None + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower", "Alexa.Actions.Close"], + "directive": {"name": "SetMode", "payload": {"mode": "position.closed"}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise", "Alexa.Actions.Open"], + "directive": {"name": "SetMode", "payload": {"mode": "position.open"}}, + } in position_action_mappings - state_mappings = semantics["stateMappings"] - assert state_mappings is not None + position_state_mappings = position_semantics["stateMappings"] + assert position_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": "position.closed", + } in position_state_mappings + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Open"], + "value": "position.open", + } in position_state_mappings - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2534,7 +2585,7 @@ async def test_cover_position_mode(hass): assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.closed" - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2548,7 +2599,7 @@ async def test_cover_position_mode(hass): assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.open" - call, msg = await assert_request_calls_service( + _, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "cover#test_mode", @@ -2668,7 +2719,7 @@ async def test_cover_tilt_position_range(hass): range_capability = get_capability(capabilities, "Alexa.RangeController") assert range_capability is not None - assert range_capability["instance"] == "cover.tilt_position" + assert range_capability["instance"] == "cover.tilt" semantics = range_capability["semantics"] assert semantics is not None @@ -2686,7 +2737,7 @@ async def test_cover_tilt_position_range(hass): "cover.set_cover_tilt_position", hass, payload={"rangeValue": "50"}, - instance="cover.tilt_position", + instance="cover.tilt", ) assert call.data["position"] == 50 @@ -2697,7 +2748,7 @@ async def test_cover_tilt_position_range(hass): "cover.close_cover_tilt", hass, payload={"rangeValue": "0"}, - instance="cover.tilt_position", + instance="cover.tilt", ) properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" @@ -2711,7 +2762,7 @@ async def test_cover_tilt_position_range(hass): "cover.open_cover_tilt", hass, payload={"rangeValue": "100"}, - instance="cover.tilt_position", + instance="cover.tilt", ) properties = msg["context"]["properties"][0] assert properties["name"] == "rangeValue" @@ -2727,12 +2778,12 @@ async def test_cover_tilt_position_range(hass): False, "cover.set_cover_tilt_position", "tilt_position", - instance="cover.tilt_position", + instance="cover.tilt", ) -async def test_cover_semantics(hass): - """Test cover discovery and semantics.""" +async def test_cover_semantics_position_and_tilt(hass): + """Test cover discovery and semantics with position and tilt support.""" device = ( "cover.test_semantics", "open", @@ -2754,50 +2805,57 @@ async def test_cover_semantics(hass): appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" ) - for range_instance in ("cover.position", "cover.tilt_position"): - range_capability = get_capability( - capabilities, "Alexa.RangeController", range_instance - ) - semantics = range_capability["semantics"] - assert semantics is not None + # Assert for Position Semantics + position_capability = get_capability( + capabilities, "Alexa.RangeController", "cover.position" + ) + position_semantics = position_capability["semantics"] + assert position_semantics is not None - action_mappings = semantics["actionMappings"] - assert action_mappings is not None - if range_instance == "cover.position": - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Lower"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, - } in action_mappings - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Raise"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, - } in action_mappings - elif range_instance == "cover.position": - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Close"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, - } in action_mappings - assert { - "@type": "ActionsToDirective", - "actions": ["Alexa.Actions.Open"], - "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, - } in action_mappings + position_action_mappings = position_semantics["actionMappings"] + assert position_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in position_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in position_action_mappings - state_mappings = semantics["stateMappings"] - assert state_mappings is not None - assert { - "@type": "StatesToValue", - "states": ["Alexa.States.Closed"], - "value": 0, - } in state_mappings - assert { - "@type": "StatesToRange", - "states": ["Alexa.States.Open"], - "range": {"minimumValue": 1, "maximumValue": 100}, - } in state_mappings + # Assert for Tilt Semantics + tilt_capability = get_capability( + capabilities, "Alexa.RangeController", "cover.tilt" + ) + tilt_semantics = tilt_capability["semantics"] + assert tilt_semantics is not None + tilt_action_mappings = tilt_semantics["actionMappings"] + assert tilt_action_mappings is not None + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in tilt_action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in tilt_action_mappings + + tilt_state_mappings = tilt_semantics["stateMappings"] + assert tilt_state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in tilt_state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in tilt_state_mappings async def test_input_number(hass): From 7b29a498c6d90476c9a17fb24d796e5d5c9f4154 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 18 Jan 2020 00:28:34 +0100 Subject: [PATCH 148/393] Fix issue with group unique id when normalising bridge id (#30904) --- homeassistant/components/deconz/__init__.py | 11 +++++++++-- homeassistant/components/deconz/config_flow.py | 4 ++-- homeassistant/components/deconz/const.py | 3 ++- homeassistant/components/deconz/light.py | 7 ++++++- homeassistant/components/deconz/services.py | 12 ++++++------ tests/components/deconz/test_services.py | 6 +++--- 6 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 096bc6c2904..507b48da9db 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,10 +1,11 @@ """Support for deCONZ devices.""" import voluptuous as vol +from homeassistant.config_entries import _UNDEF from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_MASTER_GATEWAY, DOMAIN +from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services @@ -37,8 +38,14 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: + + new_data = _UNDEF + if CONF_BRIDGE_ID in config_entry.data: + new_data = dict(config_entry.data) + new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID] + hass.config_entries.async_update_entry( - config_entry, unique_id=gateway.api.config.bridgeid + config_entry, unique_id=gateway.api.config.bridgeid, data=new_data ) hass.data[DOMAIN][config_entry.unique_id] = gateway diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index dd37cc31fae..5a9ef232e61 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.helpers import aiohttp_client from .const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, + CONF_BRIDGE_ID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -74,7 +74,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.bridge_id = bridge[CONF_BRIDGEID] + self.bridge_id = bridge[CONF_BRIDGE_ID] self.deconz_config = { CONF_HOST: bridge[CONF_HOST], CONF_PORT: bridge[CONF_PORT], diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 41ef80b367f..e951e61fde7 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,7 +5,8 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" -CONF_BRIDGEID = "bridgeid" +CONF_BRIDGE_ID = "bridgeid" +CONF_GROUP_ID_BASE = "group_id_base" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index af708a15391..15d3b828741 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -22,6 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import ( + CONF_GROUP_ID_BASE, COVER_TYPES, DOMAIN as DECONZ_DOMAIN, NEW_GROUP, @@ -205,7 +206,11 @@ class DeconzGroup(DeconzLight): """Set up group and create an unique id.""" super().__init__(device, gateway) - self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}" + group_id_base = self.gateway.config_entry.unique_id + if CONF_GROUP_ID_BASE in self.gateway.config_entry.data: + group_id_base = self.gateway.config_entry.data[CONF_GROUP_ID_BASE] + + self._unique_id = f"{group_id_base}-{self._device.deconz_id}" @property def unique_id(self): diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 9d133acdb1d..f20ff65c434 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -6,7 +6,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway from .const import ( _LOGGER, - CONF_BRIDGEID, + CONF_BRIDGE_ID, DOMAIN, NEW_GROUP, NEW_LIGHT, @@ -27,14 +27,14 @@ SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( vol.Optional(SERVICE_ENTITY): cv.entity_id, vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str, + vol.Optional(CONF_BRIDGE_ID): str, } ), cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), ) SERVICE_DEVICE_REFRESH = "device_refresh" -SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) +SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGE_ID): str})) async def async_setup_services(hass): @@ -97,7 +97,7 @@ 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_BRIDGEID) + bridgeid = data.get(CONF_BRIDGE_ID) field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] @@ -119,8 +119,8 @@ async def async_configure_service(hass, data): async def async_refresh_devices_service(hass, data): """Refresh available devices from deCONZ.""" gateway = get_master_gateway(hass) - if CONF_BRIDGEID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + if CONF_BRIDGE_ID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGE_ID]] groups = set(gateway.api.groups.keys()) lights = set(gateway.api.lights.keys()) diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index f77a74006e7..07985e4d9f4 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -5,7 +5,7 @@ import pytest import voluptuous as vol from homeassistant.components import deconz -from homeassistant.components.deconz.const import CONF_BRIDGEID +from homeassistant.components.deconz.const import CONF_BRIDGE_ID from .test_gateway import BRIDGEID, setup_deconz_integration @@ -91,7 +91,7 @@ async def test_configure_service_with_field(hass): data = { deconz.services.SERVICE_FIELD: "/light/2", - CONF_BRIDGEID: BRIDGEID, + CONF_BRIDGE_ID: BRIDGEID, deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } @@ -180,7 +180,7 @@ async def test_service_refresh_devices(hass): """Test that service can refresh devices.""" gateway = await setup_deconz_integration(hass) - data = {CONF_BRIDGEID: BRIDGEID} + data = {CONF_BRIDGE_ID: BRIDGEID} with patch( "pydeconz.DeconzSession.request", From c9db21ffac81fcf9dc035233b3734533f0667c33 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 18 Jan 2020 00:33:46 +0100 Subject: [PATCH 149/393] Fix service device refresh calling state update (#30920) --- .../components/deconz/binary_sensor.py | 8 ++++--- .../components/deconz/deconz_device.py | 5 +++- .../components/deconz/deconz_event.py | 24 +++++++++++-------- homeassistant/components/deconz/manifest.json | 10 +++++--- homeassistant/components/deconz/sensor.py | 18 ++++++++------ homeassistant/components/deconz/services.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6261473bb0e..225a28f52f8 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -54,11 +54,13 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): """Representation of a deCONZ binary sensor.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"on", "reachable", "state"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index a9a1e2cdb1f..06756bb49f6 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -97,8 +97,11 @@ class DeconzDevice(DeconzBase, Entity): unsub_dispatcher() @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the device's state.""" + if ignore_update: + return + self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 3c2442994a5..527e8d2ab7a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -39,17 +39,21 @@ class DeconzEvent(DeconzBase): self._device = None @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Fire the event if reason is that state is updated.""" - if "state" in self._device.changed_keys: - data = { - CONF_ID: self.event_id, - CONF_UNIQUE_ID: self.serial, - CONF_EVENT: self._device.state, - } - if self._device.gesture: - data[CONF_GESTURE] = self._device.gesture - self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) + if ignore_update or "state" not in self._device.changed_keys: + return + + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_EVENT: self._device.state, + } + + if self._device.gesture: + data[CONF_GESTURE] = self._device.gesture + + self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) async def async_update_device_registry(self): """Update device registry.""" diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index a327d7106fc..f448e9105c8 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,13 +3,17 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==67"], + "requirements": [ + "pydeconz==68" + ], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "dependencies": [], - "codeowners": ["@kane610"], + "codeowners": [ + "@kane610" + ], "quality_scale": "platinum" -} +} \ No newline at end of file diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 8194dd145dc..8261f03e902 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -97,11 +97,13 @@ class DeconzSensor(DeconzDevice): """Representation of a deCONZ sensor.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the sensor's state.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"on", "reachable", "state"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property @@ -155,11 +157,13 @@ class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" @callback - def async_update_callback(self, force_update=False): + def async_update_callback(self, force_update=False, ignore_update=False): """Update the battery's state, if needed.""" - changed = set(self._device.changed_keys) + if ignore_update: + return + keys = {"battery", "reachable"} - if force_update or any(key in changed for key in keys): + if force_update or self._device.changed_keys.intersection(keys): self.async_schedule_update_ha_state() @property @@ -217,7 +221,7 @@ class DeconzSensorStateTracker: self.sensor = None @callback - def async_update_callback(self): + def async_update_callback(self, ignore_update=False): """Sensor state updated.""" if "battery" in self.sensor.changed_keys: async_dispatcher_send( diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index f20ff65c434..f893b9880fd 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -127,7 +127,7 @@ async def async_refresh_devices_service(hass, data): scenes = set(gateway.api.scenes.keys()) sensors = set(gateway.api.sensors.keys()) - await gateway.api.refresh_state() + await gateway.api.refresh_state(ignore_update=True) gateway.async_add_device_callback( NEW_GROUP, diff --git a/requirements_all.txt b/requirements_all.txt index d726862f290..cc3b6d733c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1188,7 +1188,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==67 +pydeconz==68 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c87df849389..0ae6225c7a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -417,7 +417,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.1 # homeassistant.components.deconz -pydeconz==67 +pydeconz==68 # homeassistant.components.zwave pydispatcher==2.0.5 From 9f20185cee2b760dcda6eea125f3523a9598bf45 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 17 Jan 2020 15:38:38 -0800 Subject: [PATCH 150/393] Fix hue accepting filename (#30924) --- homeassistant/components/hue/__init__.py | 7 +++++-- tests/components/hue/test_init.py | 23 +++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index b294a811c61..7349f4fe6a6 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -36,6 +36,7 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS ): cv.boolean, + vol.Optional("filename"): str, } ) @@ -46,8 +47,10 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_BRIDGES): vol.All( cv.ensure_list, [ - cv.deprecated("filename", invalidation_version="0.106.0"), - vol.All(BRIDGE_CONFIG_SCHEMA), + vol.All( + cv.deprecated("filename", invalidation_version="0.106.0"), + BRIDGE_CONFIG_SCHEMA, + ), ], ) } diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 2e147fd097b..35e1ba689b4 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -29,12 +29,14 @@ async def test_setup_defined_hosts_known_auth(hass): hue.DOMAIN, { hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - "filename": "bla", - } + hue.CONF_BRIDGES: [ + { + hue.CONF_HOST: "0.0.0.0", + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True, + }, + {hue.CONF_HOST: "1.1.1.1", "filename": "bla"}, + ] } }, ) @@ -42,7 +44,7 @@ async def test_setup_defined_hosts_known_auth(hass): ) # Flow started for discovered bridge - assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.flow.async_progress()) == 1 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { @@ -50,8 +52,13 @@ async def test_setup_defined_hosts_known_auth(hass): hue.CONF_HOST: "0.0.0.0", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, + }, + "1.1.1.1": { + hue.CONF_HOST: "1.1.1.1", + hue.CONF_ALLOW_HUE_GROUPS: True, + hue.CONF_ALLOW_UNREACHABLE: False, "filename": "bla", - } + }, } From 4d7c8e254b208bebe665eb44156a0fcd51b16787 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 18 Jan 2020 00:31:52 +0000 Subject: [PATCH 151/393] [ci skip] Translation update --- .../components/airly/.translations/es.json | 3 ++ .../components/vizio/.translations/es.json | 40 +++++++++++++++++++ .../components/vizio/.translations/fr.json | 12 +++++- .../vizio/.translations/zh-Hant.json | 40 +++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/vizio/.translations/es.json create mode 100644 homeassistant/components/vizio/.translations/zh-Hant.json 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/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json new file mode 100644 index 00000000000..75c985aa48d --- /dev/null +++ b/homeassistant/components/vizio/.translations/es.json @@ -0,0 +1,40 @@ +{ + "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.", + "host_exists": "Host ya configurado del componente de Vizio", + "name_exists": "Nombre ya configurado del componente de Vizio", + "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": { + "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 index 8e01a7aad96..90a985ca18c 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -18,12 +18,20 @@ "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": { + "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/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json new file mode 100644 index 00000000000..069d77ba4db --- /dev/null +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "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", + "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_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": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "name_exists": "\u540d\u7a31\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": { + "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 From 58520aa733c18e93ee155dd4bbd4f373cd64afc5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 17 Jan 2020 19:43:02 -0500 Subject: [PATCH 152/393] Add battery voltage state attribute for ZHA devices (#30901) * Add battery voltage state attribute for ZHA devices. * Pylint. --- homeassistant/components/zha/core/channels/general.py | 9 +++++++++ homeassistant/components/zha/sensor.py | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) 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/sensor.py b/homeassistant/components/zha/sensor.py index ce02bf11d9d..5c51717b161 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -128,7 +128,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 @@ -209,6 +209,12 @@ class Battery(Sensor): state_attrs["battery_quantity"] = battery_quantity return state_attrs + def async_update_state_attribute(self, key, value): + """Update a single device state attribute.""" + if key == "battery_voltage": + self._device_state_attributes["voltage"] = f"{round(value/10, 1)}V" + self.async_schedule_update_ha_state() + @STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): From 3bf657284cf42f86008eaa2ff9a55a7ae2ab2d02 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Fri, 17 Jan 2020 20:53:31 -0500 Subject: [PATCH 153/393] Refactor rounding for ZHA electrical measurement sensor (#30923) --- homeassistant/components/zha/sensor.py | 7 ++++--- tests/components/zha/test_sensor.py | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 5c51717b161..987fbf59baf 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -152,8 +152,6 @@ 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 def async_set_state(self, state): @@ -231,7 +229,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/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): From 52cee84c2cff598efe0d4cb0f57e6359a0f88a40 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 18 Jan 2020 06:41:42 +0100 Subject: [PATCH 154/393] Update outdated documentation links in json files (#30916) --- homeassistant/components/apprise/manifest.json | 2 +- homeassistant/components/geonetnz_volcano/manifest.json | 2 +- homeassistant/components/pcal9535a/manifest.json | 2 +- homeassistant/components/sinch/manifest.json | 2 +- homeassistant/components/starline/manifest.json | 2 +- homeassistant/components/versasense/manifest.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 9d79c2554ec..1f41d5a24e2 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -1,7 +1,7 @@ { "domain": "apprise", "name": "Apprise", - "documentation": "https://www.home-assistant.io/components/apprise", + "documentation": "https://www.home-assistant.io/integrations/apprise", "requirements": ["apprise==0.8.3"], "dependencies": [], "codeowners": ["@caronc"] 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/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/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/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/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"] From 8630a076a7a9a61796921ca6c04933c1981f27ff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 18 Jan 2020 03:15:06 -0500 Subject: [PATCH 155/393] Use default media player device classes for vizio component (#30802) * use media player defined device classes instead of custom ones, add options flow test, add timeout options parameter * make options update error more generic * fix config flow options update logic * simplify logic for options update during import * use platform list for load and unload * update private config flow function name and description * fix grammar in strings.json * update mock config variable names to be more accurate * remove timeout conf option, create device_class property * update requirements * update .coveragerc to indicate that config_flow has tests * fix source of device_class property and move constants to const.py * fix grammar in error message * remove redundant device check in async_setup_entry since device connection is checked during config flow * revert change to async_setup_entry, raise ConfigEntryNotReady if device can't be connected to * update error text * add more context to error text --- .coveragerc | 4 +- homeassistant/components/vizio/__init__.py | 57 +++++++-------- homeassistant/components/vizio/config_flow.py | 28 ++++---- homeassistant/components/vizio/const.py | 69 +++++++++++++++++- .../components/vizio/media_player.py | 71 ++++++++----------- homeassistant/components/vizio/strings.json | 5 +- tests/components/vizio/test_config_flow.py | 66 +++++++++++------ 7 files changed, 188 insertions(+), 112 deletions(-) diff --git a/.coveragerc b/.coveragerc index 87bd0522e81..91f99fe84d4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -782,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 diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index c890af2700d..ac02698dbfd 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1,49 +1,30 @@ """The vizio component.""" +import asyncio + import voluptuous as vol +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, - CONF_HOST, - CONF_NAME, -) +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, - DOMAIN, -) +from .const import DOMAIN, VIZIO_SCHEMA def validate_auth(config: ConfigType) -> ConfigType: - """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" + """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( @@ -53,6 +34,8 @@ CONFIG_SCHEMA = vol.Schema( 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.""" @@ -69,15 +52,23 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Load the saved entities.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "media_player") - ) + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) return True async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "media_player") + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) - return True + return unload_ok diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 5863d89c972..b02be9a5934 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -6,6 +6,7 @@ from pyvizio import VizioAsync 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, @@ -27,8 +28,8 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def update_schema_defaults(input_dict: Dict[str, Any]) -> vol.Schema: - """Update schema defaults based on user input/config dict. Retains info already provided for future form views.""" +def _config_flow_schema(input_dict: Dict[str, Any]) -> 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.""" return vol.Schema( { vol.Required( @@ -38,7 +39,7 @@ def update_schema_defaults(input_dict: Dict[str, Any]) -> vol.Schema: vol.Optional( CONF_DEVICE_CLASS, default=input_dict.get(CONF_DEVICE_CLASS, DEFAULT_DEVICE_CLASS), - ): vol.All(str, vol.Lower, vol.In(["tv", "soundbar"])), + ): 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, @@ -72,7 +73,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: # Store current values in case setup fails and user needs to edit - self.user_schema = update_schema_defaults(user_input) + self.user_schema = _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): @@ -116,7 +117,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=user_input[CONF_NAME], data=user_input ) - schema = self.user_schema or self.import_schema or update_schema_defaults({}) + schema = self.user_schema or self.import_schema or _config_flow_schema({}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) @@ -127,20 +128,23 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[ CONF_NAME ] == import_config.get(CONF_NAME): + new_options = {} + if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: - new_volume_step = { - CONF_VOLUME_STEP: import_config[CONF_VOLUME_STEP] - } + new_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] + + if new_options: self.hass.config_entries.async_update_entry( entry=entry, - data=entry.data.copy().update(new_volume_step), - options=entry.options.copy().update(new_volume_step), + data=entry.data.copy().update(new_options), + options=entry.options.copy().update(new_options), ) - return self.async_abort(reason="updated_volume_step") + return self.async_abort(reason="updated_options") + return self.async_abort(reason="already_setup") # Store import values in case setup fails so user can see error - self.import_schema = update_schema_defaults(import_config) + self.import_schema = _config_flow_schema(import_config) return await self.async_step_user(user_input=import_config) diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index b87e40d3b46..0345b0c9992 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -1,11 +1,76 @@ """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_VOLUME_STEP = 1 -DEFAULT_DEVICE_CLASS = "tv" + DEVICE_ID = "pyvizio" 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/media_player.py b/homeassistant/components/vizio/media_player.py index 44f44c0c48e..76bb476317e 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,5 +1,4 @@ """Vizio SmartCast Device support.""" -from datetime import timedelta import logging from typing import Callable, List @@ -7,16 +6,6 @@ from pyvizio import VizioAsync from homeassistant import util from homeassistant.components.media_player import 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.config_entries import ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -35,29 +24,23 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, ICON +from .const import ( + CONF_VOLUME_STEP, + 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) PARALLEL_UPDATES = 0 -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), -} - async def async_setup_entry( hass: HomeAssistantType, @@ -68,10 +51,10 @@ async def async_setup_entry( host = config_entry.data[CONF_HOST] token = config_entry.data.get(CONF_ACCESS_TOKEN) name = config_entry.data[CONF_NAME] - device_type = config_entry.data[CONF_DEVICE_CLASS] + 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 CONF_VOLUME_STEP not in config_entry.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} @@ -84,22 +67,24 @@ async def async_setup_entry( host, name, token, - device_type, + VIZIO_DEVICE_CLASSES[device_class], session=async_get_clientsession(hass, False), ) if not await device.can_connect(): fail_auth_msg = "" if token: - fail_auth_msg = ", auth token is correct" + fail_auth_msg = "and auth token '{token}' are correct." + else: + fail_auth_msg = "is correct." _LOGGER.error( - "Failed to set up Vizio platform, please check if host " - "is valid and available, device type is correct%s", + "Failed to connect to Vizio device, please check if host '{host}'" + "is valid and available. Also check if device class '{device_class}' %s", fail_auth_msg, ) raise PlatformNotReady - entity = VizioDevice(config_entry, device, name, volume_step, device_type) + entity = VizioDevice(config_entry, device, name, volume_step, device_class) async_add_entities([entity], True) @@ -113,7 +98,7 @@ class VizioDevice(MediaPlayerDevice): device: VizioAsync, name: str, volume_step: int, - device_type: str, + device_class: str, ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry @@ -125,11 +110,11 @@ 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_class = device_class + self._supported_commands = SUPPORTED_COMMANDS[device_class] self._device = device self._max_volume = float(self._device.get_max_volume()) - self._icon = ICON[device_type] + self._icon = ICON[device_class] self._available = True @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @@ -169,7 +154,8 @@ class VizioDevice(MediaPlayerDevice): 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 + # 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: @@ -253,6 +239,11 @@ class VizioDevice(MediaPlayerDevice): "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.""" await self._device.pow_on() diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 029643ab578..07bbfc666cf 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -23,7 +23,7 @@ "already_setup": "This entry has already been setup.", "host_exists": "Vizio component with host already configured.", "name_exists": "Vizio component with name already configured.", - "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." + "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." } }, "options": { @@ -32,7 +32,8 @@ "init": { "title": "Update Vizo SmartCast Options", "data": { - "volume_step": "Volume Step Size" + "volume_step": "Volume Step Size", + "timeout": "API Request Timeout (seconds)" } } } diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 9e657cf926d..c8255b9f5fe 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -6,12 +6,13 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.vizio import VIZIO_SCHEMA +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.const import ( CONF_ACCESS_TOKEN, @@ -27,20 +28,18 @@ _LOGGER = logging.getLogger(__name__) NAME = "Vizio" HOST = "192.168.1.1:9000" -DEVICE_CLASS_TV = "tv" -DEVICE_CLASS_SOUNDBAR = "soundbar" ACCESS_TOKEN = "deadbeef" VOLUME_STEP = 2 UNIQUE_ID = "testid" -MOCK_USER_VALID_TV_ENTRY = { +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_ENTRY = { +MOCK_IMPORT_VALID_TV_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_TV, @@ -48,16 +47,16 @@ MOCK_IMPORT_VALID_TV_ENTRY = { CONF_VOLUME_STEP: VOLUME_STEP, } -MOCK_INVALID_TV_ENTRY = { +MOCK_INVALID_TV_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_TV, } -MOCK_SOUNDBAR_ENTRY = { +MOCK_SPEAKER_CONFIG = { CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, } @@ -100,7 +99,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType, vizio_connect) user_input={ CONF_NAME: NAME, CONF_HOST: HOST, - CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR, + CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER, }, ) @@ -108,7 +107,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType, vizio_connect) assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SOUNDBAR + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER async def test_user_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None: @@ -139,17 +138,40 @@ async def test_user_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> N 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, context={"source": "test"}, 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 ) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_SOUNDBAR_ENTRY, + data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP}, ) entry.add_to_hass(hass) - fail_entry = MOCK_SOUNDBAR_ENTRY.copy() + fail_entry = MOCK_SPEAKER_CONFIG.copy() fail_entry[CONF_NAME] = "newtestname" result = await hass.config_entries.flow.async_init( @@ -160,7 +182,7 @@ async def test_user_host_already_configured( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=fail_entry, + result["flow_id"], user_input=fail_entry ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -173,12 +195,12 @@ async def test_user_name_already_configured( """Test name is already configured during user setup.""" entry = MockConfigEntry( domain=DOMAIN, - data=MOCK_SOUNDBAR_ENTRY, + data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP}, ) entry.add_to_hass(hass) - fail_entry = MOCK_SOUNDBAR_ENTRY.copy() + fail_entry = MOCK_SPEAKER_CONFIG.copy() fail_entry[CONF_HOST] = "0.0.0.0" result = await hass.config_entries.flow.async_init( @@ -207,7 +229,7 @@ async def test_user_error_on_could_not_connect( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_USER_VALID_TV_ENTRY + result["flow_id"], MOCK_USER_VALID_TV_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cant_connect"} @@ -225,7 +247,7 @@ async def test_user_error_on_tv_needs_token( assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_INVALID_TV_ENTRY + result["flow_id"], MOCK_INVALID_TV_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -240,7 +262,7 @@ async def test_import_flow_minimum_fields( DOMAIN, context={"source": "import"}, data=vol.Schema(VIZIO_SCHEMA)( - {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SOUNDBAR} + {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER} ), ) @@ -248,7 +270,7 @@ async def test_import_flow_minimum_fields( 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_SOUNDBAR + assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP @@ -257,7 +279,7 @@ async def test_import_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, - data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_ENTRY), + data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -275,11 +297,11 @@ async def test_import_entity_already_configured( """Test entity is already configured during import setup.""" entry = MockConfigEntry( domain=DOMAIN, - data=vol.Schema(VIZIO_SCHEMA)(MOCK_SOUNDBAR_ENTRY), + 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_SOUNDBAR_ENTRY.copy()) + fail_entry = vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG.copy()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data=fail_entry From bfa8cb760f41a820672371f261a9191dcb04589a Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 18 Jan 2020 12:14:50 +0000 Subject: [PATCH 156/393] Add services to geniushub (#30918) adds: - `set_zone_override`, and - `set_zone_mode` --- .../components/geniushub/__init__.py | 99 +++++++++++++++++-- .../components/geniushub/services.yaml | 29 ++++++ 2 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/geniushub/services.yaml 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}' + From a037c1d7884e9913d1fbd47f13356ba70407fa60 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 18 Jan 2020 12:21:22 +0000 Subject: [PATCH 157/393] Add services to evohome (#29816) --- homeassistant/components/evohome/__init__.py | 242 +++++++++++++++--- homeassistant/components/evohome/climate.py | 111 ++++++-- homeassistant/components/evohome/const.py | 2 +- .../components/evohome/services.yaml | 53 ++++ .../components/evohome/water_heater.py | 23 +- 5 files changed, 363 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/evohome/services.yaml diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 3d903e86e30..b9d3f35964a 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. @@ -497,12 +669,12 @@ class EvoChild(EvoDevice): 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..50155e6dd21 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, @@ -90,8 +102,9 @@ async def async_setup_platform( zone.zoneId, zone.name, ) + new_entity = EvoThermostat(broker, zone) - async_add_entities([EvoThermostat(broker, zone)], update_before_add=True) + async_add_entities([new_entity], update_before_add=True) return controller = EvoController(broker, broker.tcs) @@ -105,13 +118,15 @@ async def async_setup_platform( zone.zoneId, zone.name, ) - zones.append(EvoZone(broker, zone)) + new_entity = EvoZone(broker, zone) + + zones.append(new_entity) 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,9 +134,31 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): self._preset_modes = None - async def _set_tcs_mode(self, op_mode: str) -> None: + 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._call_client_api(self._evo_tcs.set_status(op_mode)) + await self._evo_broker.call_client_api(self._evo_tcs.set_status(mode)) @property def hvac_modes(self) -> List[str]: @@ -135,7 +172,7 @@ 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.""" @@ -152,6 +189,32 @@ class EvoZone(EvoChild, EvoClimateDevice): else: self._precision = self._evo_device.setpointCapabilities["valueResolution"] + 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.""" @@ -206,16 +269,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 +300,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 +326,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 +339,14 @@ class EvoZone(EvoChild, EvoClimateDevice): class EvoController(EvoClimateDevice): - """Base for a Honeywell evohome Controller (hub). + """Base for a Honeywell TCC Controller (hub). The Controller (aka TCS, temperature control system) is the parent of all the child (CH/DHW) devices. It is also a Climate device. """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize a evohome Controller (hub).""" + """Initialize an evohome Controller (hub).""" super().__init__(evo_broker, evo_device) self._unique_id = evo_device.systemId @@ -349,7 +416,7 @@ class EvoController(EvoClimateDevice): class EvoThermostat(EvoZone): - """Base for a Honeywell Round Thermostat. + """Base for a Honeywell TCC Round Thermostat. These are implemented as a combined Controller/Zone. """ 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..cd4fb2aadce 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -38,17 +38,16 @@ async def async_setup_platform( 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.""" From 4b67508330ac970608df43302f103a9d1eedb37a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 18 Jan 2020 14:02:15 +0100 Subject: [PATCH 158/393] Revert "Pulseaudio: Changed default port from 4712 to 4713 (#28857)" (#30939) This reverts commit e915dd0d95d6bc407e4659bfadcef1468c8434f4. --- homeassistant/components/pulseaudio_loopback/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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." From 26fb1ce25577315a639dae552c3e98cda0a7ef85 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 18 Jan 2020 10:03:59 -0500 Subject: [PATCH 159/393] Don't use unit_of_measurement in state attributes. (#30941) --- homeassistant/components/zha/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 987fbf59baf..bb764ab406d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -210,7 +210,7 @@ class Battery(Sensor): def async_update_state_attribute(self, key, value): """Update a single device state attribute.""" if key == "battery_voltage": - self._device_state_attributes["voltage"] = f"{round(value/10, 1)}V" + self._device_state_attributes[key] = round(value / 10, 1) self.async_schedule_update_ha_state() From afb1b0cd3c9a9ad57a6aca757181d46cda56dc67 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Sat, 18 Jan 2020 17:37:39 +0000 Subject: [PATCH 160/393] Add play media support and Spotify control to Openhome (#28698) --- .../components/openhome/media_player.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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() From 656ef6566b741b8c9e29c31e0395e54c7b8d664b Mon Sep 17 00:00:00 2001 From: ajmarks Date: Sat, 18 Jan 2020 15:33:18 -0500 Subject: [PATCH 161/393] Minor enhancements to jewish_calendar (#30632) * Minor enhancement to jewish_calendar: - Expose more halachic times from the underlying hdate module - Correct and standardize some transliterations * Undo breking name change * Add icon for talit time Co-authored-by: Andrew Marks <52414333+amarks-coatue@users.noreply.github.com> --- homeassistant/components/jewish_calendar/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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", From 078ce24e5a0f0afcf7bb286b9f9c2c5ed6460068 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 18 Jan 2020 19:27:55 -0500 Subject: [PATCH 162/393] Add logical Zigbee device type to ZHA device info (#30954) * add device type to device info * capitalize * use zigpy logical device type --- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index bf778812453..3fbb62f8433 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -18,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" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 634a06f7f58..5c3b3578c12 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, ) @@ -160,6 +162,12 @@ 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.""" + device_type = self._zigpy_device.node_desc.logical_type + return device_type.name if device_type else UNKNOWN + @property def power_source(self): """Return the power source for the device.""" @@ -281,6 +289,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): From 6c84c126ea8c4217309215ed52a65b65f1accd95 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 19 Jan 2020 00:32:00 +0000 Subject: [PATCH 163/393] [ci skip] Translation update --- .../components/airly/.translations/fr.json | 3 +++ .../climate/.translations/zh-Hans.json | 17 ++++++++++++ .../deconz/.translations/zh-Hans.json | 23 ++++++++++++++++ .../device_tracker/.translations/zh-Hans.json | 8 ++++++ .../components/fan/.translations/zh-Hans.json | 16 ++++++++++++ .../lock/.translations/zh-Hans.json | 8 ++++++ .../media_player/.translations/zh-Hans.json | 11 ++++++++ .../sensor/.translations/zh-Hans.json | 26 +++++++++++++++++++ .../components/sentry/.translations/fr.json | 1 + .../unifi/.translations/zh-Hans.json | 12 +++++++++ .../vacuum/.translations/zh-Hans.json | 15 +++++++++++ .../components/vizio/.translations/da.json | 2 ++ .../components/vizio/.translations/de.json | 2 ++ .../components/vizio/.translations/en.json | 2 ++ .../components/vizio/.translations/fr.json | 4 +++ .../components/vizio/.translations/pl.json | 11 ++++++++ .../vizio/.translations/zh-Hant.json | 2 ++ 17 files changed, 163 insertions(+) create mode 100644 homeassistant/components/climate/.translations/zh-Hans.json create mode 100644 homeassistant/components/device_tracker/.translations/zh-Hans.json create mode 100644 homeassistant/components/fan/.translations/zh-Hans.json create mode 100644 homeassistant/components/lock/.translations/zh-Hans.json create mode 100644 homeassistant/components/media_player/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/zh-Hans.json create mode 100644 homeassistant/components/vacuum/.translations/zh-Hans.json create mode 100644 homeassistant/components/vizio/.translations/pl.json 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/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/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/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/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/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/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/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/sentry/.translations/fr.json b/homeassistant/components/sentry/.translations/fr.json index d5537f8632d..7702874866a 100644 --- a/homeassistant/components/sentry/.translations/fr.json +++ b/homeassistant/components/sentry/.translations/fr.json @@ -9,6 +9,7 @@ }, "step": { "user": { + "description": "Entrez votre DSN Sentry", "title": "Sentry" } }, 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/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/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index fecc07bd9a1..62da3d0184d 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -5,6 +5,7 @@ "already_setup": "Denne post er allerede blevet konfigureret.", "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": { @@ -30,6 +31,7 @@ "step": { "init": { "data": { + "timeout": "Timeout for API-anmodning (sekunder)", "volume_step": "Lydstyrkestrinst\u00f8rrelse" }, "title": "Opdater Vizo SmartCast-indstillinger" diff --git a/homeassistant/components/vizio/.translations/de.json b/homeassistant/components/vizio/.translations/de.json index a0eba0a29e1..ead4ed4828b 100644 --- a/homeassistant/components/vizio/.translations/de.json +++ b/homeassistant/components/vizio/.translations/de.json @@ -5,6 +5,7 @@ "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": { @@ -30,6 +31,7 @@ "step": { "init": { "data": { + "timeout": "API Request Timeout (Sekunden)", "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "title": "Aktualisieren Sie die Vizo SmartCast-Optionen" diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 3be97349890..4db4c35894e 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -5,6 +5,7 @@ "already_setup": "This entry has already been setup.", "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": { @@ -30,6 +31,7 @@ "step": { "init": { "data": { + "timeout": "API Request Timeout (seconds)", "volume_step": "Volume Step Size" }, "title": "Update Vizo SmartCast Options" diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index 90a985ca18c..9ec2abe56a1 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -5,6 +5,7 @@ "already_setup": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e.", "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": { @@ -29,6 +30,9 @@ "options": { "step": { "init": { + "data": { + "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)" + }, "title": "Mettre \u00e0 jour les options de Vizo SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json new file mode 100644 index 00000000000..708334eba3d --- /dev/null +++ b/homeassistant/components/vizio/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "Token dost\u0119pu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index 069d77ba4db..b6951080f4a 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -5,6 +5,7 @@ "already_setup": "\u6b64\u7269\u4ef6\u5df2\u8a2d\u5b9a\u904e\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": { @@ -30,6 +31,7 @@ "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" From 7c155731fcdff7d8e32fd6f3fb0a7ebd45b3d7d1 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 19 Jan 2020 14:19:46 +0100 Subject: [PATCH 164/393] Fix can't add multiple iCloud accounts (remove account name) (#30898) * Fix can't add multiple iCloud accounts (remove account name) * Update tests with flow.async_init() --- homeassistant/components/icloud/__init__.py | 20 +-- .../components/icloud/config_flow.py | 21 +-- homeassistant/components/icloud/const.py | 1 - tests/components/icloud/test_config_flow.py | 125 ++++++++++-------- 4 files changed, 75 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index f36ad607634..d1e00d65e10 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -23,7 +23,6 @@ from homeassistant.util.dt import utcnow from homeassistant.util.location import distance from .const import ( - CONF_ACCOUNT_NAME, CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, DEFAULT_GPS_ACCURACY_THRESHOLD, @@ -100,7 +99,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,20 +138,13 @@ 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] 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) hass.data[DOMAIN][username] = account @@ -254,7 +245,6 @@ class IcloudAccount: username: str, password: str, icloud_dir: Store, - account_name: str, max_interval: int, gps_accuracy_threshold: int, ): @@ -262,7 +252,6 @@ class IcloudAccount: 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 @@ -434,11 +423,6 @@ class IcloudAccount: 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.""" @@ -471,7 +455,6 @@ class IcloudDevice: 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 @@ -494,7 +477,6 @@ class IcloudDevice: 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, diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index cf05c07e26f..553ec1a28b4 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,14 +43,10 @@ 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.""" + def _configuration_exists(self, username: str) -> bool: + """Return True if username 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 - ): + if entry.data[CONF_USERNAME] == username: return True return False @@ -91,13 +85,12 @@ 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): + if self._configuration_exists(self._username): errors[CONF_USERNAME] = "username_exists" return await self._show_setup_form(user_input, errors) @@ -119,7 +112,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,9 +119,7 @@ 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) - ): + if self._configuration_exists(user_input[CONF_USERNAME]): return self.async_abort(reason="username_exists") return await self.async_step_user(user_input) @@ -214,7 +204,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 c2545d911df..57a3f48936c 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -3,7 +3,6 @@ DOMAIN = "icloud" 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/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 5555150befc..035266287f0 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -11,22 +11,21 @@ from homeassistant.components.icloud.config_flow import ( 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 @@ -92,15 +91,17 @@ def init_config_flow(hass: HomeAssistantType): 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 +111,41 @@ 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["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,67 +155,80 @@ 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["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["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} + ).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["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} ).add_to_hass(hass) # Should fail, same USERNAME (import) - result = await flow.async_step_import( - {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, - } + 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 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"} @@ -222,14 +236,14 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): 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"} @@ -290,7 +304,6 @@ async def test_verification_code_success( 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 From 90e811df20c80b08a9fd2d0b44a91f03ffdb8777 Mon Sep 17 00:00:00 2001 From: Jan De Luyck <5451787+jdeluyck@users.noreply.github.com> Date: Sun, 19 Jan 2020 21:01:16 +0100 Subject: [PATCH 165/393] Update emulated_roku to 0.2.0 (#30974) --- homeassistant/components/emulated_roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 62d51d7d910..8b5925fd12f 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.1.9"], + "requirements": ["emulated_roku==0.2.0"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index cc3b6d733c9..949ec192564 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -474,7 +474,7 @@ eliqonline==1.2.2 elkm1-lib==0.7.15 # homeassistant.components.emulated_roku -emulated_roku==0.1.9 +emulated_roku==0.2.0 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ae6225c7a3..d5863fbdee7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,7 +171,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.emulated_roku -emulated_roku==0.1.9 +emulated_roku==0.2.0 # homeassistant.components.season ephem==3.7.7.0 From f14d34560e25ad612a7b960430eb0aa0ffb26cd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jan 2020 12:56:31 -0800 Subject: [PATCH 166/393] Improve Wemo config entry support, add device info (#30963) * Improve config entry support, add device info * async_dispatch_connect * Fix I/O in event loop * Do not raise PlatformNotReady inside dispatcher * Make main discovery process async * Do discovery as part of set up. * Greatly simplify set up * Add parallel updates to fan&switch * mini cleanup * Address comments --- homeassistant/components/wemo/__init__.py | 177 +++++++++--------- .../components/wemo/binary_sensor.py | 47 ++--- homeassistant/components/wemo/fan.py | 71 +++---- homeassistant/components/wemo/light.py | 56 +++--- homeassistant/components/wemo/switch.py | 44 ++--- 5 files changed, 206 insertions(+), 189 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9b1c4cd465f..3e4081ae300 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"].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..8e43f47ef00 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.serialnumber)}, + "model": self.wemo.model_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) From e4a53d3a0869c73f8d72cc81f677fd5f4c29ba4d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jan 2020 12:57:48 -0800 Subject: [PATCH 167/393] Improve Wemo config entry support, add device info (#30963) * Improve config entry support, add device info * async_dispatch_connect * Fix I/O in event loop * Do not raise PlatformNotReady inside dispatcher * Make main discovery process async * Do discovery as part of set up. * Greatly simplify set up * Add parallel updates to fan&switch * mini cleanup * Address comments From 765b45c81bc764944c62e91bc4a8a964899b6506 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jan 2020 13:13:10 -0800 Subject: [PATCH 168/393] Improve Wemo config entry support, add device info (#30963) * Improve config entry support, add device info * async_dispatch_connect * Fix I/O in event loop * Do not raise PlatformNotReady inside dispatcher * Make main discovery process async * Do discovery as part of set up. * Greatly simplify set up * Add parallel updates to fan&switch * mini cleanup * Address comments From 8fcd0e9a7901493f89d10c0b5de6d635f7c07fe4 Mon Sep 17 00:00:00 2001 From: steve-gombos <3118886+steve-gombos@users.noreply.github.com> Date: Sun, 19 Jan 2020 16:18:11 -0500 Subject: [PATCH 169/393] Ring camera fix (#30975) * Fix ring camera entities * Reverted test refresh interval * Fix black errors --- homeassistant/components/ring/camera.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 1526a915482..96b1a962a67 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -51,8 +51,7 @@ class RingCam(RingEntityMixin, Camera): self._last_event = None self._last_video_id = None self._video_url = None - self._utcnow = dt_util.utcnow() - self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL + self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL async def async_added_to_hass(self): """Register callbacks.""" @@ -80,7 +79,7 @@ class RingCam(RingEntityMixin, Camera): self._last_event = None self._last_video_id = None self._video_url = None - self._expires_at = self._utcnow + self._expires_at = dt_util.utcnow() self.async_write_ha_state() @property @@ -141,10 +140,8 @@ class RingCam(RingEntityMixin, Camera): if self._last_event["recording"]["status"] != "ready": return - if ( - self._last_video_id == self._last_event["id"] - and self._utcnow <= self._expires_at - ): + utcnow = dt_util.utcnow() + if self._last_video_id == self._last_event["id"] and utcnow <= self._expires_at: return try: @@ -160,4 +157,4 @@ class RingCam(RingEntityMixin, Camera): if video_url: self._last_video_id = self._last_event["id"] self._video_url = video_url - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._expires_at = FORCE_REFRESH_INTERVAL + utcnow From ecef0f6e937fa9b6d6cc5494042a275941644c62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jan 2020 13:39:16 -0800 Subject: [PATCH 170/393] Catch all Ring timeout errors (#30960) * Catch more Ring errors * Fix comment & Disable wifi entities by default --- homeassistant/components/ring/__init__.py | 30 ++++++++++++++++++++--- homeassistant/components/ring/light.py | 9 ++++++- homeassistant/components/ring/sensor.py | 12 ++++----- homeassistant/components/ring/switch.py | 9 ++++++- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 57148cc15af..34aa9f6b0ec 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Optional from oauthlib.oauth2 import AccessDeniedError +import requests from ring_doorbell import Auth, Ring import voluptuous as vol @@ -95,13 +96,19 @@ async def async_setup_entry(hass, entry): "api": ring, "devices": ring.devices(), "device_data": GlobalDataUpdater( - hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1) + hass, "device", entry.entry_id, ring, "update_devices", timedelta(minutes=1) ), "dings_data": GlobalDataUpdater( - hass, entry.entry_id, ring, "update_dings", timedelta(seconds=5) + hass, + "active dings", + entry.entry_id, + ring, + "update_dings", + timedelta(seconds=5), ), "history_data": DeviceDataUpdater( hass, + "history", entry.entry_id, ring, lambda device: device.history(limit=10), @@ -109,6 +116,7 @@ async def async_setup_entry(hass, entry): ), "health_data": DeviceDataUpdater( hass, + "health", entry.entry_id, ring, lambda device: device.update_health_data(), @@ -168,6 +176,7 @@ class GlobalDataUpdater: def __init__( self, hass: HomeAssistant, + data_type: str, config_entry_id: str, ring: Ring, update_method: str, @@ -175,6 +184,7 @@ class GlobalDataUpdater: ): """Initialize global data updater.""" self.hass = hass + self.data_type = data_type self.config_entry_id = config_entry_id self.ring = ring self.update_method = update_method @@ -215,6 +225,11 @@ class GlobalDataUpdater: _LOGGER.error("Ring access token is no longer valid. Set up Ring again") await self.hass.config_entries.async_unload(self.config_entry_id) return + except requests.Timeout: + _LOGGER.warning( + "Time out fetching Ring %s data", self.data_type, + ) + return for update_callback in self.listeners: update_callback() @@ -226,12 +241,14 @@ class DeviceDataUpdater: def __init__( self, hass: HomeAssistant, + data_type: str, config_entry_id: str, ring: Ring, update_method: str, update_interval: timedelta, ): """Initialize device data updater.""" + self.data_type = data_type self.hass = hass self.config_entry_id = config_entry_id self.ring = ring @@ -282,7 +299,7 @@ class DeviceDataUpdater: def refresh_all(self, _=None): """Refresh all registered devices.""" - for info in self.devices.values(): + for device_id, info in self.devices.items(): try: data = info["data"] = self.update_method(info["device"]) except AccessDeniedError: @@ -291,6 +308,13 @@ class DeviceDataUpdater: self.hass.config_entries.async_unload(self.config_entry_id) ) return + except requests.Timeout: + _LOGGER.warning( + "Time out fetching Ring %s data for device %s", + self.data_type, + device_id, + ) + continue for update_callback in info["update_callbacks"]: self.hass.loop.call_soon_threadsafe(update_callback, data) diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 86ef55af16d..fa47ac35ee3 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import requests + from homeassistant.components.light import Light from homeassistant.core import callback import homeassistant.util.dt as dt_util @@ -72,7 +74,12 @@ class RingLight(RingEntityMixin, Light): def _set_light(self, new_state): """Update light state, and causes Home Assistant to correctly update.""" - self._device.lights = new_state + try: + self._device.lights = new_state + except requests.Timeout: + _LOGGER.error("Time out setting %s light to %s", self.entity_id, new_state) + return + self._light_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_schedule_update_ha_state() diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 2b921dddd2f..329077a18e7 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -15,9 +15,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] - # Makes a ton of requests. We will make this a config entry option in the future - wifi_enabled = False - sensors = [] for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): @@ -25,9 +22,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if device_type not in SENSOR_TYPES[sensor_type][1]: continue - if not wifi_enabled and sensor_type.startswith("wifi_"): - continue - for device in devices[device_type]: if device_type == "battery" and device.battery_life is None: continue @@ -124,6 +118,12 @@ class HealthDataRingSensor(RingSensor): """Call update method.""" self.async_write_ha_state() + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # These sensors are data hungry and not useful. Disable by default. + return False + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 65eed83d98e..e2f1882adf6 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import requests + from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback import homeassistant.util.dt as dt_util @@ -74,7 +76,12 @@ class SirenSwitch(BaseRingSwitch): def _set_switch(self, new_state): """Update switch state, and causes Home Assistant to correctly update.""" - self._device.siren = new_state + try: + self._device.siren = new_state + except requests.Timeout: + _LOGGER.error("Time out setting %s siren to %s", self.entity_id, new_state) + return + self._siren_on = new_state > 0 self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() From a40a5a754b75741c9c449fd2cdabdf1a2ca7edca Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 19 Jan 2020 22:29:15 +0000 Subject: [PATCH 171/393] initial commit (#30968) --- homeassistant/components/evohome/climate.py | 128 +++++--------------- 1 file changed, 32 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 50155e6dd21..37c30ce4655 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -93,20 +93,6 @@ 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, - ) - new_entity = EvoThermostat(broker, zone) - - async_add_entities([new_entity], update_before_add=True) - return - controller = EvoController(broker, broker.tcs) zones = [] @@ -134,32 +120,6 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): self._preset_modes = None - 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_modes(self) -> List[str]: """Return a list of available hvac operation modes.""" @@ -355,7 +315,38 @@ class EvoController(EvoClimateDevice): 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: @@ -413,58 +404,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 TCC 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) From f20b3515f2078e1d2f06e2d175f48a7d96f61f04 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 20 Jan 2020 00:31:55 +0000 Subject: [PATCH 172/393] [ci skip] Translation update --- .../components/airly/.translations/ko.json | 3 ++ .../components/brother/.translations/ko.json | 1 + .../components/deconz/.translations/ko.json | 4 ++ .../components/gios/.translations/ko.json | 5 ++- .../components/netatmo/.translations/ko.json | 18 ++++++++ .../components/ring/.translations/ko.json | 27 ++++++++++++ .../samsungtv/.translations/ko.json | 26 ++++++++++++ .../components/vizio/.translations/ca.json | 9 ++++ .../components/vizio/.translations/es.json | 2 + .../components/vizio/.translations/ko.json | 42 +++++++++++++++++++ 10 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/netatmo/.translations/ko.json create mode 100644 homeassistant/components/ring/.translations/ko.json create mode 100644 homeassistant/components/samsungtv/.translations/ko.json create mode 100644 homeassistant/components/vizio/.translations/ko.json 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/brother/.translations/ko.json b/homeassistant/components/brother/.translations/ko.json index 4d2e213cbee..8ec7497296c 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": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index e50dca926cc..d526d706a8b 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -77,9 +77,13 @@ "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", 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/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/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/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/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index ab8d3c017ca..abbf1092bf3 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -16,5 +16,14 @@ } }, "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/es.json b/homeassistant/components/vizio/.translations/es.json index 75c985aa48d..997dde7088a 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -5,6 +5,7 @@ "already_setup": "Esta entrada ya ha sido configurada.", "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": { @@ -30,6 +31,7 @@ "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" diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json new file mode 100644 index 00000000000..f3630f8393d --- /dev/null +++ b/homeassistant/components/vizio/.translations/ko.json @@ -0,0 +1,42 @@ +{ + "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.", + "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": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc774\ub984\uc774 \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 From 0c3ffbe282fe86ada44ea09b87b90a1f258a562a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jan 2020 17:55:18 -0800 Subject: [PATCH 173/393] Add foundation for integration services (#30813) * Add foundation for integration services * Fix tests * Remove async_get_platform * Migrate Sonos partially to EntityPlatform.async_register_entity_service * Tweaks * Move other Sonos services to media player domain * Move other Sonos services to media player domain * Address comments * Remove lock * Fix typos * Use make_entity_service_schema * Add area extraction to async_extract_entities Co-authored-by: Anders Melchiorsen --- homeassistant/components/sonos/__init__.py | 126 +--------- .../components/sonos/media_player.py | 224 +++++++++++------- homeassistant/helpers/entity_component.py | 40 +--- homeassistant/helpers/entity_platform.py | 34 ++- homeassistant/helpers/service.py | 26 +- tests/helpers/test_service.py | 178 ++++++++++---- 6 files changed, 339 insertions(+), 289 deletions(-) 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/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 404fd4ed46d..733cb22b3b2 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -6,17 +6,15 @@ import logging 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, + service, +) from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform @@ -166,39 +164,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) 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/service.py b/homeassistant/helpers/service.py index 16fabe251af..d621d4e6242 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -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. diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index b42b30a836a..c80b6eac193 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) @@ -678,3 +683,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", + ] From e53513301696fb31b01589a68607752fe689bff1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jan 2020 23:13:15 -0500 Subject: [PATCH 174/393] Fix options update during import config flow step for vizio component (Bugfix for #30653) (#30977) * fix options update logic during import * add missing tests * fix abort reasons and strings, add missing test * combine steps when testing esn already exists * readd removed test * no mock_coro_func needed * add block_until_done and assert entry options --- homeassistant/components/vizio/config_flow.py | 19 +-- homeassistant/components/vizio/strings.json | 8 +- tests/components/vizio/test_config_flow.py | 108 +++++++++++++++--- 3 files changed, 107 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index b02be9a5934..560b01df83a 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -111,7 +111,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if await self.async_set_unique_id( unique_id=unique_id, raise_on_progress=True ): - return self.async_abort(reason="already_setup") + 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 @@ -128,16 +130,19 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[ CONF_NAME ] == import_config.get(CONF_NAME): - new_options = {} + updated_options = {} if entry.data[CONF_VOLUME_STEP] != import_config[CONF_VOLUME_STEP]: - new_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] + updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] + + if updated_options: + new_data = entry.data.copy() + new_data.update(updated_options) + new_options = entry.options.copy() + new_options.update(updated_options) - if new_options: self.hass.config_entries.async_update_entry( - entry=entry, - data=entry.data.copy().update(new_options), - options=entry.options.copy().update(new_options), + entry=entry, data=new_data, options=new_options, ) return self.async_abort(reason="updated_options") diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 07bbfc666cf..a6367cb3c8f 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -13,16 +13,14 @@ } }, "error": { - "host_exists": "Host already configured.", - "name_exists": "Name already configured.", + "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_in_progress": "Config flow for vizio component already in progress.", "already_setup": "This entry has already been setup.", - "host_exists": "Vizio component with host already configured.", - "name_exists": "Vizio component with name already configured.", + "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_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." } }, diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index c8255b9f5fe..4dbb375c3fe 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.vizio.const import ( DOMAIN, VIZIO_SCHEMA, ) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_DEVICE_CLASS, @@ -27,7 +28,9 @@ 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" @@ -69,12 +72,27 @@ def vizio_connect_fixture(): ), patch( "homeassistant.components.vizio.config_flow.VizioAsync.get_unique_id", return_value=UNIQUE_ID, - ), patch( - "homeassistant.components.vizio.async_setup_entry", return_value=True ): 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_cant_connect") def vizio_cant_connect_fixture(): """Mock vizio device cant connect.""" @@ -85,11 +103,13 @@ def vizio_cant_connect_fixture(): yield -async def test_user_flow_minimum_fields(hass: HomeAssistantType, vizio_connect) -> None: +async def test_user_flow_minimum_fields( + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup +) -> None: """Test user config flow with minimum fields.""" # test form shows 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" @@ -110,11 +130,13 @@ async def test_user_flow_minimum_fields(hass: HomeAssistantType, vizio_connect) assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_SPEAKER -async def test_user_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None: +async def test_user_flow_all_fields( + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup +) -> None: """Test user config flow with all fields.""" # test form shows 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 @@ -162,7 +184,7 @@ async def test_options_flow(hass: HomeAssistantType) -> None: async def test_user_host_already_configured( - hass: HomeAssistantType, vizio_connect + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup ) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( @@ -175,7 +197,7 @@ async def test_user_host_already_configured( fail_entry[CONF_NAME] = "newtestname" 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 @@ -190,7 +212,7 @@ async def test_user_host_already_configured( async def test_user_name_already_configured( - hass: HomeAssistantType, vizio_connect + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup ) -> None: """Test name is already configured during user setup.""" entry = MockConfigEntry( @@ -204,7 +226,7 @@ async def test_user_name_already_configured( fail_entry[CONF_HOST] = "0.0.0.0" 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" @@ -217,12 +239,34 @@ async def test_user_name_already_configured( assert result["errors"] == {CONF_NAME: "name_exists"} +async def test_user_esn_already_exists( + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup +) -> 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 ) -> None: """Test with could_not_connect during user_setup.""" 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 @@ -236,11 +280,11 @@ async def test_user_error_on_could_not_connect( async def test_user_error_on_tv_needs_token( - hass: HomeAssistantType, vizio_connect + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup ) -> 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": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -255,7 +299,7 @@ async def test_user_error_on_tv_needs_token( async def test_import_flow_minimum_fields( - hass: HomeAssistantType, vizio_connect + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup ) -> None: """Test import config flow with minimum fields.""" result = await hass.config_entries.flow.async_init( @@ -274,7 +318,9 @@ async def test_import_flow_minimum_fields( assert result["data"][CONF_VOLUME_STEP] == DEFAULT_VOLUME_STEP -async def test_import_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> None: +async def test_import_flow_all_fields( + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup +) -> None: """Test import config flow with all fields.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -292,7 +338,7 @@ async def test_import_flow_all_fields(hass: HomeAssistantType, vizio_connect) -> async def test_import_entity_already_configured( - hass: HomeAssistantType, vizio_connect + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup ) -> None: """Test entity is already configured during import setup.""" entry = MockConfigEntry( @@ -309,3 +355,33 @@ async def test_import_entity_already_configured( 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, vizio_bypass_update +) -> 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_options" + assert ( + hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] + == VOLUME_STEP + 1 + ) From 5f31b48f1d43b0d9170a650d4dc2f74a62b476c0 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Mon, 20 Jan 2020 14:05:10 +0800 Subject: [PATCH 175/393] print component import error to logfile (#30346) --- homeassistant/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2b96bb3ea9d..f62228b28f5 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -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) From b2212ad44548bb63be5061463dde1ae1345254cd Mon Sep 17 00:00:00 2001 From: Faucogney Anthony Date: Mon, 20 Jan 2020 07:17:06 +0100 Subject: [PATCH 176/393] Add Derivative component (#26456) * create derivation component based on integration component remove left and right * Update test (was'n saved) * add some functionnal point test * Change derivation to derivative * Continue migration from derivation to derivative * Add codeowners info * fix tests typo and values * Improve code from reviewer add test case fix test values still a prefix issue that should not * create derivation component based on integration component remove left and right * Update test (was'n saved) * add some functionnal point test * Change derivation to derivative * Continue migration from derivation to derivative * Add codeowners info * fix tests typo and values * Improve code from reviewer add test case fix test values still a prefix issue that should not * Update homeassistant/components/derivative/sensor.py Fix test issue with unit of measurement Co-Authored-By: Santobert * Fix review Move ValueError to SyntaxError * precise state test * un comment original tests and remove error tests * Fix isort issue * Fix review - update doc link - migrate to general const import * Rollback import conf_unit, just defined localy Co-authored-by: Santobert --- CODEOWNERS | 1 + .../components/derivative/__init__.py | 1 + .../components/derivative/manifest.json | 10 + homeassistant/components/derivative/sensor.py | 177 +++++++++++ tests/components/derivative/__init__.py | 1 + tests/components/derivative/test_sensor.py | 291 ++++++++++++++++++ 6 files changed, 481 insertions(+) create mode 100644 homeassistant/components/derivative/__init__.py create mode 100644 homeassistant/components/derivative/manifest.json create mode 100644 homeassistant/components/derivative/sensor.py create mode 100644 tests/components/derivative/__init__.py create mode 100644 tests/components/derivative/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 52bd64dffed..18359beb5d0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -75,6 +75,7 @@ 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 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..177d1258f3c --- /dev/null +++ b/homeassistant/components/derivative/sensor.py @@ -0,0 +1,177 @@ +"""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" + +# SI Metric prefixes +UNIT_PREFIXES = {None: 1, "k": 10 ** 3, "G": 10 ** 6, "T": 10 ** 9} + +# SI Time prefixes +UNIT_TIME = {"s": 1, "min": 60, "h": 60 * 60, "d": 24 * 60 * 60} + +ICON = "mdi:chart-line" + +DEFAULT_ROUND = 3 + +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, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the derivative sensor.""" + derivative = DerivativeSensor( + config[CONF_SOURCE], + config.get(CONF_NAME), + config[CONF_ROUND_DIGITS], + config[CONF_UNIT_PREFIX], + config[CONF_UNIT_TIME], + config.get(CONF_UNIT), + ) + + 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, + ): + """Initialize the derivative sensor.""" + self._sensor_source_id = source_entity + self._round_digits = round_digits + self._state = 0 + + 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] + + 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 + + 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. + gradient = 0 + elapsed_time = ( + new_state.last_updated - old_state.last_updated + ).total_seconds() + gradient = Decimal(new_state.state) - Decimal(old_state.state) + derivative = gradient / ( + Decimal(elapsed_time) * (self._unit_prefix * 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/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..8893319ab36 --- /dev/null +++ b/tests/components/derivative/test_sensor.py @@ -0,0 +1,291 @@ +"""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 test_dataSet1(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "unit_time": "s", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, 30), (40, 5), (50, 0)]: + 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"]) == -0.5 + + +async def test_dataSet2(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "unit_time": "s", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 5), (30, 0)]: + 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"]) == -0.5 + + +async def test_dataSet3(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "unit_time": "s", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 5), (30, 10)]: + 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"]) == 0.5 + + assert state.attributes.get("unit_of_measurement") == "/s" + + +async def test_dataSet4(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "unit_time": "s", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 5), (30, 5)]: + 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"]) == 0 + + +async def test_dataSet5(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "unit_time": "s", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 10), (30, -10)]: + 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"]) == -2 + + +async def test_dataSet6(hass): + """Test derivative sensor state.""" + config = { + "sensor": { + "platform": "derivative", + "name": "power", + "source": "sensor.energy", + "round": 2, + } + } + + 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() + + # Testing a energy sensor with non-monotonic intervals and values + for time, value in [(20, 0), (30, 36000)]: + 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"]) == 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 From bb37f7bb7517cc48e03fa5b3053f4677a6b794a7 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 19 Jan 2020 23:06:35 -0800 Subject: [PATCH 177/393] Fix capability_attributes when supported_features is None (#30993) * Fix capability_attributes when supported_features is None (water_heater) * Fix capability_attributes when supported_features is None (media_player) --- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/water_heater/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 83c117d6c05..b73208402f8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -785,7 +785,7 @@ class MediaPlayerDevice(Entity): @property def capability_attributes(self): """Return capabilitiy attributes.""" - supported_features = self.supported_features + supported_features = self.supported_features or 0 data = {} if supported_features & SUPPORT_SELECT_SOURCE: diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 8da94ff1098..ecff3105ae0 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -146,7 +146,7 @@ class WaterHeaterDevice(Entity): @property def capability_attributes(self): """Return capabilitiy attributes.""" - supported_features = self.supported_features + supported_features = self.supported_features or 0 data = { ATTR_MIN_TEMP: show_temp( From 9993333bf91d6a9f093d6cc52fc0229b381b86cc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 20 Jan 2020 00:08:32 -0700 Subject: [PATCH 178/393] Add state reproduction to remotes (#30990) --- .../components/remote/reproduce_state.py | 61 +++++++++++++++++++ .../components/remote/test_reproduce_state.py | 52 ++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 homeassistant/components/remote/reproduce_state.py create mode 100644 tests/components/remote/test_reproduce_state.py 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/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"} From 053f18d6ce1fd6a9b541169c4c9521003c9e181c Mon Sep 17 00:00:00 2001 From: Ronald Dehuysser Date: Mon, 20 Jan 2020 09:51:59 +0100 Subject: [PATCH 179/393] Add attributes departure_minutes and delay_minutes to the nmbs sensor (#30958) * Improve sensor for automations I've updated the sensor so that departure time and delay can be used in automatons. Before, the departure time and delay time were only available wrapped in strings which makes it difficult to use them in automations. Using the extra attributes, one can easily use them in automations. * Update homeassistant/components/nmbs/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Updates based on review Changed min to minutes as requested Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/nmbs/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 35c928deb37..a91ff511b07 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -130,6 +130,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 +139,7 @@ class NMBSLiveBoard(Entity): if delay > 0: attrs["delay"] = f"{delay} minutes" + attrs["delay_minutes"] = delay return attrs @@ -200,6 +202,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 +227,7 @@ class NMBSSensor(Entity): if delay > 0: attrs["delay"] = f"{delay} minutes" + attrs["delay_minutes"] = delay return attrs From a634e62dfc0edf282d018b7fbc1dc1108aa28572 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Mon, 20 Jan 2020 10:43:20 +0100 Subject: [PATCH 180/393] Minor fixes for webostv (#30998) * restore emulation of play pause toggle * add protection to notify setup * add state check for power off to avoid switching tv back on --- .../components/webostv/media_player.py | 17 ++++++++++++++--- homeassistant/components/webostv/notify.py | 3 +++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index b7c8a416870..c523c068bcc 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -118,6 +118,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): self._customize = customize self._on_script = on_script + # Assume that the TV is not paused + self._paused = False + # Assume that the TV is not muted self._muted = False self._volume = 0 @@ -290,7 +293,10 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @cmd async def async_turn_off(self): """Turn off media player.""" - await self._client.power_off() + + # in some situations power_off may cause the TV to switch back on + if self._state != STATE_OFF: + await self._client.power_off() async def async_turn_on(self): """Turn on the media player.""" @@ -326,8 +332,11 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @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): @@ -379,11 +388,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 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) From a010577d6e3c94436d725cbbd8b02f605388b034 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 20 Jan 2020 14:24:13 +0100 Subject: [PATCH 181/393] Add kef supports_on option (#30937) * add supports_on_off option * modify support_kef inplace * all speakers support turning off remotely --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 32 +++++++++++++------- requirements_all.txt | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 949ec192564..d862efaa22b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 From 9b02ca96ba0a057e9dd7223af74548843492c978 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 20 Jan 2020 13:39:02 +0000 Subject: [PATCH 182/393] Small tweaks for evohome (#31007) --- homeassistant/components/evohome/climate.py | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 37c30ce4655..1c877b980df 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -135,20 +135,21 @@ class EvoZone(EvoChild, EvoClimateDevice): """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: @@ -206,9 +207,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: @@ -299,14 +298,17 @@ class EvoZone(EvoChild, EvoClimateDevice): class EvoController(EvoClimateDevice): - """Base for a Honeywell TCC 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 an evohome Controller (hub).""" + """Initialize a Honeywell TCC Controller/Location.""" super().__init__(evo_broker, evo_device) self._unique_id = evo_device.systemId @@ -314,7 +316,6 @@ class EvoController(EvoClimateDevice): self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._supported_features = SUPPORT_PRESET_MODE modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]] self._preset_modes = [ From d2b0031f55f03bdcc4fb5cf4ffe00949a062da80 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Jan 2020 16:26:06 +0100 Subject: [PATCH 183/393] Adds missing strings to Almond (#31013) --- homeassistant/components/almond/strings.json | 4 ++++ 1 file changed, 4 insertions(+) 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": { From f7a97dae2d5bc976424c0ae3a7c8cb5ec6baa54a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Jan 2020 16:26:44 +0100 Subject: [PATCH 184/393] Adds missing strings to Withings (#31012) --- homeassistant/components/withings/strings.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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." } } } From 6cf20fc7fa17051df3abbb2b7d7c45950e72cce7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Jan 2020 17:18:10 +0100 Subject: [PATCH 185/393] Fix deCONZ update entry from Hassio discovery (#31015) * Fix deCONZ update entry from Hassio discovery * Empty commit to re-trigger build --- homeassistant/components/deconz/config_flow.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 5a9ef232e61..43c6cee9193 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -216,15 +216,15 @@ 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]) - gateway = self.hass.data.get(DOMAIN, {}).get(self.bridge_id) - if gateway: - return self._update_entry( - gateway.config_entry, - user_input[CONF_HOST], - user_input[CONF_PORT], - user_input[CONF_API_KEY], - ) + 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._hassio_discovery = user_input From 5e2ba2eb77ceaa9f68bdcc9475e46c044c2eeaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 20 Jan 2020 18:44:55 +0200 Subject: [PATCH 186/393] Enable some more bandit checks (#30857) * Enable B108 (hardcoded tmp dir), address findings * Enable B602 (subprocess popen with shell), address findings * Enable B604 (start process with shell), address findings * Enable B306 (mktemp), B307 (eval), and B325 (tempnam), no issues to address --- .../components/command_line/cover.py | 6 +- .../components/command_line/notify.py | 5 +- .../components/command_line/sensor.py | 5 +- .../components/command_line/switch.py | 8 +- homeassistant/components/vicare/__init__.py | 3 +- homeassistant/components/x10/light.py | 2 +- homeassistant/components/yi/camera.py | 2 +- script/scaffold/__main__.py | 8 +- tests/bandit.yaml | 6 ++ tests/components/automation/test_litejet.py | 2 +- tests/components/camera/test_init.py | 2 +- tests/components/command_line/test_cover.py | 4 +- tests/components/fail2ban/test_sensor.py | 14 ++-- tests/components/litejet/test_light.py | 4 +- tests/components/litejet/test_scene.py | 4 +- tests/components/litejet/test_switch.py | 2 +- tests/components/minio/test_minio.py | 10 +-- .../components/tomato/test_device_tracker.py | 6 +- tests/test_config.py | 14 ++-- tests/test_core.py | 12 +-- tests/util/test_yaml.py | 82 +++++++++---------- 21 files changed, 110 insertions(+), 91 deletions(-) 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/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/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/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/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/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/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 75fbc03a589..294b15baf91 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -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/camera/test_init.py b/tests/components/camera/test_init.py index 7f5b2bd20b9..23c04fe347e 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -102,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/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/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/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/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/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/test_config.py b/tests/test_config.py index 8b6d8addb30..1fc92ee954b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -347,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 @@ -357,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 @@ -377,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) @@ -402,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 @@ -412,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 @@ -427,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", }, ) @@ -438,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_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_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] From 8c22858ae3ad3a1fab530d40a803d919803a9bc7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 20 Jan 2020 18:59:29 +0100 Subject: [PATCH 187/393] Use config_entry.unique_id in iCloud (#30984) * Use unique_id in iCloud * Remove missed self._configuration_exists() * Avoid breaking change * Almost fix tests * Add missing test * Fix tests --- .../components/icloud/.translations/en.json | 3 +- homeassistant/components/icloud/__init__.py | 4 + .../components/icloud/config_flow.py | 17 +-- homeassistant/components/icloud/strings.json | 3 +- tests/components/icloud/test_config_flow.py | 112 ++++++++++++------ 5 files changed, 85 insertions(+), 54 deletions(-) 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/__init__.py b/homeassistant/components/icloud/__init__.py index d1e00d65e10..bc1a7535882 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -141,6 +141,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool 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( diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 553ec1a28b4..9b00ccb2a8d 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -43,13 +43,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._trusted_device = None self._verification_code = None - def _configuration_exists(self, username: str) -> bool: - """Return True if username exists in configuration.""" - for entry in self._async_current_entries(): - if entry.data[CONF_USERNAME] == username: - return True - return False - async def _show_setup_form(self, user_input=None, errors=None): """Show the setup form to the user.""" @@ -90,9 +83,10 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD ) - if self._configuration_exists(self._username): - 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( @@ -119,9 +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]): - 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): 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/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 035266287f0..747af7c940a 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -5,7 +5,6 @@ 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, @@ -41,6 +40,9 @@ def mock_controller_service(): "homeassistant.components.icloud.config_flow.PyiCloudService" ) as service_mock: service_mock.return_value.requires_2fa = 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 @@ -63,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_2fa = True service_mock.return_value.trusted_devices = TRUSTED_DEVICES service_mock.return_value.send_verification_code = Mock(return_value=False) yield service_mock @@ -75,20 +77,13 @@ 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_2fa = 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.""" result = await hass.config_entries.flow.async_init( @@ -118,6 +113,7 @@ async def test_user_with_cookie( 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 @@ -162,6 +158,7 @@ async def test_import_with_cookie( 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 @@ -180,6 +177,7 @@ async def test_import_with_cookie( }, ) 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 @@ -192,7 +190,9 @@ async def test_two_accounts_setup( ): """Test to setup two accounts.""" 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) # import with username and password @@ -202,6 +202,7 @@ async def test_two_accounts_setup( 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 @@ -212,7 +213,9 @@ async def test_two_accounts_setup( async def test_abort_if_already_setup(hass: HomeAssistantType): """Test we abort if the account is already setup.""" 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) @@ -222,7 +225,7 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): 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 hass.config_entries.flow.async_init( @@ -230,8 +233,8 @@ async def test_abort_if_already_setup(hass: HomeAssistantType): 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): @@ -251,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 @@ -273,34 +284,53 @@ 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_2fa = 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 @@ -312,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"} From 1639432463e3ba3c5a754c2eaceabcfed5542a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 20 Jan 2020 19:09:20 +0100 Subject: [PATCH 188/393] Bump zigpy-zigate to 0.5.1 (#31004) * Bump zigpy-zigate to 0.5.1 Improve startup and channel setting --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e3d0eda3e02..b436f677f6b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.12.0", "zigpy-xbee-homeassistant==0.8.0", - "zigpy-zigate==0.5.0" + "zigpy-zigate==0.5.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index d862efaa22b..2e91a3656ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2124,7 +2124,7 @@ zigpy-homeassistant==0.12.0 zigpy-xbee-homeassistant==0.8.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_all.txt b/requirements_test_all.txt index d5863fbdee7..bfabcf0d7a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,4 +690,4 @@ zigpy-homeassistant==0.12.0 zigpy-xbee-homeassistant==0.8.0 # homeassistant.components.zha -zigpy-zigate==0.5.0 +zigpy-zigate==0.5.1 From 662c12715e42f56cc918c19ed250cae01e12e1f0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 20 Jan 2020 13:33:58 -0700 Subject: [PATCH 189/393] Allow OpenUV entities to be unavailable (#31018) * Allow OpenUV entities to be unavailable * Empty commit to re-trigger build --- homeassistant/components/openuv/__init__.py | 6 ++++++ homeassistant/components/openuv/binary_sensor.py | 3 +++ homeassistant/components/openuv/sensor.py | 9 ++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 167fcdcd0e6..2e4e89a00e6 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -289,9 +289,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..aa489647e25 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -100,8 +100,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/sensor.py b/homeassistant/components/openuv/sensor.py index a482464e4d0..9b57687d4c2 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -124,7 +124,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: From 8f37c843f5af9751c0a7e9791c95319fa6bfa775 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 20 Jan 2020 20:49:04 +0000 Subject: [PATCH 190/393] Handle ghost zones gracefully (#31008) --- homeassistant/components/evohome/climate.py | 27 ++++++++++++------- .../components/evohome/water_heater.py | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 1c877b980df..46a4fbf335c 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -97,16 +97,25 @@ async def async_setup_platform( 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, - ) - new_entity = EvoZone(broker, zone) + if zone.zoneType == "Unknown": + _LOGGER.warning( + "Ignoring: %s (%s), id=%s, name=%s: invalid zone type", + zone.zoneType, + zone.modelType, + zone.zoneId, + zone.name, + ) + else: + _LOGGER.debug( + "Adding: %s (%s), id=%s, name=%s", + zone.zoneType, + zone.modelType, + zone.zoneId, + zone.name, + ) - zones.append(new_entity) + new_entity = EvoZone(broker, zone) + zones.append(new_entity) async_add_entities([controller] + zones, update_before_add=True) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index cd4fb2aadce..cc282534f1b 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,7 +34,7 @@ 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, ) From 692e4f27c408ff6f1d6747a41ad6301afa2f54c0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 21 Jan 2020 00:33:20 +0000 Subject: [PATCH 191/393] [ci skip] Translation update --- .../components/almond/.translations/da.json | 4 ++++ .../components/almond/.translations/en.json | 4 ++++ .../components/almond/.translations/fr.json | 4 ++++ .../components/almond/.translations/ru.json | 4 ++++ .../components/almond/.translations/zh-Hant.json | 4 ++++ .../components/deconz/.translations/ru.json | 2 +- homeassistant/components/hue/.translations/cs.json | 2 +- homeassistant/components/hue/.translations/es.json | 2 +- homeassistant/components/hue/.translations/fr.json | 2 +- homeassistant/components/hue/.translations/id.json | 2 +- homeassistant/components/hue/.translations/nl.json | 2 +- homeassistant/components/hue/.translations/nn.json | 2 +- .../components/hue/.translations/pt-BR.json | 2 +- homeassistant/components/hue/.translations/pt.json | 2 +- homeassistant/components/hue/.translations/ro.json | 2 +- homeassistant/components/hue/.translations/sl.json | 2 +- homeassistant/components/hue/.translations/sv.json | 2 +- .../components/icloud/.translations/ca.json | 2 +- .../components/icloud/.translations/da.json | 2 +- .../components/icloud/.translations/de.json | 2 +- .../components/icloud/.translations/en.json | 1 + .../components/icloud/.translations/es.json | 2 +- .../components/icloud/.translations/fr.json | 2 +- .../components/icloud/.translations/it.json | 2 +- .../components/icloud/.translations/ko.json | 2 +- .../components/icloud/.translations/lb.json | 2 +- .../components/icloud/.translations/nl.json | 2 +- .../components/icloud/.translations/no.json | 2 +- .../components/icloud/.translations/pl.json | 2 +- .../components/icloud/.translations/pt-BR.json | 2 +- .../components/icloud/.translations/ru.json | 2 +- .../components/icloud/.translations/sl.json | 2 +- .../components/icloud/.translations/zh-Hant.json | 2 +- .../components/netatmo/.translations/ru.json | 2 +- .../components/transmission/.translations/ru.json | 6 +++--- homeassistant/components/vizio/.translations/da.json | 1 + homeassistant/components/vizio/.translations/en.json | 5 +++-- homeassistant/components/vizio/.translations/fr.json | 1 + homeassistant/components/vizio/.translations/no.json | 3 +++ homeassistant/components/vizio/.translations/ru.json | 12 +++++++++--- .../components/vizio/.translations/zh-Hant.json | 5 +++-- .../components/withings/.translations/da.json | 5 +++++ .../components/withings/.translations/en.json | 7 ++++++- .../components/withings/.translations/ru.json | 5 +++++ .../components/withings/.translations/zh-Hant.json | 7 ++++++- 45 files changed, 94 insertions(+), 40 deletions(-) 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/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/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/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/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/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/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/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json index 30e6c50b81b..0ce7ca3eff3 100644 --- a/homeassistant/components/icloud/.translations/ca.json +++ b/homeassistant/components/icloud/.translations/ca.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json index 1a06bd8e0f2..215dc3df90f 100644 --- a/homeassistant/components/icloud/.translations/da.json +++ b/homeassistant/components/icloud/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "username_exists": "Kontoen er allerede konfigureret" + "already_configured": "Kontoen er allerede konfigureret" }, "error": { "login": "Loginfejl: Kontroller din email og adgangskode", diff --git a/homeassistant/components/icloud/.translations/de.json b/homeassistant/components/icloud/.translations/de.json index a9be5a16dce..5fcde349d5a 100644 --- a/homeassistant/components/icloud/.translations/de.json +++ b/homeassistant/components/icloud/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "username_exists": "Konto bereits konfiguriert" + "already_configured": "Konto bereits konfiguriert" }, "error": { "login": "Login-Fehler: Bitte \u00fcberpr\u00fcfe deine E-Mail & Passwort", diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json index 3b7da70bcaf..78f372d0080 100644 --- a/homeassistant/components/icloud/.translations/en.json +++ b/homeassistant/components/icloud/.translations/en.json @@ -6,6 +6,7 @@ "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..5f5901c753f 100644 --- a/homeassistant/components/icloud/.translations/es.json +++ b/homeassistant/components/icloud/.translations/es.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json index 81996d908a6..7c80dd217db 100644 --- a/homeassistant/components/icloud/.translations/fr.json +++ b/homeassistant/components/icloud/.translations/fr.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json index 0a986f1fe77..3c1c7ecf3c2 100644 --- a/homeassistant/components/icloud/.translations/it.json +++ b/homeassistant/components/icloud/.translations/it.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json index a689a895278..0bf20de2d19 100644 --- a/homeassistant/components/icloud/.translations/ko.json +++ b/homeassistant/components/icloud/.translations/ko.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json index eaeb300f7a8..41dc4457f0d 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/nl.json b/homeassistant/components/icloud/.translations/nl.json index d35496b171b..7ee80c39680 100644 --- a/homeassistant/components/icloud/.translations/nl.json +++ b/homeassistant/components/icloud/.translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "username_exists": "Account reeds geconfigureerd" + "already_configured": "Account reeds geconfigureerd" }, "error": { "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json index a582b916310..51eb45ec1ee 100644 --- a/homeassistant/components/icloud/.translations/no.json +++ b/homeassistant/components/icloud/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "username_exists": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert" }, "error": { "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json index f154f77f186..244450234c3 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/pt-BR.json b/homeassistant/components/icloud/.translations/pt-BR.json index c8bfe0e0a6d..1a62aeda6af 100644 --- a/homeassistant/components/icloud/.translations/pt-BR.json +++ b/homeassistant/components/icloud/.translations/pt-BR.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json index 000edd71e00..85cb395a5bb 100644 --- a/homeassistant/components/icloud/.translations/ru.json +++ b/homeassistant/components/icloud/.translations/ru.json @@ -1,7 +1,7 @@ { "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.", diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json index 91cb4312cb3..96dc743396d 100644 --- a/homeassistant/components/icloud/.translations/sl.json +++ b/homeassistant/components/icloud/.translations/sl.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/icloud/.translations/zh-Hant.json b/homeassistant/components/icloud/.translations/zh-Hant.json index 80d8ba1485b..63a8091dd53 100644 --- a/homeassistant/components/icloud/.translations/zh-Hant.json +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "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", diff --git a/homeassistant/components/netatmo/.translations/ru.json b/homeassistant/components/netatmo/.translations/ru.json index ba213fff2a6..c34fb331ceb 100644 --- a/homeassistant/components/netatmo/.translations/ru.json +++ b/homeassistant/components/netatmo/.translations/ru.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Netatmo" + "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" 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/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index 62da3d0184d..dc5de132cc2 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -3,6 +3,7 @@ "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.", diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 4db4c35894e..60fd9049bb3 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -3,6 +3,7 @@ "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.", @@ -10,8 +11,8 @@ }, "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": "Host already configured.", - "name_exists": "Name already configured.", + "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": { diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index 9ec2abe56a1..78d2347bfac 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -3,6 +3,7 @@ "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.", diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index 30e6fc9af6f..c0f5950613a 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -3,8 +3,10 @@ "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": { @@ -30,6 +32,7 @@ "step": { "init": { "data": { + "timeout": "Tidsavbrudd for API-foresp\u00f8rsel (sekunder)", "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" }, "title": "Oppdater Vizo SmartCast alternativer" diff --git a/homeassistant/components/vizio/.translations/ru.json b/homeassistant/components/vizio/.translations/ru.json index 24e8411b438..2206336a5b4 100644 --- a/homeassistant/components/vizio/.translations/ru.json +++ b/homeassistant/components/vizio/.translations/ru.json @@ -3,8 +3,11 @@ "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." + "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.", @@ -29,9 +32,12 @@ "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/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index b6951080f4a..6707a321911 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -3,6 +3,7 @@ "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", @@ -10,8 +11,8 @@ }, "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": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "name_exists": "\u540d\u7a31\u5df2\u8a2d\u5b9a\u5b8c\u6210\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": { diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index e4599fe8ec2..7b51cec402d 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." }, "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + }, "profile": { "data": { "profile": "Profile" 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/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/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" From 0b72587af2a891ddfe4e95e14edd8796b3757b2c Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Tue, 21 Jan 2020 06:11:23 +0100 Subject: [PATCH 192/393] Upgrade importlib-metadata to version 1.4.0 (#31027) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b6e36d330f1..d9785e1423f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 home-assistant-frontend==20200108.2 -importlib-metadata==1.3.0 +importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2e91a3656ec..f8262e3b0cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ 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 diff --git a/setup.py b/setup.py index 35594c21507..6ffaa2c195c 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. From bc6603d8d704073c17a205df0221a5b95cd19b13 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Tue, 21 Jan 2020 06:12:16 +0100 Subject: [PATCH 193/393] Upgrade pyyaml to 5.3 (#31026) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d9785e1423f..679af5bf3f4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2 +pyyaml==5.3 requests==2.22.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.12 diff --git a/requirements_all.txt b/requirements_all.txt index f8262e3b0cc..13fb83247c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,7 +12,7 @@ cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.2 +pyyaml==5.3 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 diff --git a/setup.py b/setup.py index 6ffaa2c195c..521b9f2678c 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.2", + "pyyaml==5.3", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", From 41014d73be52c8df1c0898c70fa43ee44b8b7fca Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 21 Jan 2020 06:07:47 -0500 Subject: [PATCH 194/393] Allow ZHA device creation for the Zigbee coordinator (#31032) * allow zha device creation for coordinator * don't let coordinator get removed * fix truthy issue in logical device type --- homeassistant/components/zha/api.py | 5 +++++ homeassistant/components/zha/core/device.py | 6 ++++-- homeassistant/components/zha/core/gateway.py | 8 -------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 7c732b6906e..5fb7f6d8fdb 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -906,6 +906,11 @@ def async_load_api(hass): async def remove(service): """Remove a node from the network.""" ieee = service.data.get(ATTR_IEEE_ADDRESS) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_device = zha_gateway.get_device(ieee) + if 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) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 5c3b3578c12..3ed44a8f2aa 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -165,8 +165,10 @@ class ZHADevice(LogMixin): @property def device_type(self): """Return the logical device type for the device.""" - device_type = self._zigpy_device.node_desc.logical_type - return device_type.name if device_type else UNKNOWN + 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): diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 72931c665ee..72b5aa87329 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -135,8 +135,6 @@ 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) ) @@ -160,9 +158,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, @@ -336,9 +331,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( From fb35d382e1e1cc8be5a167e1f3834c0d48273b5a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Jan 2020 12:38:38 +0100 Subject: [PATCH 195/393] Remove all empty *_setup_platform() from integrations (#31025) * Remove all empty *_setup_platform() from integrations * Fix tests for smartthings * Fix tests for heos --- homeassistant/components/abode/alarm_control_panel.py | 5 ----- homeassistant/components/abode/binary_sensor.py | 5 ----- homeassistant/components/abode/camera.py | 5 ----- homeassistant/components/abode/cover.py | 5 ----- homeassistant/components/abode/light.py | 5 ----- homeassistant/components/abode/lock.py | 5 ----- homeassistant/components/abode/sensor.py | 5 ----- homeassistant/components/abode/switch.py | 5 ----- homeassistant/components/ambient_station/binary_sensor.py | 5 ----- homeassistant/components/ambient_station/sensor.py | 5 ----- homeassistant/components/ecobee/binary_sensor.py | 5 ----- homeassistant/components/ecobee/climate.py | 5 ----- homeassistant/components/ecobee/sensor.py | 5 ----- homeassistant/components/ecobee/weather.py | 5 ----- homeassistant/components/glances/sensor.py | 5 ----- homeassistant/components/heos/media_player.py | 5 ----- homeassistant/components/homekit_controller/air_quality.py | 5 ----- .../components/homekit_controller/alarm_control_panel.py | 5 ----- .../components/homekit_controller/binary_sensor.py | 5 ----- homeassistant/components/homekit_controller/climate.py | 5 ----- homeassistant/components/homekit_controller/cover.py | 5 ----- homeassistant/components/homekit_controller/fan.py | 5 ----- homeassistant/components/homekit_controller/light.py | 5 ----- homeassistant/components/homekit_controller/lock.py | 5 ----- homeassistant/components/homekit_controller/sensor.py | 5 ----- homeassistant/components/homekit_controller/switch.py | 5 ----- homeassistant/components/iqvia/sensor.py | 5 ----- homeassistant/components/linky/sensor.py | 5 ----- homeassistant/components/luftdaten/sensor.py | 5 ----- homeassistant/components/neato/camera.py | 5 ----- homeassistant/components/neato/sensor.py | 5 ----- homeassistant/components/neato/switch.py | 5 ----- homeassistant/components/neato/vacuum.py | 5 ----- homeassistant/components/netatmo/binary_sensor.py | 5 ----- homeassistant/components/openuv/binary_sensor.py | 5 ----- homeassistant/components/openuv/sensor.py | 5 ----- homeassistant/components/ps4/media_player.py | 5 ----- homeassistant/components/rainmachine/binary_sensor.py | 5 ----- homeassistant/components/rainmachine/sensor.py | 5 ----- homeassistant/components/rainmachine/switch.py | 5 ----- homeassistant/components/simplisafe/alarm_control_panel.py | 5 ----- homeassistant/components/smartthings/binary_sensor.py | 5 ----- homeassistant/components/smartthings/climate.py | 5 ----- homeassistant/components/smartthings/cover.py | 5 ----- homeassistant/components/smartthings/fan.py | 5 ----- homeassistant/components/smartthings/light.py | 5 ----- homeassistant/components/smartthings/lock.py | 5 ----- homeassistant/components/smartthings/scene.py | 5 ----- homeassistant/components/smartthings/sensor.py | 5 ----- homeassistant/components/smartthings/switch.py | 5 ----- homeassistant/components/solaredge/sensor.py | 5 ----- homeassistant/components/tesla/binary_sensor.py | 5 ----- homeassistant/components/tesla/climate.py | 5 ----- homeassistant/components/tesla/lock.py | 5 ----- homeassistant/components/tesla/sensor.py | 5 ----- homeassistant/components/tesla/switch.py | 5 ----- homeassistant/components/transmission/sensor.py | 5 ----- homeassistant/components/transmission/switch.py | 5 ----- homeassistant/components/velbus/binary_sensor.py | 5 ----- homeassistant/components/velbus/climate.py | 5 ----- homeassistant/components/velbus/cover.py | 5 ----- homeassistant/components/velbus/light.py | 5 ----- homeassistant/components/velbus/sensor.py | 5 ----- homeassistant/components/velbus/switch.py | 5 ----- homeassistant/components/zha/binary_sensor.py | 5 ----- homeassistant/components/zha/cover.py | 5 ----- homeassistant/components/zha/fan.py | 5 ----- homeassistant/components/zha/light.py | 5 ----- homeassistant/components/zha/lock.py | 5 ----- homeassistant/components/zha/sensor.py | 5 ----- homeassistant/components/zha/switch.py | 5 ----- homeassistant/components/zwave/binary_sensor.py | 5 ----- homeassistant/components/zwave/climate.py | 5 ----- homeassistant/components/zwave/cover.py | 5 ----- homeassistant/components/zwave/fan.py | 5 ----- homeassistant/components/zwave/light.py | 5 ----- homeassistant/components/zwave/lock.py | 5 ----- homeassistant/components/zwave/sensor.py | 5 ----- homeassistant/components/zwave/switch.py | 5 ----- tests/components/heos/test_media_player.py | 5 ----- tests/components/smartthings/test_binary_sensor.py | 5 ----- tests/components/smartthings/test_climate.py | 5 ----- tests/components/smartthings/test_cover.py | 6 ------ tests/components/smartthings/test_fan.py | 6 ------ tests/components/smartthings/test_light.py | 6 ------ tests/components/smartthings/test_lock.py | 6 ------ tests/components/smartthings/test_scene.py | 6 ------ tests/components/smartthings/test_sensor.py | 5 ----- tests/components/smartthings/test_switch.py | 6 ------ 89 files changed, 451 deletions(-) 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/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/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/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/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/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/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 854c12e6f88..0419c0354e6 100644 --- a/homeassistant/components/homekit_controller/air_quality.py +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -78,11 +78,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 8cdbe9b2f36..800c988279a 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -40,11 +40,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 2998ce18641..9fd93cf732a 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -93,11 +93,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index d0ab7bd2e99..ff234f566c7 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -45,11 +45,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7e5591d9505..dec94771b03 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -36,11 +36,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index efb41808429..a6c4ae769e2 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -230,11 +230,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index fe2a0e9bc97..9ce262291b3 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -18,11 +18,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 53f7bb5dfd5..5183a636f0f 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -17,11 +17,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index f91dae26ba0..0e3680db346 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -241,11 +241,6 @@ 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"] diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 7eedda1b191..6b71b15daff 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -12,11 +12,6 @@ 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"] 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/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/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/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 92e1539da4f..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 = [] diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index d420fbb1783..6d0de6dcceb 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -76,11 +76,6 @@ async def async_setup_entry(hass, entry, async_add_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 access to Netatmo binary sensor.""" - pass - - class NetatmoBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Camera device.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index aa489647e25..2790bc7ede0 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -23,11 +23,6 @@ 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 - - async def async_setup_entry(hass, entry, async_add_entities): """Set up an OpenUV sensor based on a config entry.""" openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 9b57687d4c2..00954646708 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -44,11 +44,6 @@ 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 - - async def async_setup_entry(hass, entry, async_add_entities): """Set up a Nest sensor based on a config entry.""" openuv = hass.data[DOMAIN][DATA_OPENUV_CLIENT][entry.entry_id] 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/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 1fe98482211..2d7ab613554 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -28,11 +28,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine binary sensors based on the old way.""" - pass - - 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] diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 399e86b7db1..bc1c734b98e 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -22,11 +22,6 @@ from . import ( _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine sensors based on the old way.""" - pass - - 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] diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 36c5eefb3d6..8da2cc4ee45 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -95,11 +95,6 @@ VEGETATION_MAP = { } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up RainMachine switches sensor based on the old way.""" - pass - - async def async_setup_entry(hass, entry, async_add_entities): """Set up RainMachine switches based on a config entry.""" rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 2cb6c4b41c5..37aa2d84585 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -57,11 +57,6 @@ VOLUME_STRING_MAP = { } -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 - - async def async_setup_entry(hass, entry, async_add_entities): """Set up a SimpliSafe alarm control panel based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] 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/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/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 8f610d960b3..3664cf6252d 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( 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/sensor.py b/homeassistant/components/tesla/sensor.py index a282f65f9e1..363cdc742d3 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"] 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/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/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..3e7df39b333 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.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 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"] 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/sensor.py b/homeassistant/components/velbus/sensor.py index 8af5df9e165..7ebdda2d781 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -7,11 +7,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 sensor based on config_entry.""" cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] 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/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index d8bc1187be8..d25410a0667 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.""" diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index ef410308eb1..5b83b8cefcb 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -26,11 +26,6 @@ SCAN_INTERVAL = timedelta(minutes=60) 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 covers.""" - pass - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation cover from config entry.""" diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index f489447e530..50e9f63a067 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.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index eb7d3297b43..11fa87d4618 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.""" diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index bf82252246c..584df99fe08 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.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index bb764ab406d..52d4660a467 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.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index cbd29925f62..a68fca76af4 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.""" 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..9c582eba89a 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.""" 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/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/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/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 From c2df4f56a3eb7ff54fa25ccc73174331b2527b57 Mon Sep 17 00:00:00 2001 From: James Nimmo Date: Wed, 22 Jan 2020 04:57:27 +1300 Subject: [PATCH 196/393] Bump pyintesishome to v1.6 (#31044) --- homeassistant/components/intesishome/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 13fb83247c0..60a34a3c22f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1294,7 +1294,7 @@ pyialarm==0.3 pyicloud==0.9.1 # homeassistant.components.intesishome -pyintesishome==1.5 +pyintesishome==1.6 # homeassistant.components.ipma pyipma==1.2.1 From 2aff913d9b6198e232c399cd48bfafed81f099a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 21 Jan 2020 16:04:22 +0000 Subject: [PATCH 197/393] Update pyipma to 2.0 (#30746) * update ipma component for pyipma 2.0 * fix wind speed; refactor forecast * update requirements*.txt * fix tests; update CODEOWNERS; update pyipma to 2.0.1 * minor changes as suggested in PR * make lint happy * fix mocking coroutines * restore old unique id * fix station lat/lon; update pyipma version --- CODEOWNERS | 2 +- homeassistant/components/ipma/manifest.json | 4 +- homeassistant/components/ipma/weather.py | 132 ++++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipma/test_weather.py | 106 ++++++++++------ 6 files changed, 138 insertions(+), 110 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 18359beb5d0..9636f324769 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,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 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/requirements_all.txt b/requirements_all.txt index 60a34a3c22f..f0e1848c76e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1297,7 +1297,7 @@ pyicloud==0.9.1 pyintesishome==1.6 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfabcf0d7a1..594e8622e70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ pyhomematic==0.1.63 pyicloud==0.9.1 # homeassistant.components.ipma -pyipma==1.2.1 +pyipma==2.0.2 # homeassistant.components.iqvia pyiqvia==0.2.1 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) From 27c25b6f441f3a5207e3b39df116df5548ffc360 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Wed, 22 Jan 2020 00:06:17 +0800 Subject: [PATCH 198/393] Add xiaomi miio sensors cgllc.airmonitor.s1 & cgllc.airmonitor.b1 (#30345) * support xiaomi miio sensor: cgllc.airmonitor.s1 & cgllc.airmonitor.b1 * support xiaomi miio sensor: cgllc.airmonitor.s1 & cgllc.airmonitor.b1 * rollback sensor, modify air_quality * only set air_quality props * remove set state from "zhimi.airmonitor.v1" * unit_of_measurement return None when the pm25 isn't reported * import libs by isort --- .../components/xiaomi_miio/air_quality.py | 92 ++++++++++++++++--- homeassistant/components/xiaomi_miio/const.py | 5 + 2 files changed, 86 insertions(+), 11 deletions(-) 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" From 19d30f0a1ba4ac4d382a8d24146600283b0e9ef7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Jan 2020 18:38:50 +0100 Subject: [PATCH 199/393] Update issue and PR templates (#31056) * Update issue and PR templates * Empty commit to re-trigger build --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 53 ++++++++++++++++ .github/ISSUE_TEMPLATE/Bug_report.md | 52 --------------- .github/ISSUE_TEMPLATE/config.yml | 17 +++++ .github/PULL_REQUEST_TEMPLATE.md | 94 ++++++++++++++++++++++------ 4 files changed, 145 insertions(+), 71 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.md delete mode 100644 .github/ISSUE_TEMPLATE/Bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md new file mode 100644 index 00000000000..f326139ba4a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -0,0 +1,53 @@ +--- +name: Bug report +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..19dd772c4e6 --- /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 backed. Please report issues with the UI in the frontend repository. + - name: Report incorrect/missing information on our website + urls: 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..4951ae1a0c0 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,35 +1,91 @@ -## 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 + + +- [ ] Bugfix (non-breaking change which fixes an issue) +- [ ] New integration (thank you!) +- [ ] New feature (which adds functionality to an existing integration) +- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) +- [ ] Code quality improvements to existing code or addition of tests + +## Example entry for `configuration.yaml`: + + ```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 in [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. 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`. + +The integration reached or maintains the following Integration 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 From e00388eea0ccc7256a29de7d685dbdfbfae57e4b Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 21 Jan 2020 18:49:15 +0000 Subject: [PATCH 200/393] switch evohome to use a whitelist for valid zonetype (#31047) --- homeassistant/components/evohome/__init__.py | 3 ++- homeassistant/components/evohome/climate.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index b9d3f35964a..949471d64d0 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -663,7 +663,8 @@ 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 diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 46a4fbf335c..b7f6e965a8f 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -97,15 +97,7 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): - if zone.zoneType == "Unknown": - _LOGGER.warning( - "Ignoring: %s (%s), id=%s, name=%s: invalid zone type", - zone.zoneType, - zone.modelType, - zone.zoneId, - zone.name, - ) - else: + if zone.modelType == "HeatingZone" or zone.zoneType == "Thermostat": _LOGGER.debug( "Adding: %s (%s), id=%s, name=%s", zone.zoneType, @@ -117,6 +109,16 @@ async def async_setup_platform( 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) From e8e7e66e2bcd18e61ba0c84f47c05fa974dfd640 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Jan 2020 21:44:39 +0100 Subject: [PATCH 201/393] Bump pytest to 5.3.4 (#31045) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f4fb13a417c..030e3dc60ce 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,6 +14,6 @@ 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 From ff7d2ac681ab085f74ca167d8e07f525d3287e60 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 21 Jan 2020 23:24:01 +0100 Subject: [PATCH 202/393] Iteration on issue and PR templates (#31058) * Iteration on issue and PR templates * Some small improvements to make it look better in the PR editor * Better English * Please keep template * Consistency * Adds www to website link, to make more clear it is our website * Process review suggestion by Martin * Added dep bump as type of change * English --- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 64 ++++++++++++++++++---------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index f326139ba4a..34c023c4410 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -1,5 +1,5 @@ --- -name: Bug report +name: Report a bug with Home Assistant about: Report an issue with Home Assistant --- ## Breaking change ## 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 or feature that would cause existing functionality not to work as expected) +- [ ] 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 ``` ## Additional information - This PR fixes or closes issue: fixes # @@ -54,10 +61,10 @@ ## Checklist - [ ] The code change is tested and works locally. @@ -69,22 +76,33 @@ If user exposed functionality or configuration variables are added/changed: -- [ ] Documentation added/updated in [home-assistant.io][docs-repository] +- [ ] 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`. +- [ ] 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: - +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 From 3b4c3e6e176a5634acdec710c3980e755e7afa36 Mon Sep 17 00:00:00 2001 From: Joe Gross Date: Tue, 21 Jan 2020 16:03:42 -0800 Subject: [PATCH 203/393] Fix prometheus component to fully sanitize Unicode characters (#31037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix prometheus component to fully santize Unicode characters. Sanitization used isdigit() to match numerics but that also matches Unicode "digit" characters that require special handling, like superscripts. Failures would look like this: ValueError: Invalid metric name: sensor_unit_u0x23_per_m³ Changed sanitize to use string.digits, which matches only ascii digits, and added test. See https://stackoverflow.com/questions/44891070/whats-the-difference-between-str-isdigit-isnumeric-and-isdecimal-in-python for discussion. https://docs.python.org/3/library/stdtypes.html#str.isdigit https://docs.python.org/3.7/library/string.html#string.digits * fix formatting to comply with black --- homeassistant/components/prometheus/__init__.py | 5 ++++- tests/components/prometheus/test_init.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) 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/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 + ) From 15f6e28a049dd3c377a143bbf3b6fc29d91b645a Mon Sep 17 00:00:00 2001 From: Jens Date: Wed, 22 Jan 2020 01:09:59 +0100 Subject: [PATCH 204/393] Add tahoma io:AwningValanceIOComponent (#30944) --- homeassistant/components/tahoma/__init__.py | 5 +++-- homeassistant/components/tahoma/cover.py | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) 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") From c3888330c1ab22b846dc7bfcd810c483db839e3e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 22 Jan 2020 01:28:19 +0100 Subject: [PATCH 205/393] Fix fan_modes in tuya climate (#30942) --- homeassistant/components/tuya/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.""" From 7e604bed7df21ea6cd5027ea1c041a030cb4a21d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 22 Jan 2020 00:31:43 +0000 Subject: [PATCH 206/393] [ci skip] Translation update --- .../components/almond/.translations/ca.json | 3 ++ .../components/almond/.translations/no.json | 4 ++ .../components/almond/.translations/pl.json | 4 ++ .../components/icloud/.translations/ca.json | 1 - .../components/icloud/.translations/da.json | 1 - .../components/icloud/.translations/de.json | 1 - .../components/icloud/.translations/en.json | 1 - .../components/icloud/.translations/es.json | 1 - .../components/icloud/.translations/fr.json | 1 - .../components/icloud/.translations/it.json | 1 - .../components/icloud/.translations/ko.json | 1 - .../components/icloud/.translations/lb.json | 1 - .../components/icloud/.translations/nl.json | 1 - .../components/icloud/.translations/no.json | 1 - .../components/icloud/.translations/pl.json | 1 - .../icloud/.translations/pt-BR.json | 1 - .../components/icloud/.translations/ru.json | 1 - .../components/icloud/.translations/sl.json | 1 - .../icloud/.translations/zh-Hant.json | 1 - .../components/vizio/.translations/it.json | 11 ++++++ .../components/vizio/.translations/no.json | 4 +- .../components/vizio/.translations/pl.json | 38 +++++++++++++++++-- .../components/withings/.translations/ca.json | 3 ++ .../components/withings/.translations/it.json | 4 ++ .../components/withings/.translations/no.json | 7 +++- .../components/withings/.translations/pl.json | 5 +++ 26 files changed, 77 insertions(+), 22 deletions(-) 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/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..201905255a7 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, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon} ?", + "title": "Almond przez dodatek Hass.io" + }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" } diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json index 0ce7ca3eff3..33fd399d33e 100644 --- a/homeassistant/components/icloud/.translations/ca.json +++ b/homeassistant/components/icloud/.translations/ca.json @@ -6,7 +6,6 @@ "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 215dc3df90f..e60b5120a83 100644 --- a/homeassistant/components/icloud/.translations/da.json +++ b/homeassistant/components/icloud/.translations/da.json @@ -6,7 +6,6 @@ "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 5fcde349d5a..c31f648a4ad 100644 --- a/homeassistant/components/icloud/.translations/de.json +++ b/homeassistant/components/icloud/.translations/de.json @@ -6,7 +6,6 @@ "error": { "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\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut" }, "step": { diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json index 78f372d0080..3b7da70bcaf 100644 --- a/homeassistant/components/icloud/.translations/en.json +++ b/homeassistant/components/icloud/.translations/en.json @@ -6,7 +6,6 @@ "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 5f5901c753f..7a0d4b66047 100644 --- a/homeassistant/components/icloud/.translations/es.json +++ b/homeassistant/components/icloud/.translations/es.json @@ -6,7 +6,6 @@ "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 7c80dd217db..91cff9912b6 100644 --- a/homeassistant/components/icloud/.translations/fr.json +++ b/homeassistant/components/icloud/.translations/fr.json @@ -6,7 +6,6 @@ "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 3c1c7ecf3c2..9d93a07565f 100644 --- a/homeassistant/components/icloud/.translations/it.json +++ b/homeassistant/components/icloud/.translations/it.json @@ -6,7 +6,6 @@ "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 0bf20de2d19..10df5c4519c 100644 --- a/homeassistant/components/icloud/.translations/ko.json +++ b/homeassistant/components/icloud/.translations/ko.json @@ -6,7 +6,6 @@ "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 41dc4457f0d..f90ec545c39 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -6,7 +6,6 @@ "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 7ee80c39680..fe0e7d07572 100644 --- a/homeassistant/components/icloud/.translations/nl.json +++ b/homeassistant/components/icloud/.translations/nl.json @@ -6,7 +6,6 @@ "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 51eb45ec1ee..589c220ec9c 100644 --- a/homeassistant/components/icloud/.translations/no.json +++ b/homeassistant/components/icloud/.translations/no.json @@ -6,7 +6,6 @@ "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 244450234c3..169fe2eac2d 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -6,7 +6,6 @@ "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 1a62aeda6af..4e45568ae68 100644 --- a/homeassistant/components/icloud/.translations/pt-BR.json +++ b/homeassistant/components/icloud/.translations/pt-BR.json @@ -6,7 +6,6 @@ "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 85cb395a5bb..b3a9578ad1e 100644 --- a/homeassistant/components/icloud/.translations/ru.json +++ b/homeassistant/components/icloud/.translations/ru.json @@ -6,7 +6,6 @@ "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 96dc743396d..14d6168409c 100644 --- a/homeassistant/components/icloud/.translations/sl.json +++ b/homeassistant/components/icloud/.translations/sl.json @@ -6,7 +6,6 @@ "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 63a8091dd53..a3f4e68e167 100644 --- a/homeassistant/components/icloud/.translations/zh-Hant.json +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -6,7 +6,6 @@ "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/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index bde1c2d37ff..910de4e2e46 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "already_setup": "Questa voce \u00e8 gi\u00e0 stata configurata." + }, "error": { "host_exists": "Host gi\u00e0 configurato.", "name_exists": "Nome gi\u00e0 configurato." + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso", + "device_class": "Tipo di dispositivo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index c0f5950613a..cdf16bfe28d 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -11,8 +11,8 @@ }, "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": "Vert er allerede konfigurert.", - "name_exists": "Navnet er allerede konfigurert.", + "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": { diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index 708334eba3d..ad79dc827c4 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -1,11 +1,43 @@ { "config": { + "abort": { + "already_in_progress": "Trwa konfiguracja przep\u0142ywu dla komponentu Vizio.", + "already_setup": "Ten wpis zosta\u0142 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 iz menu Integracje przed ponown\u0105 pr\u00f3b\u0105 dodania tego urz\u0105dzenia.", + "host_exists": "Komponent Vizio z ju\u017c skonfigurowanym hostem.", + "name_exists": "Komponent Vizio z ju\u017c skonfigurowan\u0105 nazw\u0105.", + "updated_options": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci opcji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", + "updated_volume_step": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale rozmiar kroku 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 - Podane warto\u015bci s\u0105 dok\u0142adne \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` to potrzebny jest wa\u017cny token dost\u0119pu." + }, "step": { "user": { "data": { - "access_token": "Token dost\u0119pu" - } + "access_token": "Token dost\u0119pu", + "device_class": "Typ urz\u0105dzenia", + "host": ":", + "name": "Nazwa" + }, + "title": "Skonfiguruj klienta Vizio SmartCast" } - } + }, + "title": "Vizio SmartCast" + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", + "volume_step": "Wielko\u015b\u0107 kroku g\u0142o\u015bno\u015bci" + }, + "title": "Zaktualizuj opcje Vizo SmartCast" + } + }, + "title": "Zaktualizuj opcje Vizo SmartCast" } } \ No newline at end of file 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/it.json b/homeassistant/components/withings/.translations/it.json index 4ac73dde195..de854b3e53f 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -1,12 +1,16 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", "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." }, "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, "profile": { "data": { "profile": "Profilo" 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..97aa393fde4 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,12 +1,17 @@ { "config": { "abort": { + "authorize_url_timeout": "Przekroczono 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" From 58973d526575110cac8adf239f3682d88d90ec44 Mon Sep 17 00:00:00 2001 From: Leon Knauer Date: Wed, 22 Jan 2020 01:34:19 +0100 Subject: [PATCH 207/393] Fix luftdaten broken icon for "Pressure at sealevel" (#31053) --- homeassistant/components/luftdaten/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From bc600995c171cee96a3675fae25043250b616213 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 22 Jan 2020 01:39:12 +0100 Subject: [PATCH 208/393] Do not stop fetching iCloud data after service error (#31069) --- homeassistant/components/icloud/__init__.py | 22 ++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index bc1a7535882..525831ce214 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -262,14 +262,14 @@ class IcloudAccount: self._icloud_dir = icloud_dir - self.api = None + self.api: PyiCloudService = None self._owner_fullname = None self._family_members_fullname = {} self._devices = {} self.unsub_device_tracker = None - def setup(self): + def setup(self) -> None: """Set up an iCloud account.""" try: self.api = PyiCloudService( @@ -285,7 +285,8 @@ class IcloudAccount: # Gets device owners infos user_info = self.api.devices.response["userInfo"] except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") + _LOGGER.error("No iCloud device found") + return self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" @@ -308,7 +309,18 @@ class IcloudAccount: try: api_devices = self.api.devices except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") + _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: @@ -330,8 +342,8 @@ class IcloudAccount: self._devices[device_id] = IcloudDevice(self, device, status) self._devices[device_id].update(status) - dispatcher_send(self.hass, SERVICE_UPDATE) self._fetch_interval = self._determine_interval() + dispatcher_send(self.hass, SERVICE_UPDATE) track_point_in_utc_time( self.hass, self.keep_alive, From ee74f95371623b888885728b9b110a4dd0810488 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 22 Jan 2020 00:24:59 -0500 Subject: [PATCH 209/393] Enhance info_from_service function in zeroconf integration (#31059) * enhance zeroconf service info decoding to include raw bytes * Update homeassistant/components/zeroconf/__init__.py Co-Authored-By: Paulus Schoutsen * fix test based on last commit * fix test based on last commit * remove .keys() when asserting processed and raw service info properties Co-authored-by: Paulus Schoutsen --- homeassistant/components/zeroconf/__init__.py | 13 +++++++++---- tests/components/zeroconf/test_init.py | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) 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/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 From 463d949ee013b5285f96adda3f0b4acfc968c02b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 22 Jan 2020 04:13:35 -0500 Subject: [PATCH 210/393] Add zeroconf discovery support for vizio integration (#30949) * add missing tests * readd removed test * add zeroconf discovery support for vizio integration * no mock_coro_func needed * add reasonable timeout and don't log exceptions from pyvizio due to timeout * add test to test options update and bump pyvizio to avoid timeout issues * update requirements_* * fix gaps in coverage * change return hint for async_setup_entry * use source variables instead of strings * only get unique ID if about to create entry * update based on review * Revert "update based on review" This reverts commit 0d612a90eb7d02c92061f902973e527267e3110a. * f-string * fix last review * revert cleanup changes to simplify PR * remove unnecessary ConfigFlow object variables to simplify logic * revert cleanup changes to make review easier, noted for future cleanup * revert cleanup changes to make review easier, noted for future cleanup * move zeroconf service type constant to test module --- homeassistant/components/vizio/config_flow.py | 70 +++++++++++---- homeassistant/components/vizio/const.py | 1 + homeassistant/components/vizio/manifest.json | 5 +- .../components/vizio/media_player.py | 4 +- homeassistant/components/vizio/strings.json | 3 +- homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/test_config_flow.py | 85 +++++++++++++++++-- 9 files changed, 144 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 560b01df83a..a02ae22a46b 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict -from pyvizio import VizioAsync +from pyvizio import VizioAsync, async_guess_device_type import voluptuous as vol from homeassistant import config_entries @@ -13,6 +13,8 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, + CONF_PORT, + CONF_TYPE, ) from homeassistant.core import callback @@ -64,6 +66,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize config flow.""" self.import_schema = None self.user_schema = None + self._must_show_form = None async def async_step_user( self, user_input: Dict[str, Any] = None @@ -101,24 +104,31 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "tv_needs_token" if not errors: - unique_id = await VizioAsync.get_unique_id( - user_input[CONF_HOST], - user_input.get(CONF_ACCESS_TOKEN), - user_input[CONF_DEVICE_CLASS], - ) - - # Abort flow if existing component with same unique ID matches new config entry - 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" + # 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], ) - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + 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 self.import_schema or _config_flow_schema({}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) @@ -153,6 +163,34 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 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) + class VizioOptionsConfigFlow(config_entries.OptionsFlow): """Handle Transmission client options.""" diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py index 0345b0c9992..92fb37c153e 100644 --- a/homeassistant/components/vizio/const.py +++ b/homeassistant/components/vizio/const.py @@ -30,6 +30,7 @@ CONF_VOLUME_STEP = "volume_step" DEFAULT_DEVICE_CLASS = DEVICE_CLASS_TV DEFAULT_NAME = "Vizio SmartCast" +DEFAULT_TIMEOUT = 8 DEFAULT_VOLUME_STEP = 1 DEVICE_ID = "pyvizio" diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index b5ea057a33d..ea1162540cf 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,8 +2,9 @@ "domain": "vizio", "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.1"], + "requirements": ["pyvizio==0.1.4"], "dependencies": [], "codeowners": ["@raman325"], - "config_flow": true + "config_flow": true, + "zeroconf": ["_viziocast._tcp.local."] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 76bb476317e..d143c4232bf 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -26,6 +26,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( CONF_VOLUME_STEP, + DEFAULT_TIMEOUT, DEFAULT_VOLUME_STEP, DEVICE_ID, DOMAIN, @@ -46,7 +47,7 @@ async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable[[List[Entity], bool], None], -) -> bool: +) -> None: """Set up a Vizio media player entry.""" host = config_entry.data[CONF_HOST] token = config_entry.data.get(CONF_ACCESS_TOKEN) @@ -69,6 +70,7 @@ async def async_setup_entry( token, VIZIO_DEVICE_CLASSES[device_class], session=async_get_clientsession(hass, False), + timeout=DEFAULT_TIMEOUT, ) if not await device.can_connect(): diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index a6367cb3c8f..305e49d56f8 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -30,8 +30,7 @@ "init": { "title": "Update Vizo SmartCast Options", "data": { - "volume_step": "Volume Step Size", - "timeout": "API Request Timeout (seconds)" + "volume_step": "Volume Step Size" } } } diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index eceb2ee3fd5..9af4bde8a2a 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -24,6 +24,9 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_viziocast._tcp.local.": [ + "vizio" + ], "_wled._tcp.local.": [ "wled" ] diff --git a/requirements_all.txt b/requirements_all.txt index f0e1848c76e..ef2c85373e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1690,7 +1690,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.1 +pyvizio==0.1.4 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 594e8622e70..7ed5c31ca49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.1 +pyvizio==0.1.4 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 4dbb375c3fe..196ef35469d 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -14,12 +14,14 @@ from homeassistant.components.vizio.const import ( DOMAIN, VIZIO_SCHEMA, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +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 @@ -62,6 +64,19 @@ MOCK_SPEAKER_CONFIG = { 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(): @@ -93,6 +108,16 @@ def vizio_bypass_update_fixture(): 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.""" @@ -175,7 +200,7 @@ async def test_options_flow(hass: HomeAssistantType) -> None: assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP}, + result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -188,9 +213,7 @@ async def test_user_host_already_configured( ) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_SPEAKER_CONFIG, - options={CONF_VOLUME_STEP: VOLUME_STEP}, + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} ) entry.add_to_hass(hass) fail_entry = MOCK_SPEAKER_CONFIG.copy() @@ -216,9 +239,7 @@ async def test_user_name_already_configured( ) -> None: """Test name is already configured during user setup.""" entry = MockConfigEntry( - domain=DOMAIN, - data=MOCK_SPEAKER_CONFIG, - options={CONF_VOLUME_STEP: VOLUME_STEP}, + domain=DOMAIN, data=MOCK_SPEAKER_CONFIG, options={CONF_VOLUME_STEP: VOLUME_STEP} ) entry.add_to_hass(hass) @@ -385,3 +406,51 @@ async def test_import_flow_update_options( hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 ) + + +async def test_zeroconf_flow( + hass: HomeAssistantType, vizio_connect, vizio_bypass_setup, vizio_guess_device_type +) -> 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, vizio_bypass_setup +) -> 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" From 96dba7b91f447123de015096ffaeeb62656a211e Mon Sep 17 00:00:00 2001 From: Dougal Matthews Date: Wed, 22 Jan 2020 09:38:24 +0000 Subject: [PATCH 211/393] Add the Home Assistant version as a Sentry release (#31065) This helps make it easier to identify which version of Home Assistant first introduces a error. --- homeassistant/components/sentry/__init__.py | 2 ++ 1 file changed, 2 insertions(+) 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 From db76b91ffa90fb6d90a48bf09f1288eadaf44fd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jan 2020 10:45:38 +0100 Subject: [PATCH 212/393] Add disabled entities support to WLED (#31040) --- homeassistant/components/wled/__init__.py | 18 ++++++- homeassistant/components/wled/sensor.py | 5 +- tests/components/wled/test_sensor.py | 58 ++++++++++++++++++++--- 3 files changed, 73 insertions(+), 8 deletions(-) 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/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" From 3c44a1353a848ca09aa63c4aed664aaa12de99cb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 22 Jan 2020 09:23:35 -0500 Subject: [PATCH 213/393] change group id creation (#31075) --- homeassistant/components/zha/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 5fb7f6d8fdb..3871a26c9d7 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -317,11 +317,14 @@ 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) + # we start with one to fill any gaps from a user removing existing groups + group_id = 1 + while group_id in zha_gateway.application_controller.groups: + group_id += 1 # guard against group already existing if zigpy_group is None: From e5365779fe596750cbd402d34c2123a5688d7bfc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Jan 2020 09:57:47 -0800 Subject: [PATCH 214/393] Allow unloading mobile app (#30995) --- .../components/mobile_app/__init__.py | 43 ++++++- .../components/mobile_app/websocket_api.py | 121 ------------------ tests/components/mobile_app/conftest.py | 12 +- tests/components/mobile_app/test_init.py | 37 ++++++ tests/components/mobile_app/test_webhook.py | 3 +- .../mobile_app/test_websocket_api.py | 76 ----------- 6 files changed, 86 insertions(+), 206 deletions(-) delete mode 100644 homeassistant/components/mobile_app/websocket_api.py create mode 100644 tests/components/mobile_app/test_init.py delete mode 100644 tests/components/mobile_app/test_websocket_api.py 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/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 From fae74f7ed7ed367f3ee15704d39487b799f381b2 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Wed, 22 Jan 2020 19:06:08 +0100 Subject: [PATCH 215/393] Improve state tracking for WebOsTV (#31042) * upgrade to aiopylgtv 0.3.0 and corresponding simplification and cleanup of webostv state tracking * properly handle case where Live TV is not reported in list of apps * fix tests (entity state is no longer linked to source id) * fix pylint checks * avoid unnecessary retrieval of channel list * use only standard home assistant states --- homeassistant/components/webostv/__init__.py | 7 +- .../components/webostv/manifest.json | 2 +- .../components/webostv/media_player.py | 105 ++++++++---------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/webostv/test_media_player.py | 2 - 6 files changed, 52 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index e03fea68fd7..13f3d9e8f8d 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -21,7 +21,6 @@ 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" @@ -46,9 +45,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, } ) @@ -100,9 +96,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/manifest.json b/homeassistant/components/webostv/manifest.json index ff254e35159..4328ff96b56 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.0"], "dependencies": ["configurator"], "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index c523c068bcc..c34fb376d31 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -59,6 +59,8 @@ SUPPORT_WEBOSTV = ( MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) +LIVE_TV_APP_ID = "com.webos.app.livetv" + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the LG WebOS TV platform.""" @@ -121,17 +123,8 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): # Assume that the TV is not paused self._paused = False - # Assume that the TV is not muted - self._muted = False - self._volume = 0 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.""" @@ -141,10 +134,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) @@ -162,18 +151,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) @@ -183,8 +160,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 ( @@ -195,8 +175,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 ( @@ -206,6 +188,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.""" @@ -231,17 +227,24 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - return self._state + client_state = self._client.power_state.get("state") + if client_state in [None, "Power Off", "Suspend", "Active Standby"]: + return STATE_OFF + + return STATE_ON @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): @@ -256,30 +259,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 @@ -293,22 +293,13 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @cmd async def async_turn_off(self): """Turn off media player.""" - - # in some situations power_off may cause the TV to switch back on - if self._state != STATE_OFF: - await self._client.power_off() + await self._client.power_off() 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.""" @@ -360,7 +351,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 diff --git a/requirements_all.txt b/requirements_all.txt index ef2c85373e6..8b7423ce5a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.2.7 +aiopylgtv==0.3.0 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ed5c31ca49..59e3424b986 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.2.7 +aiopylgtv==0.3.0 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 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() From 93d109e524af5f9b73830194c920771716e0b4ee Mon Sep 17 00:00:00 2001 From: thoscut Date: Wed, 22 Jan 2020 19:31:03 +0100 Subject: [PATCH 216/393] Add DIRECTORY and PLUGIN to kodi media types (#28336) --- homeassistant/components/kodi/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 13aa18d01ad..78355937d15 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -781,6 +781,10 @@ class KodiDevice(MediaPlayerDevice): return self.server.Player.Open({"item": {"channelid": int(media_id)}}) if media_type == "PLAYLIST": return self.server.Player.Open({"item": {"playlistid": int(media_id)}}) + if media_type == "DIRECTORY": + return self.server.Player.Open({"item": {"directory": str(media_id)}}) + if media_type == "PLUGIN": + return self.server.Player.Open({"item": {"file": str(media_id)}}) return self.server.Player.Open({"item": {"file": str(media_id)}}) From 4015a046d2ba07ef7cfd44e26a0892c2e60e5a4d Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 22 Jan 2020 14:04:31 -0500 Subject: [PATCH 217/393] Update AdjustRange Handler Service Calls. (#31016) Add a AlexaGlobalCatalog value to all labels. --- .../components/alexa/capabilities.py | 7 ++- homeassistant/components/alexa/handlers.py | 20 +++++-- tests/components/alexa/test_smart_home.py | 60 ++++++++++++++++++- 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 6a910b3bb8f..080a8c39147 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1203,7 +1203,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 @@ -1397,7 +1400,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, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index fc49266f812..8bd52b1e40b 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1211,18 +1211,26 @@ async def async_api_adjust_range(hass, config, directive, context): range_delta = 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) - ) + 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) 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) - ) + 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}": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a29df07bc1f..161f69287d4 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1553,9 +1553,37 @@ async def test_cover_position_range(hass): assert properties["namespace"] == "Alexa.RangeController" assert properties["value"] == 100 + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_range", + "cover.open_cover", + hass, + payload={"rangeValueDelta": "99"}, + 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"}, + 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"), (35, "5"), (0, "-99"), (100, "99")], + [(25, "-5"), (35, "5")], "Alexa.RangeController", "AdjustRangeValue", "cover#test_range", @@ -2769,9 +2797,37 @@ async def test_cover_tilt_position_range(hass): assert properties["namespace"] == "Alexa.RangeController" assert properties["value"] == 100 + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_tilt_range", + "cover.open_cover_tilt", + hass, + payload={"rangeValueDelta": "99"}, + 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"}, + 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"), (35, "5"), (0, "-99"), (100, "99")], + [(25, "-5"), (35, "5")], "Alexa.RangeController", "AdjustRangeValue", "cover#test_tilt_range", From 4c27d6b9aaae4ae9c98f76aa002d9b5c5de88619 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 22 Jan 2020 20:34:11 +0100 Subject: [PATCH 218/393] Add zeroconf discovery support to Brother Printer integration (#30959) * Add zeroconf discovery support * Fix data for config_entry * Add sting for zeroconf confirm dialog * Add and fix tests * Fix pylint errors * Suggested changes * Tests * Remove unnecessary object * Add error handling * Remove unnecessary objects * Suggested change * Suggested change * Use core interfaces for tests --- .../components/brother/config_flow.py | 57 ++++++ .../components/brother/manifest.json | 1 + homeassistant/components/brother/strings.json | 8 + homeassistant/generated/zeroconf.py | 3 + tests/components/brother/test_config_flow.py | 166 +++++++++++++----- 5 files changed, 192 insertions(+), 43 deletions(-) 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/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 9af4bde8a2a..8d3bff42d12 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -24,6 +24,9 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_printer._tcp.local.": [ + "brother" + ], "_viziocast._tcp.local.": [ "vizio" ], 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" From 0fba9e44edd76818afb9691ca9732fd59546a334 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Jan 2020 12:36:25 -0800 Subject: [PATCH 219/393] Migrate zone to use collection helpers (#30774) --- .../components/owntracks/messages.py | 11 +- .../components/zone/.translations/bg.json | 21 - .../components/zone/.translations/ca.json | 21 - .../components/zone/.translations/cs.json | 21 - .../components/zone/.translations/cy.json | 21 - .../components/zone/.translations/da.json | 21 - .../components/zone/.translations/de.json | 21 - .../components/zone/.translations/en.json | 21 - .../components/zone/.translations/es-419.json | 21 - .../components/zone/.translations/es.json | 21 - .../components/zone/.translations/et.json | 16 - .../components/zone/.translations/fr.json | 21 - .../components/zone/.translations/he.json | 21 - .../components/zone/.translations/hr.json | 21 - .../components/zone/.translations/hu.json | 21 - .../components/zone/.translations/id.json | 21 - .../components/zone/.translations/it.json | 21 - .../components/zone/.translations/ja.json | 13 - .../components/zone/.translations/ko.json | 21 - .../components/zone/.translations/lb.json | 21 - .../components/zone/.translations/nl.json | 21 - .../components/zone/.translations/nn.json | 21 - .../components/zone/.translations/no.json | 21 - .../components/zone/.translations/pl.json | 21 - .../components/zone/.translations/pt-BR.json | 21 - .../components/zone/.translations/pt.json | 21 - .../components/zone/.translations/ru.json | 21 - .../components/zone/.translations/sl.json | 21 - .../components/zone/.translations/sv.json | 21 - .../components/zone/.translations/th.json | 17 - .../components/zone/.translations/uk.json | 21 - .../components/zone/.translations/vi.json | 21 - .../zone/.translations/zh-Hans.json | 21 - .../zone/.translations/zh-Hant.json | 21 - homeassistant/components/zone/__init__.py | 340 ++++++--- homeassistant/components/zone/config_flow.py | 78 +-- homeassistant/components/zone/manifest.json | 2 +- homeassistant/components/zone/strings.json | 21 - homeassistant/components/zone/zone.py | 71 -- homeassistant/generated/config_flows.py | 1 - homeassistant/helpers/collection.py | 6 +- homeassistant/helpers/condition.py | 2 +- homeassistant/helpers/entity.py | 4 +- homeassistant/helpers/entity_component.py | 48 +- tests/components/proximity/test_init.py | 5 +- tests/components/zone/test_config_flow.py | 60 -- tests/components/zone/test_init.py | 646 ++++++++++++------ 47 files changed, 762 insertions(+), 1209 deletions(-) delete mode 100644 homeassistant/components/zone/.translations/bg.json delete mode 100644 homeassistant/components/zone/.translations/ca.json delete mode 100644 homeassistant/components/zone/.translations/cs.json delete mode 100644 homeassistant/components/zone/.translations/cy.json delete mode 100644 homeassistant/components/zone/.translations/da.json delete mode 100644 homeassistant/components/zone/.translations/de.json delete mode 100644 homeassistant/components/zone/.translations/en.json delete mode 100644 homeassistant/components/zone/.translations/es-419.json delete mode 100644 homeassistant/components/zone/.translations/es.json delete mode 100644 homeassistant/components/zone/.translations/et.json delete mode 100644 homeassistant/components/zone/.translations/fr.json delete mode 100644 homeassistant/components/zone/.translations/he.json delete mode 100644 homeassistant/components/zone/.translations/hr.json delete mode 100644 homeassistant/components/zone/.translations/hu.json delete mode 100644 homeassistant/components/zone/.translations/id.json delete mode 100644 homeassistant/components/zone/.translations/it.json delete mode 100644 homeassistant/components/zone/.translations/ja.json delete mode 100644 homeassistant/components/zone/.translations/ko.json delete mode 100644 homeassistant/components/zone/.translations/lb.json delete mode 100644 homeassistant/components/zone/.translations/nl.json delete mode 100644 homeassistant/components/zone/.translations/nn.json delete mode 100644 homeassistant/components/zone/.translations/no.json delete mode 100644 homeassistant/components/zone/.translations/pl.json delete mode 100644 homeassistant/components/zone/.translations/pt-BR.json delete mode 100644 homeassistant/components/zone/.translations/pt.json delete mode 100644 homeassistant/components/zone/.translations/ru.json delete mode 100644 homeassistant/components/zone/.translations/sl.json delete mode 100644 homeassistant/components/zone/.translations/sv.json delete mode 100644 homeassistant/components/zone/.translations/th.json delete mode 100644 homeassistant/components/zone/.translations/uk.json delete mode 100644 homeassistant/components/zone/.translations/vi.json delete mode 100644 homeassistant/components/zone/.translations/zh-Hans.json delete mode 100644 homeassistant/components/zone/.translations/zh-Hant.json delete mode 100644 homeassistant/components/zone/strings.json delete mode 100644 homeassistant/components/zone/zone.py delete mode 100644 tests/components/zone/test_config_flow.py 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/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json deleted file mode 100644 index 5770058c5eb..00000000000 --- a/homeassistant/components/zone/.translations/bg.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" - }, - "step": { - "init": { - "data": { - "icon": "\u0418\u043a\u043e\u043d\u0430", - "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", - "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", - "name": "\u0418\u043c\u0435", - "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430", - "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" - }, - "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430" - } - }, - "title": "\u0417\u043e\u043d\u0430" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json deleted file mode 100644 index aa8296b92df..00000000000 --- a/homeassistant/components/zone/.translations/ca.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "El nom ja existeix" - }, - "step": { - "init": { - "data": { - "icon": "Icona", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom", - "passive": "Passiu", - "radius": "Radi" - }, - "title": "Definici\u00f3 dels par\u00e0metres de la zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json deleted file mode 100644 index a521377e5e0..00000000000 --- a/homeassistant/components/zone/.translations/cs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "N\u00e1zev ji\u017e existuje" - }, - "step": { - "init": { - "data": { - "icon": "Ikona", - "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", - "name": "N\u00e1zev", - "passive": "Pasivn\u00ed", - "radius": "Polom\u011br" - }, - "title": "Definujte parametry z\u00f3ny" - } - }, - "title": "Z\u00f3na" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cy.json b/homeassistant/components/zone/.translations/cy.json deleted file mode 100644 index e34fae81b61..00000000000 --- a/homeassistant/components/zone/.translations/cy.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Enw eisoes yn bodoli" - }, - "step": { - "init": { - "data": { - "icon": "Eicon", - "latitude": "Lledred", - "longitude": "Hydred", - "name": "Enw", - "passive": "Goddefol", - "radius": "Radiws" - }, - "title": "Ddiffinio paramedrau parth" - } - }, - "title": "Parth" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/da.json b/homeassistant/components/zone/.translations/da.json deleted file mode 100644 index c6981f242d2..00000000000 --- a/homeassistant/components/zone/.translations/da.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Navnet findes allerede" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Breddegrad", - "longitude": "L\u00e6ngdegrad", - "name": "Navn", - "passive": "Passiv", - "radius": "Radius" - }, - "title": "Definer zoneparametre" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json deleted file mode 100644 index 483c7f065a3..00000000000 --- a/homeassistant/components/zone/.translations/de.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Name existiert bereits" - }, - "step": { - "init": { - "data": { - "icon": "Symbol", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name", - "passive": "Passiv", - "radius": "Radius" - }, - "title": "Definiere die Zonenparameter" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json deleted file mode 100644 index 1faf0110a53..00000000000 --- a/homeassistant/components/zone/.translations/en.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Name already exists" - }, - "step": { - "init": { - "data": { - "icon": "Icon", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name", - "passive": "Passive", - "radius": "Radius" - }, - "title": "Define zone parameters" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/es-419.json b/homeassistant/components/zone/.translations/es-419.json deleted file mode 100644 index b15be44b7b1..00000000000 --- a/homeassistant/components/zone/.translations/es-419.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "El nombre ya existe" - }, - "step": { - "init": { - "data": { - "icon": "Icono", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre", - "passive": "Pasivo", - "radius": "Radio" - }, - "title": "Definir par\u00e1metros de zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/es.json b/homeassistant/components/zone/.translations/es.json deleted file mode 100644 index 7a0f6c967c2..00000000000 --- a/homeassistant/components/zone/.translations/es.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "El nombre ya existe" - }, - "step": { - "init": { - "data": { - "icon": "Icono", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre", - "passive": "Pasivo", - "radius": "Radio" - }, - "title": "Definir par\u00e1metros de la zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/et.json b/homeassistant/components/zone/.translations/et.json deleted file mode 100644 index aa921f376e7..00000000000 --- a/homeassistant/components/zone/.translations/et.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "step": { - "init": { - "data": { - "icon": "Ikoon", - "latitude": "Laius", - "longitude": "Pikkus", - "name": "Nimi", - "radius": "Raadius" - }, - "title": "M\u00e4\u00e4ra tsooni parameetrid" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json deleted file mode 100644 index eb02aba7b50..00000000000 --- a/homeassistant/components/zone/.translations/fr.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" - }, - "step": { - "init": { - "data": { - "icon": "Ic\u00f4ne", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom", - "passive": "Passif", - "radius": "Rayon" - }, - "title": "D\u00e9finir les param\u00e8tres de la zone" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/he.json b/homeassistant/components/zone/.translations/he.json deleted file mode 100644 index b6a2a30b625..00000000000 --- a/homeassistant/components/zone/.translations/he.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u05d4\u05e9\u05dd \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" - }, - "step": { - "init": { - "data": { - "icon": "\u05e1\u05de\u05dc", - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "name": "\u05e9\u05dd", - "passive": "\u05e4\u05e1\u05d9\u05d1\u05d9", - "radius": "\u05e8\u05d3\u05d9\u05d5\u05e1" - }, - "title": "\u05d4\u05d2\u05d3\u05e8 \u05e4\u05e8\u05de\u05d8\u05e8\u05d9\u05dd \u05e9\u05dc \u05d0\u05d6\u05d5\u05e8" - } - }, - "title": "\u05d0\u05d6\u05d5\u05e8" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hr.json b/homeassistant/components/zone/.translations/hr.json deleted file mode 100644 index 8a9f543be0a..00000000000 --- a/homeassistant/components/zone/.translations/hr.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Ime ve\u0107 postoji" - }, - "step": { - "init": { - "data": { - "icon": "Ikona", - "latitude": "Zemljopisna \u0161irina", - "longitude": "Zemljopisna du\u017eina", - "name": "Ime", - "passive": "Pasivno", - "radius": "Radijus" - }, - "title": "Definirajte parametre zone" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json deleted file mode 100644 index 0181f688c27..00000000000 --- a/homeassistant/components/zone/.translations/hu.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v", - "passive": "Passz\u00edv", - "radius": "Sug\u00e1r" - }, - "title": "Z\u00f3na param\u00e9terek megad\u00e1sa" - } - }, - "title": "Z\u00f3na" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/id.json b/homeassistant/components/zone/.translations/id.json deleted file mode 100644 index b84710dc408..00000000000 --- a/homeassistant/components/zone/.translations/id.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Nama sudah ada" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Lintang", - "longitude": "Garis bujur", - "name": "Nama", - "passive": "Pasif", - "radius": "Radius" - }, - "title": "Tentukan parameter zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json deleted file mode 100644 index 24de27a8bbb..00000000000 --- a/homeassistant/components/zone/.translations/it.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Il nome \u00e8 gi\u00e0 esistente" - }, - "step": { - "init": { - "data": { - "icon": "Icona", - "latitude": "Latitudine", - "longitude": "Longitudine", - "name": "Nome", - "passive": "Passiva", - "radius": "Raggio" - }, - "title": "Imposta i parametri della zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ja.json b/homeassistant/components/zone/.translations/ja.json deleted file mode 100644 index 093f5ad9938..00000000000 --- a/homeassistant/components/zone/.translations/ja.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "config": { - "step": { - "init": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json deleted file mode 100644 index 421f079a67e..00000000000 --- a/homeassistant/components/zone/.translations/ko.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" - }, - "step": { - "init": { - "data": { - "icon": "\uc544\uc774\ucf58", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "name": "\uc774\ub984", - "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", - "radius": "\ubc18\uacbd" - }, - "title": "\uad6c\uc5ed \uc124\uc815" - } - }, - "title": "\uad6c\uc5ed" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/lb.json b/homeassistant/components/zone/.translations/lb.json deleted file mode 100644 index 10b65bcca30..00000000000 --- a/homeassistant/components/zone/.translations/lb.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Numm g\u00ebtt et schonn" - }, - "step": { - "init": { - "data": { - "icon": "Ikone", - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", - "name": "Numm", - "passive": "Passif", - "radius": "Radius" - }, - "title": "D\u00e9fin\u00e9iert Zone Parameter" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nl.json b/homeassistant/components/zone/.translations/nl.json deleted file mode 100644 index 6dcf565ada6..00000000000 --- a/homeassistant/components/zone/.translations/nl.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Naam bestaat al" - }, - "step": { - "init": { - "data": { - "icon": "Pictogram", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "name": "Naam", - "passive": "Passief", - "radius": "Straal" - }, - "title": "Definieer zone parameters" - } - }, - "title": "Zone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nn.json b/homeassistant/components/zone/.translations/nn.json deleted file mode 100644 index 39161f98c82..00000000000 --- a/homeassistant/components/zone/.translations/nn.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Namnet eksisterar allereie" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Breiddegrad", - "longitude": "Lengdegrad", - "name": "Namn", - "passive": "Passiv", - "radius": "Radius" - }, - "title": "Definer soneparameterar" - } - }, - "title": "Sone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json deleted file mode 100644 index 3c1a91976f0..00000000000 --- a/homeassistant/components/zone/.translations/no.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Navnet eksisterer allerede" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn", - "passive": "Passiv", - "radius": "Radius" - }, - "title": "Definer sone parametere" - } - }, - "title": "Sone" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json deleted file mode 100644 index e649de4c75e..00000000000 --- a/homeassistant/components/zone/.translations/pl.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Nazwa ju\u017c istnieje" - }, - "step": { - "init": { - "data": { - "icon": "Ikona", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa", - "passive": "Pasywnie", - "radius": "Promie\u0144" - }, - "title": "Zdefiniuj parametry strefy" - } - }, - "title": "Strefa" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json deleted file mode 100644 index f2a41b0b267..00000000000 --- a/homeassistant/components/zone/.translations/pt-BR.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "O nome j\u00e1 existe" - }, - "step": { - "init": { - "data": { - "icon": "\u00cdcone", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome", - "passive": "Passivo", - "radius": "Raio" - }, - "title": "Definir par\u00e2metros da zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json deleted file mode 100644 index 2c3292e58c1..00000000000 --- a/homeassistant/components/zone/.translations/pt.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Nome j\u00e1 existente" - }, - "step": { - "init": { - "data": { - "icon": "\u00cdcone", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome", - "passive": "Passivo", - "radius": "Raio" - }, - "title": "Definir os par\u00e2metros da zona" - } - }, - "title": "Zona" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json deleted file mode 100644 index 6a017e9e1c3..00000000000 --- a/homeassistant/components/zone/.translations/ru.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "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." - }, - "step": { - "init": { - "data": { - "icon": "\u0417\u043d\u0430\u0447\u043e\u043a", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f", - "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" - }, - "title": "\u0417\u043e\u043d\u0430" - } - }, - "title": "\u0417\u043e\u043d\u0430" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json deleted file mode 100644 index 1885cb5d2c8..00000000000 --- a/homeassistant/components/zone/.translations/sl.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Ime \u017ee obstaja" - }, - "step": { - "init": { - "data": { - "icon": "Ikona", - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "name": "Ime", - "passive": "Pasivno", - "radius": "Radij" - }, - "title": "Dolo\u010dite parametre obmo\u010dja" - } - }, - "title": "Obmo\u010dje" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json deleted file mode 100644 index 55c5bcf7127..00000000000 --- a/homeassistant/components/zone/.translations/sv.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Namnet finns redan" - }, - "step": { - "init": { - "data": { - "icon": "Ikon", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Namn", - "passive": "Passiv", - "radius": "Radie" - }, - "title": "Definiera zonparametrar" - } - }, - "title": "Zon" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/th.json b/homeassistant/components/zone/.translations/th.json deleted file mode 100644 index e39765f2da2..00000000000 --- a/homeassistant/components/zone/.translations/th.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u0e21\u0e35\u0e0a\u0e37\u0e48\u0e2d\u0e19\u0e35\u0e49\u0e2d\u0e22\u0e39\u0e48\u0e41\u0e25\u0e49\u0e27" - }, - "step": { - "init": { - "data": { - "latitude": "\u0e40\u0e2a\u0e49\u0e19\u0e23\u0e38\u0e49\u0e07", - "longitude": "\u0e40\u0e2a\u0e49\u0e19\u0e41\u0e27\u0e07", - "name": "\u0e0a\u0e37\u0e48\u0e2d" - } - } - }, - "title": "\u0e42\u0e0b\u0e19" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/uk.json b/homeassistant/components/zone/.translations/uk.json deleted file mode 100644 index ce082d34a1c..00000000000 --- a/homeassistant/components/zone/.translations/uk.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" - }, - "step": { - "init": { - "data": { - "icon": "\u0406\u043a\u043e\u043d\u043a\u0430", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430", - "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0438\u0439", - "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" - }, - "title": "\u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0437\u043e\u043d\u0438" - } - }, - "title": "\u0417\u043e\u043d\u0430" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json deleted file mode 100644 index 7217944bd6b..00000000000 --- a/homeassistant/components/zone/.translations/vi.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i" - }, - "step": { - "init": { - "data": { - "icon": "Bi\u1ec3u t\u01b0\u1ee3ng", - "latitude": "V\u0129 \u0111\u1ed9", - "longitude": "Kinh \u0111\u1ed9", - "name": "T\u00ean", - "passive": "Th\u1ee5 \u0111\u1ed9ng", - "radius": "B\u00e1n k\u00ednh" - }, - "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng" - } - }, - "title": "V\u00f9ng" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json deleted file mode 100644 index 6d06b68dad8..00000000000 --- a/homeassistant/components/zone/.translations/zh-Hans.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" - }, - "step": { - "init": { - "data": { - "icon": "\u56fe\u6807", - "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6", - "name": "\u540d\u79f0", - "passive": "\u88ab\u52a8", - "radius": "\u534a\u5f84" - }, - "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" - } - }, - "title": "\u533a\u57df" - } -} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json deleted file mode 100644 index 12c1141397d..00000000000 --- a/homeassistant/components/zone/.translations/zh-Hant.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" - }, - "step": { - "init": { - "data": { - "icon": "\u5716\u793a", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u540d\u7a31", - "passive": "\u88ab\u52d5", - "radius": "\u534a\u5f91" - }, - "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578" - } - }, - "title": "\u5340\u57df" - } -} \ No newline at end of file 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/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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3886dfd2f20..2a013b16ae2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -98,6 +98,5 @@ FLOWS = [ "wled", "wwlln", "zha", - "zone", "zwave" ] diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 80790a6d831..1b3721788f5 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -114,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.""" @@ -133,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) @@ -246,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 diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 02853f7615b..c3d09853960 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -473,7 +473,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) ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7ccc6c35613..b9d1a73351c 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): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 733cb22b3b2..e26dc5dfbea 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -3,6 +3,8 @@ 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 @@ -13,6 +15,7 @@ from homeassistant.helpers import ( config_per_platform, config_validation as cv, discovery, + entity, service, ) from homeassistant.loader import async_get_integration, bind_hass @@ -38,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: @@ -59,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 @@ -68,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 @@ -81,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): @@ -237,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. @@ -250,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/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/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" From a0d2a3c6c58c4a1ed53a7182914ff4f916603a85 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 23 Jan 2020 00:31:52 +0000 Subject: [PATCH 220/393] [ci skip] Translation update --- .../components/almond/.translations/it.json | 3 ++ .../components/almond/.translations/ko.json | 4 ++ .../components/almond/.translations/lb.json | 4 ++ .../binary_sensor/.translations/it.json | 8 ++-- .../components/brother/.translations/da.json | 8 ++++ .../components/brother/.translations/en.json | 8 ++++ .../components/sensor/.translations/it.json | 2 +- .../components/vizio/.translations/it.json | 3 +- .../components/vizio/.translations/ko.json | 5 ++- .../components/vizio/.translations/lb.json | 43 +++++++++++++++++++ .../components/withings/.translations/fr.json | 3 ++ .../components/withings/.translations/it.json | 1 + .../components/withings/.translations/ko.json | 7 ++- .../components/withings/.translations/lb.json | 5 +++ .../components/zone/.translations/bg.json | 21 +++++++++ .../components/zone/.translations/ca.json | 21 +++++++++ .../components/zone/.translations/cs.json | 21 +++++++++ .../components/zone/.translations/cy.json | 21 +++++++++ .../components/zone/.translations/da.json | 21 +++++++++ .../components/zone/.translations/de.json | 21 +++++++++ .../components/zone/.translations/en.json | 21 +++++++++ .../components/zone/.translations/es-419.json | 21 +++++++++ .../components/zone/.translations/es.json | 21 +++++++++ .../components/zone/.translations/et.json | 16 +++++++ .../components/zone/.translations/fr.json | 21 +++++++++ .../components/zone/.translations/he.json | 21 +++++++++ .../components/zone/.translations/hr.json | 21 +++++++++ .../components/zone/.translations/hu.json | 21 +++++++++ .../components/zone/.translations/id.json | 21 +++++++++ .../components/zone/.translations/it.json | 21 +++++++++ .../components/zone/.translations/ja.json | 13 ++++++ .../components/zone/.translations/ko.json | 21 +++++++++ .../components/zone/.translations/lb.json | 21 +++++++++ .../components/zone/.translations/nl.json | 21 +++++++++ .../components/zone/.translations/nn.json | 21 +++++++++ .../components/zone/.translations/no.json | 21 +++++++++ .../components/zone/.translations/pl.json | 21 +++++++++ .../components/zone/.translations/pt-BR.json | 21 +++++++++ .../components/zone/.translations/pt.json | 21 +++++++++ .../components/zone/.translations/ru.json | 21 +++++++++ .../components/zone/.translations/sl.json | 21 +++++++++ .../components/zone/.translations/sv.json | 21 +++++++++ .../components/zone/.translations/th.json | 17 ++++++++ .../components/zone/.translations/uk.json | 21 +++++++++ .../components/zone/.translations/vi.json | 21 +++++++++ .../zone/.translations/zh-Hans.json | 21 +++++++++ .../zone/.translations/zh-Hant.json | 21 +++++++++ 47 files changed, 771 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/vizio/.translations/lb.json create mode 100644 homeassistant/components/zone/.translations/bg.json create mode 100644 homeassistant/components/zone/.translations/ca.json create mode 100644 homeassistant/components/zone/.translations/cs.json create mode 100644 homeassistant/components/zone/.translations/cy.json create mode 100644 homeassistant/components/zone/.translations/da.json create mode 100644 homeassistant/components/zone/.translations/de.json create mode 100644 homeassistant/components/zone/.translations/en.json create mode 100644 homeassistant/components/zone/.translations/es-419.json create mode 100644 homeassistant/components/zone/.translations/es.json create mode 100644 homeassistant/components/zone/.translations/et.json create mode 100644 homeassistant/components/zone/.translations/fr.json create mode 100644 homeassistant/components/zone/.translations/he.json create mode 100644 homeassistant/components/zone/.translations/hr.json create mode 100644 homeassistant/components/zone/.translations/hu.json create mode 100644 homeassistant/components/zone/.translations/id.json create mode 100644 homeassistant/components/zone/.translations/it.json create mode 100644 homeassistant/components/zone/.translations/ja.json create mode 100644 homeassistant/components/zone/.translations/ko.json create mode 100644 homeassistant/components/zone/.translations/lb.json create mode 100644 homeassistant/components/zone/.translations/nl.json create mode 100644 homeassistant/components/zone/.translations/nn.json create mode 100644 homeassistant/components/zone/.translations/no.json create mode 100644 homeassistant/components/zone/.translations/pl.json create mode 100644 homeassistant/components/zone/.translations/pt-BR.json create mode 100644 homeassistant/components/zone/.translations/pt.json create mode 100644 homeassistant/components/zone/.translations/ru.json create mode 100644 homeassistant/components/zone/.translations/sl.json create mode 100644 homeassistant/components/zone/.translations/sv.json create mode 100644 homeassistant/components/zone/.translations/th.json create mode 100644 homeassistant/components/zone/.translations/uk.json create mode 100644 homeassistant/components/zone/.translations/vi.json create mode 100644 homeassistant/components/zone/.translations/zh-Hans.json create mode 100644 homeassistant/components/zone/.translations/zh-Hant.json diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index 9d529e5e5c8..d2d0314fba5 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -6,6 +6,9 @@ "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, "step": { + "hassio_confirm": { + "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/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/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/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/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/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index 910de4e2e46..edbe662d8a0 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -5,7 +5,8 @@ }, "error": { "host_exists": "Host gi\u00e0 configurato.", - "name_exists": "Nome gi\u00e0 configurato." + "name_exists": "Nome gi\u00e0 configurato.", + "tv_needs_token": "Quando Device Type \u00e8 `tv`, \u00e8 necessario un token di accesso valido." }, "step": { "user": { diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json index f3630f8393d..3e54d343f7a 100644 --- a/homeassistant/components/vizio/.translations/ko.json +++ b/homeassistant/components/vizio/.translations/ko.json @@ -3,6 +3,7 @@ "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.", @@ -10,8 +11,8 @@ }, "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": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "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": { 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/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/it.json b/homeassistant/components/withings/.translations/it.json index de854b3e53f..cc7a941813d 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -2,6 +2,7 @@ "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": { 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/zone/.translations/bg.json b/homeassistant/components/zone/.translations/bg.json new file mode 100644 index 00000000000..5770058c5eb --- /dev/null +++ b/homeassistant/components/zone/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + }, + "step": { + "init": { + "data": { + "icon": "\u0418\u043a\u043e\u043d\u0430", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u0437\u043e\u043d\u0430\u0442\u0430" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ca.json b/homeassistant/components/zone/.translations/ca.json new file mode 100644 index 00000000000..aa8296b92df --- /dev/null +++ b/homeassistant/components/zone/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nom ja existeix" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom", + "passive": "Passiu", + "radius": "Radi" + }, + "title": "Definici\u00f3 dels par\u00e0metres de la zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cs.json b/homeassistant/components/zone/.translations/cs.json new file mode 100644 index 00000000000..a521377e5e0 --- /dev/null +++ b/homeassistant/components/zone/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "N\u00e1zev ji\u017e existuje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", + "name": "N\u00e1zev", + "passive": "Pasivn\u00ed", + "radius": "Polom\u011br" + }, + "title": "Definujte parametry z\u00f3ny" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cy.json b/homeassistant/components/zone/.translations/cy.json new file mode 100644 index 00000000000..e34fae81b61 --- /dev/null +++ b/homeassistant/components/zone/.translations/cy.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Enw eisoes yn bodoli" + }, + "step": { + "init": { + "data": { + "icon": "Eicon", + "latitude": "Lledred", + "longitude": "Hydred", + "name": "Enw", + "passive": "Goddefol", + "radius": "Radiws" + }, + "title": "Ddiffinio paramedrau parth" + } + }, + "title": "Parth" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/da.json b/homeassistant/components/zone/.translations/da.json new file mode 100644 index 00000000000..c6981f242d2 --- /dev/null +++ b/homeassistant/components/zone/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Navnet findes allerede" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Navn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer zoneparametre" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json new file mode 100644 index 00000000000..483c7f065a3 --- /dev/null +++ b/homeassistant/components/zone/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "init": { + "data": { + "icon": "Symbol", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definiere die Zonenparameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json new file mode 100644 index 00000000000..1faf0110a53 --- /dev/null +++ b/homeassistant/components/zone/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name already exists" + }, + "step": { + "init": { + "data": { + "icon": "Icon", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name", + "passive": "Passive", + "radius": "Radius" + }, + "title": "Define zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/es-419.json b/homeassistant/components/zone/.translations/es-419.json new file mode 100644 index 00000000000..b15be44b7b1 --- /dev/null +++ b/homeassistant/components/zone/.translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "init": { + "data": { + "icon": "Icono", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre", + "passive": "Pasivo", + "radius": "Radio" + }, + "title": "Definir par\u00e1metros de zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/es.json b/homeassistant/components/zone/.translations/es.json new file mode 100644 index 00000000000..7a0f6c967c2 --- /dev/null +++ b/homeassistant/components/zone/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "init": { + "data": { + "icon": "Icono", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre", + "passive": "Pasivo", + "radius": "Radio" + }, + "title": "Definir par\u00e1metros de la zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/et.json b/homeassistant/components/zone/.translations/et.json new file mode 100644 index 00000000000..aa921f376e7 --- /dev/null +++ b/homeassistant/components/zone/.translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "init": { + "data": { + "icon": "Ikoon", + "latitude": "Laius", + "longitude": "Pikkus", + "name": "Nimi", + "radius": "Raadius" + }, + "title": "M\u00e4\u00e4ra tsooni parameetrid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/fr.json b/homeassistant/components/zone/.translations/fr.json new file mode 100644 index 00000000000..eb02aba7b50 --- /dev/null +++ b/homeassistant/components/zone/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + }, + "step": { + "init": { + "data": { + "icon": "Ic\u00f4ne", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom", + "passive": "Passif", + "radius": "Rayon" + }, + "title": "D\u00e9finir les param\u00e8tres de la zone" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/he.json b/homeassistant/components/zone/.translations/he.json new file mode 100644 index 00000000000..b6a2a30b625 --- /dev/null +++ b/homeassistant/components/zone/.translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u05d4\u05e9\u05dd \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + }, + "step": { + "init": { + "data": { + "icon": "\u05e1\u05de\u05dc", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd", + "passive": "\u05e4\u05e1\u05d9\u05d1\u05d9", + "radius": "\u05e8\u05d3\u05d9\u05d5\u05e1" + }, + "title": "\u05d4\u05d2\u05d3\u05e8 \u05e4\u05e8\u05de\u05d8\u05e8\u05d9\u05dd \u05e9\u05dc \u05d0\u05d6\u05d5\u05e8" + } + }, + "title": "\u05d0\u05d6\u05d5\u05e8" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hr.json b/homeassistant/components/zone/.translations/hr.json new file mode 100644 index 00000000000..8a9f543be0a --- /dev/null +++ b/homeassistant/components/zone/.translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime ve\u0107 postoji" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radijus" + }, + "title": "Definirajte parametre zone" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/hu.json b/homeassistant/components/zone/.translations/hu.json new file mode 100644 index 00000000000..0181f688c27 --- /dev/null +++ b/homeassistant/components/zone/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v", + "passive": "Passz\u00edv", + "radius": "Sug\u00e1r" + }, + "title": "Z\u00f3na param\u00e9terek megad\u00e1sa" + } + }, + "title": "Z\u00f3na" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/id.json b/homeassistant/components/zone/.translations/id.json new file mode 100644 index 00000000000..b84710dc408 --- /dev/null +++ b/homeassistant/components/zone/.translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nama sudah ada" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Lintang", + "longitude": "Garis bujur", + "name": "Nama", + "passive": "Pasif", + "radius": "Radius" + }, + "title": "Tentukan parameter zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/it.json b/homeassistant/components/zone/.translations/it.json new file mode 100644 index 00000000000..24de27a8bbb --- /dev/null +++ b/homeassistant/components/zone/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Il nome \u00e8 gi\u00e0 esistente" + }, + "step": { + "init": { + "data": { + "icon": "Icona", + "latitude": "Latitudine", + "longitude": "Longitudine", + "name": "Nome", + "passive": "Passiva", + "radius": "Raggio" + }, + "title": "Imposta i parametri della zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ja.json b/homeassistant/components/zone/.translations/ja.json new file mode 100644 index 00000000000..093f5ad9938 --- /dev/null +++ b/homeassistant/components/zone/.translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "init": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d4c\u5ea6", + "name": "\u540d\u524d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json new file mode 100644 index 00000000000..421f079a67e --- /dev/null +++ b/homeassistant/components/zone/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "icon": "\uc544\uc774\ucf58", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984", + "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", + "radius": "\ubc18\uacbd" + }, + "title": "\uad6c\uc5ed \uc124\uc815" + } + }, + "title": "\uad6c\uc5ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/lb.json b/homeassistant/components/zone/.translations/lb.json new file mode 100644 index 00000000000..10b65bcca30 --- /dev/null +++ b/homeassistant/components/zone/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "init": { + "data": { + "icon": "Ikone", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm", + "passive": "Passif", + "radius": "Radius" + }, + "title": "D\u00e9fin\u00e9iert Zone Parameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nl.json b/homeassistant/components/zone/.translations/nl.json new file mode 100644 index 00000000000..6dcf565ada6 --- /dev/null +++ b/homeassistant/components/zone/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al" + }, + "step": { + "init": { + "data": { + "icon": "Pictogram", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam", + "passive": "Passief", + "radius": "Straal" + }, + "title": "Definieer zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nn.json b/homeassistant/components/zone/.translations/nn.json new file mode 100644 index 00000000000..39161f98c82 --- /dev/null +++ b/homeassistant/components/zone/.translations/nn.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Namnet eksisterar allereie" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breiddegrad", + "longitude": "Lengdegrad", + "name": "Namn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer soneparameterar" + } + }, + "title": "Sone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json new file mode 100644 index 00000000000..3c1a91976f0 --- /dev/null +++ b/homeassistant/components/zone/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer sone parametere" + } + }, + "title": "Sone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json new file mode 100644 index 00000000000..e649de4c75e --- /dev/null +++ b/homeassistant/components/zone/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa", + "passive": "Pasywnie", + "radius": "Promie\u0144" + }, + "title": "Zdefiniuj parametry strefy" + } + }, + "title": "Strefa" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt-BR.json b/homeassistant/components/zone/.translations/pt-BR.json new file mode 100644 index 00000000000..f2a41b0b267 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "O nome j\u00e1 existe" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + }, + "title": "Definir par\u00e2metros da zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json new file mode 100644 index 00000000000..2c3292e58c1 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nome j\u00e1 existente" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + }, + "title": "Definir os par\u00e2metros da zona" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json new file mode 100644 index 00000000000..6a017e9e1c3 --- /dev/null +++ b/homeassistant/components/zone/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "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." + }, + "step": { + "init": { + "data": { + "icon": "\u0417\u043d\u0430\u0447\u043e\u043a", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u0417\u043e\u043d\u0430" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sl.json b/homeassistant/components/zone/.translations/sl.json new file mode 100644 index 00000000000..1885cb5d2c8 --- /dev/null +++ b/homeassistant/components/zone/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime \u017ee obstaja" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radij" + }, + "title": "Dolo\u010dite parametre obmo\u010dja" + } + }, + "title": "Obmo\u010dje" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/sv.json b/homeassistant/components/zone/.translations/sv.json new file mode 100644 index 00000000000..55c5bcf7127 --- /dev/null +++ b/homeassistant/components/zone/.translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Namnet finns redan" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn", + "passive": "Passiv", + "radius": "Radie" + }, + "title": "Definiera zonparametrar" + } + }, + "title": "Zon" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/th.json b/homeassistant/components/zone/.translations/th.json new file mode 100644 index 00000000000..e39765f2da2 --- /dev/null +++ b/homeassistant/components/zone/.translations/th.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "name_exists": "\u0e21\u0e35\u0e0a\u0e37\u0e48\u0e2d\u0e19\u0e35\u0e49\u0e2d\u0e22\u0e39\u0e48\u0e41\u0e25\u0e49\u0e27" + }, + "step": { + "init": { + "data": { + "latitude": "\u0e40\u0e2a\u0e49\u0e19\u0e23\u0e38\u0e49\u0e07", + "longitude": "\u0e40\u0e2a\u0e49\u0e19\u0e41\u0e27\u0e07", + "name": "\u0e0a\u0e37\u0e48\u0e2d" + } + } + }, + "title": "\u0e42\u0e0b\u0e19" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/uk.json b/homeassistant/components/zone/.translations/uk.json new file mode 100644 index 00000000000..ce082d34a1c --- /dev/null +++ b/homeassistant/components/zone/.translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" + }, + "step": { + "init": { + "data": { + "icon": "\u0406\u043a\u043e\u043d\u043a\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u0438\u0439", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "\u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0437\u043e\u043d\u0438" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/vi.json b/homeassistant/components/zone/.translations/vi.json new file mode 100644 index 00000000000..7217944bd6b --- /dev/null +++ b/homeassistant/components/zone/.translations/vi.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "T\u00ean \u0111\u00e3 t\u1ed3n t\u1ea1i" + }, + "step": { + "init": { + "data": { + "icon": "Bi\u1ec3u t\u01b0\u1ee3ng", + "latitude": "V\u0129 \u0111\u1ed9", + "longitude": "Kinh \u0111\u1ed9", + "name": "T\u00ean", + "passive": "Th\u1ee5 \u0111\u1ed9ng", + "radius": "B\u00e1n k\u00ednh" + }, + "title": "X\u00e1c \u0111\u1ecbnh tham s\u1ed1 v\u00f9ng" + } + }, + "title": "V\u00f9ng" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json new file mode 100644 index 00000000000..6d06b68dad8 --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u56fe\u6807", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0", + "passive": "\u88ab\u52a8", + "radius": "\u534a\u5f84" + }, + "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" + } + }, + "title": "\u533a\u57df" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hant.json b/homeassistant/components/zone/.translations/zh-Hant.json new file mode 100644 index 00000000000..12c1141397d --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u5716\u793a", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31", + "passive": "\u88ab\u52d5", + "radius": "\u534a\u5f91" + }, + "title": "\u5b9a\u7fa9\u5340\u57df\u53c3\u6578" + } + }, + "title": "\u5340\u57df" + } +} \ No newline at end of file From 192b6566353b1c81d73a90b446d7d821a6ad5929 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 22 Jan 2020 19:55:34 -0500 Subject: [PATCH 221/393] Code Cleanup for Vizio component (#31076) * code cleanup * dont use named arguments for positional arguments * remove extra comma --- homeassistant/components/vizio/__init__.py | 10 +- homeassistant/components/vizio/config_flow.py | 69 +++++++------ .../components/vizio/media_player.py | 25 ++--- tests/components/vizio/test_config_flow.py | 96 +++++++++---------- 4 files changed, 100 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index ac02698dbfd..436ad829d94 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -50,22 +50,24 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +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(entry, platform) + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: +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(entry, platform) + hass.config_entries.async_forward_entry_unload(config_entry, platform) for platform in PLATFORMS ] ) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index a02ae22a46b..5500ec3db94 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -30,8 +30,11 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _config_flow_schema(input_dict: Dict[str, Any]) -> vol.Schema: +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( @@ -50,6 +53,32 @@ def _config_flow_schema(input_dict: Dict[str, Any]) -> vol.Schema: ) +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.""" @@ -58,14 +87,13 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + 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.import_schema = None - self.user_schema = None + self._user_schema = None self._must_show_form = None async def async_step_user( @@ -76,7 +104,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: # Store current values in case setup fails and user needs to edit - self.user_schema = _config_flow_schema(user_input) + 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): @@ -129,7 +157,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # Use user_input params as default values for schema if user_input is non-empty, otherwise use default schema - schema = self.user_schema or self.import_schema or _config_flow_schema({}) + schema = self._user_schema or _get_config_flow_schema() return self.async_show_form(step_id="user", data_schema=schema, errors=errors) @@ -158,9 +186,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_setup") - # Store import values in case setup fails so user can see error - self.import_schema = _config_flow_schema(import_config) - return await self.async_step_user(user_input=import_config) async def async_step_zeroconf( @@ -190,29 +215,3 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._must_show_form = True return await self.async_step_user(user_input=discovery_info) - - -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)) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index d143c4232bf..000d1baec2d 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -67,8 +67,8 @@ async def async_setup_entry( DEVICE_ID, host, name, - token, - VIZIO_DEVICE_CLASSES[device_class], + auth_token=token, + device_type=VIZIO_DEVICE_CLASSES[device_class], session=async_get_clientsession(hass, False), timeout=DEFAULT_TIMEOUT, ) @@ -86,9 +86,9 @@ async def async_setup_entry( ) raise PlatformNotReady - entity = VizioDevice(config_entry, device, name, volume_step, device_class) + entity = VizioDevice(config_entry, device, name, volume_step, device_class,) - async_add_entities([entity], True) + async_add_entities([entity], update_before_add=True) class VizioDevice(MediaPlayerDevice): @@ -122,7 +122,7 @@ class VizioDevice(MediaPlayerDevice): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) async def async_update(self) -> None: """Retrieve latest state of the device.""" - is_on = await self._device.get_power_state(False) + is_on = await self._device.get_power_state(log_api_exception=False) if is_on is None: self._available = False @@ -139,15 +139,15 @@ class VizioDevice(MediaPlayerDevice): self._state = STATE_ON - volume = await self._device.get_current_volume(False) + 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(False) + 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(False) + inputs = await self._device.get_inputs(log_api_exception=False) if inputs is not None: self._available_inputs = [input_.name for input_ in inputs] @@ -275,7 +275,7 @@ class VizioDevice(MediaPlayerDevice): async def async_volume_up(self) -> None: """Increasing volume of the device.""" - await self._device.vol_up(self._volume_step) + await self._device.vol_up(num=self._volume_step) if self._volume_level is not None: self._volume_level = min( @@ -284,7 +284,7 @@ class VizioDevice(MediaPlayerDevice): async def async_volume_down(self) -> None: """Decreasing volume of the device.""" - await self._device.vol_down(self._volume_step) + await self._device.vol_down(num=self._volume_step) if self._volume_level is not None: self._volume_level = max( @@ -296,9 +296,10 @@ class VizioDevice(MediaPlayerDevice): 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) + await self._device.vol_up(num=num) self._volume_level = volume + elif volume < self._volume_level: num = int(self._max_volume * (self._volume_level - volume)) - await self._device.vol_down(num) + await self._device.vol_down(num=num) self._volume_level = volume diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 196ef35469d..c82c7a8de0f 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -129,7 +129,9 @@ def vizio_cant_connect_fixture(): async def test_user_flow_minimum_fields( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test user config flow with minimum fields.""" # test form shows @@ -156,7 +158,9 @@ async def test_user_flow_minimum_fields( async def test_user_flow_all_fields( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test user config flow with all fields.""" # test form shows @@ -192,9 +196,7 @@ async def test_options_flow(hass: HomeAssistantType) -> None: assert not entry.options - result = await hass.config_entries.options.async_init( - entry.entry_id, context={"source": "test"}, data=None - ) + 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" @@ -209,7 +211,9 @@ async def test_options_flow(hass: HomeAssistantType) -> None: async def test_user_host_already_configured( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test host is already configured during user setup.""" entry = MockConfigEntry( @@ -220,14 +224,7 @@ async def test_user_host_already_configured( fail_entry[CONF_NAME] = "newtestname" 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=fail_entry + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -235,7 +232,9 @@ async def test_user_host_already_configured( async def test_user_name_already_configured( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test name is already configured during user setup.""" entry = MockConfigEntry( @@ -247,13 +246,7 @@ async def test_user_name_already_configured( fail_entry[CONF_HOST] = "0.0.0.0" 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"], fail_entry + DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -261,7 +254,9 @@ async def test_user_name_already_configured( async def test_user_esn_already_exists( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + 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 @@ -283,36 +278,25 @@ async def test_user_esn_already_exists( async def test_user_error_on_could_not_connect( - hass: HomeAssistantType, vizio_cant_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} + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - 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"], 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, vizio_bypass_setup + 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} - ) - - 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"], MOCK_INVALID_TV_CONFIG + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_INVALID_TV_CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -320,12 +304,14 @@ async def test_user_error_on_tv_needs_token( async def test_import_flow_minimum_fields( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + 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": "import"}, + context={"source": SOURCE_IMPORT}, data=vol.Schema(VIZIO_SCHEMA)( {CONF_HOST: HOST, CONF_DEVICE_CLASS: DEVICE_CLASS_SPEAKER} ), @@ -340,12 +326,14 @@ async def test_import_flow_minimum_fields( async def test_import_flow_all_fields( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + 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": "import"}, + context={"source": SOURCE_IMPORT}, data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) @@ -359,7 +347,9 @@ async def test_import_flow_all_fields( async def test_import_entity_already_configured( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test entity is already configured during import setup.""" entry = MockConfigEntry( @@ -371,7 +361,7 @@ async def test_import_entity_already_configured( fail_entry = vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG.copy()) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "import"}, data=fail_entry + DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -379,7 +369,9 @@ async def test_import_entity_already_configured( async def test_import_flow_update_options( - hass: HomeAssistantType, vizio_connect, vizio_bypass_update + 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( @@ -388,6 +380,7 @@ async def test_import_flow_update_options( 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 @@ -409,7 +402,10 @@ async def test_import_flow_update_options( async def test_zeroconf_flow( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup, vizio_guess_device_type + 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() @@ -437,7 +433,9 @@ async def test_zeroconf_flow( async def test_zeroconf_flow_already_configured( - hass: HomeAssistantType, vizio_connect, vizio_bypass_setup + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_setup: pytest.fixture, ) -> None: """Test entity is already configured during zeroconf setup.""" entry = MockConfigEntry( From 80887d757a3fab9a728101774134b731e36e2394 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Jan 2020 17:46:12 -0800 Subject: [PATCH 222/393] Simplify automation services (#30996) * Simplify automation services * Empty commit to re-trigger build Co-authored-by: Franck Nijhof --- .../components/automation/__init__.py | 87 ++++--------------- 1 file changed, 18 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9f51127cf99..70b8b26fa2c 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,5 +1,4 @@ """Allow to set up simple automation rules via the config file.""" -import asyncio from functools import partial import importlib import logging @@ -24,7 +23,6 @@ from homeassistant.core import Context, CoreState, HomeAssistant 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 @@ -110,15 +108,6 @@ PLATFORM_SCHEMA = vol.All( ), ) -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): @@ -136,42 +125,25 @@ async def async_setup(hass, config): 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.""" @@ -180,33 +152,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 From 288574b8d12d27fcfa9343fd60bd240867824ebe Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 22 Jan 2020 18:48:20 -0700 Subject: [PATCH 223/393] Remove monitored conditions from OpenUV (#31019) * Remove monitored conditions from OpenUV * Code review comments --- homeassistant/components/openuv/__init__.py | 124 +++++------------- .../components/openuv/binary_sensor.py | 10 +- .../components/openuv/config_flow.py | 2 +- homeassistant/components/openuv/sensor.py | 46 ++++++- 4 files changed, 80 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 2e4e89a00e6..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.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 2790bc7ede0..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,21 +16,24 @@ 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" +BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses")} + async def async_setup_entry(hass, entry, async_add_entities): """Set up an OpenUV sensor based on a config entry.""" 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) 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 00954646708..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,17 +42,52 @@ UV_LEVEL_HIGH = "High" UV_LEVEL_MODERATE = "Moderate" UV_LEVEL_LOW = "Low" +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): """Set up a Nest sensor based on a config entry.""" 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) From 572b81e7e06965d2aa1a367dad6c0e608cdd0f9d Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Wed, 22 Jan 2020 20:49:00 -0500 Subject: [PATCH 224/393] Add myself to owners for components I contributed (#31020) * Add myself to owners for components I contributed * Update CODEOWNERS --- CODEOWNERS | 2 ++ homeassistant/components/greeneye_monitor/manifest.json | 2 +- homeassistant/components/sisyphus/manifest.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9636f324769..3a68c6c3b34 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,6 +129,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 @@ -297,6 +298,7 @@ homeassistant/components/shodan/* @fabaff 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 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/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"] } From b7678f526c0af9cacdb8315ccd1cf57c5c3e7ae8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 23 Jan 2020 02:50:24 +0100 Subject: [PATCH 225/393] Deprecate hook integration (#31046) --- homeassistant/components/hook/__init__.py | 1 - homeassistant/components/hook/manifest.json | 8 -- homeassistant/components/hook/switch.py | 130 -------------------- 3 files changed, 139 deletions(-) delete mode 100644 homeassistant/components/hook/__init__.py delete mode 100644 homeassistant/components/hook/manifest.json delete mode 100644 homeassistant/components/hook/switch.py 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 From d8eca8e3038243ba9f8f2e27eb028487c0eef77a Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Wed, 22 Jan 2020 18:57:55 -0700 Subject: [PATCH 226/393] Add keep-alive which is required for some hlk-sw16 variants (#31062) --- homeassistant/components/hlk_sw16/__init__.py | 2 ++ homeassistant/components/hlk_sw16/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 8b7423ce5a3..c6b408d33cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,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 From 3fc86988fad916c05ff4137c9d67946ca11fb752 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Thu, 23 Jan 2020 03:09:52 +0100 Subject: [PATCH 227/393] pydaikin version bump (#31080) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index c6b408d33cf..8639b765bfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1182,7 +1182,7 @@ 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59e3424b986..30f5940ebb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -414,7 +414,7 @@ pychromecast==4.0.1 pycoolmasternet==0.0.4 # homeassistant.components.daikin -pydaikin==1.6.1 +pydaikin==1.6.2 # homeassistant.components.deconz pydeconz==68 From 73a55825aff66aeb872dc580031a0c1eca5a2adf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 22 Jan 2020 21:49:47 -0700 Subject: [PATCH 228/393] Remove monitored conditions from RainMachine (#31066) * Remove monitored conditions from RainMachine * Migrate config entry * Revert "Migrate config entry" This reverts commit 84fcf5120ff317d088761aff70402608d58d7175. * Code review comments * Disable some entities by default --- .../components/rainmachine/__init__.py | 143 +----------------- .../components/rainmachine/binary_sensor.py | 47 ++++-- .../components/rainmachine/sensor.py | 83 ++++++++-- 3 files changed, 114 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 5dffecb0488..5e95b11f2e4 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -10,15 +10,11 @@ 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.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -57,82 +53,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 +76,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 +83,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( @@ -227,14 +141,7 @@ async def async_setup_entry(hass, config_entry): 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), + client, config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), ) await rainmachine.async_update() except RainMachineError as err: @@ -364,54 +271,20 @@ 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, client, default_zone_runtime): """Initialize.""" - self.binary_sensor_conditions = binary_sensor_conditions self.client = client self.data = {} self.default_zone_runtime = default_zone_runtime self.device_mac = self.client.mac - self.sensor_conditions = sensor_conditions async def async_update(self): """Update sensor/binary sensor data.""" - - 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() + tasks = { + PROVISION_SETTINGS: self.client.provisioning.settings(), + RESTRICTIONS_CURRENT: self.client.restrictions.current(), + RESTRICTIONS_UNIVERSAL: self.client.restrictions.universal(), + } results = await asyncio.gather(*tasks.values(), return_exceptions=True) for operation, result in zip(tasks, results): diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 2d7ab613554..8362c31b11f 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -6,37 +6,50 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - BINARY_SENSORS, DATA_CLIENT, 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" + +BINARY_SENSORS = { + TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True), + TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True), + TYPE_FREEZE_PROTECTION: ("Freeze Protection", "mdi:weather-snowy", True), + TYPE_HOT_DAYS: ("Extra Water on Hot Days", "mdi:thermometer-lines", True), + TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel", False), + TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False), + TYPE_RAINDELAY: ("Rain Delay Restrictions", "mdi:cancel", False), + TYPE_RAINSENSOR: ("Rain Sensor Restrictions", "mdi:cancel", False), + TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel", False), +} + 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] + for sensor_type, (name, icon, enabled_by_default) in BINARY_SENSORS.items(): binary_sensors.append( - RainMachineBinarySensor(rainmachine, sensor_type, name, icon) + RainMachineBinarySensor( + rainmachine, sensor_type, name, icon, enabled_by_default + ) ) async_add_entities(binary_sensors, True) @@ -45,15 +58,21 @@ async def async_setup_entry(hass, entry, async_add_entities): 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): """Initialize the sensor.""" super().__init__(rainmachine) + 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.""" diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index bc1c734b98e..30acacafad0 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -10,27 +10,75 @@ from . import ( 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" + +SENSORS = { + TYPE_FLOW_SENSOR_CLICK_M3: ( + "Flow Sensor Clicks", + "mdi:water-pump", + "clicks/m^3", + None, + False, + ), + TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( + "Flow Sensor Consumed Liters", + "mdi:water-pump", + "liter", + None, + False, + ), + TYPE_FLOW_SENSOR_START_INDEX: ( + "Flow Sensor Start Index", + "mdi:water-pump", + "index", + None, + False, + ), + TYPE_FLOW_SENSOR_WATERING_CLICKS: ( + "Flow Sensor Clicks", + "mdi:water-pump", + "clicks", + None, + False, + ), + TYPE_FREEZE_TEMP: ( + "Freeze Protect Temperature", + "mdi:thermometer", + "°C", + "temperature", + True, + ), +} + 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] + for ( + sensor_type, + (name, icon, unit, device_class, enabled_by_default), + ) in SENSORS.items(): sensors.append( - RainMachineSensor(rainmachine, sensor_type, name, icon, unit, device_class) + RainMachineSensor( + rainmachine, + sensor_type, + name, + icon, + unit, + device_class, + enabled_by_default, + ) ) async_add_entities(sensors, True) @@ -39,17 +87,32 @@ async def async_setup_entry(hass, entry, async_add_entities): 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, + ): """Initialize.""" super().__init__(rainmachine) 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.""" From c71ae090fc5375c9b02a8087b449431da37e8a58 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 23 Jan 2020 08:30:06 +0000 Subject: [PATCH 229/393] Add sighthound integration (#28824) * Add component files * Add test state * Adds person detected event * Update CODEOWNERS * Updates requirements * remove unused datetime * Bump sighthound version * Update CODEOWNERS * Update CODEOWNERS * Create requirements_test_all.txt * Address reviewer comments * Add test for bad_api_key --- CODEOWNERS | 1 + .../components/sighthound/__init__.py | 1 + .../components/sighthound/image_processing.py | 120 ++++++++++++++++++ .../components/sighthound/manifest.json | 12 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sighthound/__init__.py | 1 + .../sighthound/test_image_processing.py | 93 ++++++++++++++ 8 files changed, 234 insertions(+) create mode 100644 homeassistant/components/sighthound/__init__.py create mode 100644 homeassistant/components/sighthound/image_processing.py create mode 100644 homeassistant/components/sighthound/manifest.json create mode 100644 tests/components/sighthound/__init__.py create mode 100644 tests/components/sighthound/test_image_processing.py diff --git a/CODEOWNERS b/CODEOWNERS index 3a68c6c3b34..5cc80797c52 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -295,6 +295,7 @@ 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 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/requirements_all.txt b/requirements_all.txt index 8639b765bfd..6377a4a3af8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1809,6 +1809,9 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.shodan shodan==1.21.2 +# homeassistant.components.sighthound +simplehound==0.3 + # homeassistant.components.simplepush simplepush==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30f5940ebb0..0d440259ca7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -584,6 +584,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.sentry sentry-sdk==0.13.5 +# homeassistant.components.sighthound +simplehound==0.3 + # homeassistant.components.simplisafe simplisafe-python==6.0.0 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 From c5636525748c5602d0dcc53dced91e5da7da3559 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Jan 2020 11:55:29 +0100 Subject: [PATCH 230/393] Cleanup of PR and issue templates (#31070) * Remove old issue template * Small wording improvement * Consistency * Configuration fix * Process review suggestions --- .github/ISSUE_TEMPLATE.md | 48 ---------------------------- .github/ISSUE_TEMPLATE/BUG_REPORT.md | 28 ++++++++-------- .github/ISSUE_TEMPLATE/config.yml | 4 +-- 3 files changed, 16 insertions(+), 64 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md 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 index 34c023c4410..977abc6ef6b 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -3,24 +3,24 @@ name: Report a bug with Home Assistant about: Report an issue with Home Assistant --- ## The problem ## Environment - Home Assistant release with the issue: @@ -31,9 +31,9 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w ## Problem-relevant `configuration.yaml` ```yaml @@ -42,7 +42,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w ## Traceback/Error logs ```txt diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index de21025840d..5b0b8c46e96 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,9 +2,9 @@ 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 backed. Please report issues with the UI in the frontend repository. + 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 - urls: https://github.com/home-assistant/home-assistant.io/issues + 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 From e9eaa6536dace017c12fb0e0dd90da12599bf0c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Jan 2020 17:10:40 +0100 Subject: [PATCH 231/393] Upgrade sqlalchemy to 1.3.13 (#31101) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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/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/package_constraints.txt b/homeassistant/package_constraints.txt index 679af5bf3f4..4dc67061954 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 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/requirements_all.txt b/requirements_all.txt index 6377a4a3af8..a32e7a5376a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1885,7 +1885,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.12 +sqlalchemy==1.3.13 # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d440259ca7..6586db5d008 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -604,7 +604,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.12 +sqlalchemy==1.3.13 # homeassistant.components.starline starline==0.1.3 From bfea9863f19e3b53c433c975d6fa99ec1aa4ac4f Mon Sep 17 00:00:00 2001 From: mindigmarton Date: Thu, 23 Jan 2020 17:12:20 +0100 Subject: [PATCH 232/393] Update emulated_roku to 0.2.1 (#31100) --- homeassistant/components/emulated_roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index a32e7a5376a..46185c445cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -474,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6586db5d008..ef6fe126c64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 894b841a1555ab71735a4ec8878fccd7520a5de7 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 23 Jan 2020 18:02:44 +0100 Subject: [PATCH 233/393] Fix HomeKit window covering to support float numbers in the position (#31081) * Fix HomeKit window covering to support float numbers in the position * Fix HomeKit window covering to cast current position to an integer value --- homeassistant/components/homekit/type_covers.py | 3 ++- tests/components/homekit/test_type_covers.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) 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/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 From 1a1ef7680d26a09f987052b5e0f2b943d6b4bba4 Mon Sep 17 00:00:00 2001 From: tetienne Date: Thu, 23 Jan 2020 18:18:59 +0100 Subject: [PATCH 234/393] Add temperature support to light template (#30595) * Add temperature support * Use guard clause --- homeassistant/components/template/light.py | 124 ++++++++++++++++----- tests/components/template/test_light.py | 94 +++++++++++++++- 2 files changed, 187 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 70c097d0b2b..2fb240e1180 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,6 +90,7 @@ 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) @@ -101,6 +110,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 +140,8 @@ class LightTemplate(Light): level_action, level_template, entity_ids, + temperature_action, + temperature_template, ): """Initialize the light.""" self.hass = hass @@ -146,11 +159,16 @@ 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 @@ -164,12 +182,19 @@ class LightTemplate(Light): self._entity_picture_template.hass = self.hass if self._availability_template is not None: self._availability_template.hass = self.hass + if self._temperature_template is not None: + self._temperature_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 +203,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 +249,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 +277,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 +312,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 +353,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/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"): From 910a0bc8709ba4bfe453cd010c5e3d26e1d7a795 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 24 Jan 2020 02:14:47 +0800 Subject: [PATCH 235/393] Allow framerates less than 1 (#31108) --- homeassistant/components/generic/camera.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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, } ) From bdd73b03b54b2bbd6963de17bc04f24f064d2d42 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 23 Jan 2020 19:49:01 +0100 Subject: [PATCH 236/393] Upgrade shodan to 1.21.3 (#31111) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index dac6efdfce2..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.2"], + "requirements": ["shodan==1.21.3"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 46185c445cd..37f6356d441 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1807,7 +1807,7 @@ sentry-sdk==0.13.5 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.21.2 +shodan==1.21.3 # homeassistant.components.sighthound simplehound==0.3 From b743f9b8f61228bdd1e4d0bde6c28fa89458f5de Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 23 Jan 2020 20:21:19 +0100 Subject: [PATCH 237/393] Add update_entry dict to unique ID flow abort helper (#31090) * Add update_entry dict to unique ID flow abort helper * Process review suggestions * Process review suggestions * Add additional test --- homeassistant/config_entries.py | 12 ++++-- tests/test_config_entries.py | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) 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/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")) From fc95744bb7cb69d7aeb0e4b3dfcfc3b1f5a51e7e Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Thu, 23 Jan 2020 21:43:30 +0100 Subject: [PATCH 238/393] Change Samsung TV state detection (#30236) * Changed Samsung TV state detection * Trying codecov fix * Update Rewritten * Changed update method * Timeout handling * Fixed autodetect tests * Fixed last test * Added test to complete codecov * Removed state settings in send_key method * Fixed pylint * Fixed some tests * codecov fix --- .../components/samsungtv/media_player.py | 31 ++- .../components/samsungtv/test_media_player.py | 226 +++++++++++------- 2 files changed, 163 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index e7153a7f5d4..aca54838a99 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -98,7 +98,27 @@ class SamsungTVDevice(MediaPlayerDevice): 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.""" @@ -128,19 +148,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 (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 ( diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 3afedda746e..2b9f379515d 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -135,16 +135,36 @@ 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")) + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[OSError("Boom"), mock.DEFAULT], + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + await setup_samsungtv(hass, MOCK_CONFIG) - 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() + 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 + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_update_unhandled_response(hass, remote, mock_now): + """Testing update tv unhandled response exception.""" + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[exceptions.UnhandledResponse("Boom"), mock.DEFAULT], + ), patch("homeassistant.components.samsungtv.config_flow.socket"): + await setup_samsungtv(hass, MOCK_CONFIG) + + 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): @@ -155,8 +175,10 @@ async def test_send_key(hass, remote): ) 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 @@ -182,12 +204,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 @@ -221,7 +244,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): @@ -336,8 +359,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): @@ -347,8 +372,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): @@ -361,8 +388,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): @@ -372,8 +401,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): @@ -383,8 +414,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): @@ -394,8 +427,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): @@ -405,8 +440,10 @@ 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_turnon(hass, remote, delay): @@ -450,71 +487,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): @@ -527,19 +577,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 From 7fed328e1c3993f4e0757c2b543ef581f2ec4c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Thu, 23 Jan 2020 22:45:06 +0100 Subject: [PATCH 239/393] Use speak2mary for MaryTTS integration and enable sound effects (#30805) * Use speak2mary for MaryTTS integration and enable sound effects * Replace static defaults for effects with user configured ones --- .../components/marytts/manifest.json | 4 +- homeassistant/components/marytts/tts.py | 86 ++++++--------- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/marytts/test_tts.py | 102 +++++++++++------- 5 files changed, 108 insertions(+), 90 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 37f6356d441..d4acfc264da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1871,6 +1871,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6fe126c64..5ea72741fd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -602,6 +602,9 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.marytts +speak2mary==1.4.0 + # homeassistant.components.recorder # homeassistant.components.sql sqlalchemy==1.3.13 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)) From 47b708974d060bfe098f420d991b7acbb136c414 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jan 2020 00:50:59 +0100 Subject: [PATCH 240/393] Add disabled entities support to AdGuard (#31106) --- homeassistant/components/adguard/__init__.py | 17 ++++++++++++++--- homeassistant/components/adguard/sensor.py | 18 +++++++++++++++--- homeassistant/components/adguard/switch.py | 18 +++++++++++++----- 3 files changed, 42 insertions(+), 11 deletions(-) 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.""" From 6da2904e12d7bc8cd5637702f3790af7a232d23b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 24 Jan 2020 00:31:41 +0000 Subject: [PATCH 241/393] [ci skip] Translation update --- .../components/almond/.translations/it.json | 1 + .../components/brother/.translations/it.json | 8 +++++ .../components/brother/.translations/lb.json | 8 +++++ .../components/brother/.translations/no.json | 8 +++++ .../components/brother/.translations/ru.json | 8 +++++ .../components/elgato/.translations/lb.json | 2 +- .../components/elgato/.translations/ru.json | 2 +- .../components/esphome/.translations/lb.json | 2 +- .../components/vizio/.translations/it.json | 31 ++++++++++++++++--- .../components/wled/.translations/lb.json | 2 +- .../components/wled/.translations/ru.json | 2 +- 11 files changed, 65 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index d2d0314fba5..dd722907c6a 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -7,6 +7,7 @@ }, "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": { 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/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/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/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/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 fd2f6579407..454e6e78d84 100644 --- a/homeassistant/components/elgato/.translations/ru.json +++ b/homeassistant/components/elgato/.translations/ru.json @@ -19,7 +19,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 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": "Elgado Key Light" 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/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index edbe662d8a0..83c72912618 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -1,9 +1,16 @@ { "config": { "abort": { - "already_setup": "Questa voce \u00e8 gi\u00e0 stata configurata." + "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": "Host gi\u00e0 configurato.", "name_exists": "Nome gi\u00e0 configurato.", "tv_needs_token": "Quando Device Type \u00e8 `tv`, \u00e8 necessario un token di accesso valido." @@ -12,9 +19,25 @@ "user": { "data": { "access_token": "Token di accesso", - "device_class": "Tipo di dispositivo" - } + "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/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" From 1de003487e6df7bcccc5231d92cc217f0f2927b8 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 24 Jan 2020 01:39:28 -0500 Subject: [PATCH 242/393] Fix connection failure log message in async_setup_entry for Vizio component (#31097) * remove connection check during setup since it is already done during config flow * revert change and fix log message * demote connection setup to warning --- homeassistant/components/vizio/media_player.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 000d1baec2d..b2f529bce10 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -76,17 +76,19 @@ async def async_setup_entry( if not await device.can_connect(): fail_auth_msg = "" if token: - fail_auth_msg = "and auth token '{token}' are correct." + fail_auth_msg = f"and auth token '{token}' are correct." else: fail_auth_msg = "is correct." - _LOGGER.error( - "Failed to connect to Vizio device, please check if host '{host}'" - "is valid and available. Also check if device class '{device_class}' %s", + _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, ) raise PlatformNotReady - entity = VizioDevice(config_entry, device, name, volume_step, device_class,) + entity = VizioDevice(config_entry, device, name, volume_step, device_class) async_add_entities([entity], update_before_add=True) From ca591d591fd8c03bb5e9bc12934e20f5cbcf6d81 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Fri, 24 Jan 2020 09:07:36 +0100 Subject: [PATCH 243/393] Upgrade youtube_dl to version 2020.01.24 (#31124) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 9b02e8266ab..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.15"], + "requirements": ["youtube_dl==2020.01.24"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index d4acfc264da..b6143ff2ab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2103,7 +2103,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.01.15 +youtube_dl==2020.01.24 # homeassistant.components.zengge zengge==0.2 From b4f3415eb956268298ef5adb917e0ead31bf3951 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jan 2020 00:20:47 -0800 Subject: [PATCH 244/393] Update cast to 4.1.1 (#31113) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index b6143ff2ab5..643972487e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1164,7 +1164,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ea72741fd9..1451da94ff0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -408,7 +408,7 @@ 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 From 6ff572d1ddc6faae4c48aa2fb66ce8a3e6e2f117 Mon Sep 17 00:00:00 2001 From: Desausoi Laurent Date: Fri, 24 Jan 2020 12:38:35 +0100 Subject: [PATCH 245/393] Add Buienradar camera for Belgium (#30399) * Buienradar Camera for Belgium * Voluptuous check for country code * Black formatting * Testing for Buienradar Belgium * Changed MULTIPLE CHOICE variable name * Changes from frenck review --- homeassistant/components/buienradar/camera.py | 21 +++++++++++--- tests/components/buienradar/test_camera.py | 29 ++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) 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/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) From 30249d14282eec04356da8a335f5478a66d0dfa5 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Fri, 24 Jan 2020 12:40:00 +0100 Subject: [PATCH 246/393] Fix Template components to process entity_id configuration option (#31084) * Fix Template Cover component to process entity_id configuration option * Fix Template Light component to process entity_id configuration option * Empty commit to re-trigger build Co-authored-by: Franck Nijhof --- homeassistant/components/template/cover.py | 4 +++- homeassistant/components/template/light.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index f6678067d70..13828b960fd 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( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 2fb240e1180..0f70f8a358b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -94,7 +94,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= } 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( From 1effd605a566fcb0fbf0c4e9aaba89a0a1a4f142 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 24 Jan 2020 17:33:23 +0100 Subject: [PATCH 247/393] Remove unused async_setup_platform (#31132) --- .../components/homematicip_cloud/alarm_control_panel.py | 7 ------- .../components/homematicip_cloud/binary_sensor.py | 7 ------- homeassistant/components/homematicip_cloud/climate.py | 7 ------- homeassistant/components/homematicip_cloud/cover.py | 7 ------- homeassistant/components/homematicip_cloud/light.py | 7 ------- homeassistant/components/homematicip_cloud/sensor.py | 7 ------- homeassistant/components/homematicip_cloud/switch.py | 7 ------- homeassistant/components/homematicip_cloud/weather.py | 7 ------- 8 files changed, 56 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f9a91203426..dea84b90bd6 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -26,13 +26,6 @@ _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: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 3efd4ad91bc..5a679626679 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -75,13 +75,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index e3c922dc577..d932d5c3f0a 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -43,13 +43,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 32f38637e36..2e6d8b546bc 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -27,13 +27,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 79083f031ae..f35118d0d84 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -34,13 +34,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index a8ca3d17eb9..50a1c4ae34a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -56,13 +56,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 6fdb0b8c95c..bce85592891 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -26,13 +26,6 @@ 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: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index ebc7eacf78e..f6ea95ab117 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -37,13 +37,6 @@ 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: From 4571cf01e2cd8d07fb7744ab97e79970f8ce88cc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jan 2020 18:07:53 +0100 Subject: [PATCH 248/393] Update Hue existing config entry with discovery data (#31087) * Update Hue existing config entry with discovery data * Updated method documentation comments * Update implementation to match latest dev * Use named argument for clarity --- homeassistant/components/hue/config_flow.py | 7 +- tests/components/hue/test_config_flow.py | 106 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) 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/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index a5bf143775a..b1f6785b0a7 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -214,6 +214,26 @@ async def test_flow_link_timeout(hass): 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" + assert result["errors"] == {"base": "linking"} + + async def test_flow_link_button_not_pressed(hass): """Test config flow .""" mock_bridge = get_mock_bridge( @@ -303,6 +323,36 @@ async def test_bridge_ssdp_emulated_hue(hass): 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" + assert result["reason"] == "not_hue_bridge" + + async def test_bridge_ssdp_espalexa(hass): """Test if discovery info is from an Espalexa based device.""" result = await hass.config_entries.flow.async_init( @@ -417,6 +467,22 @@ async def test_bridge_homekit(hass): 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( @@ -431,3 +497,43 @@ async def test_bridge_homekit_already_configured(hass): 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" From 7e4b9adc3d0239a20a17033d0e89360c8021e67a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jan 2020 18:47:22 +0100 Subject: [PATCH 249/393] Rewrite of Spotify integration (#30717) * Rewrite of Spotify integration * Update homeassistant/components/spotify/config_flow.py Co-Authored-By: Paulus Schoutsen * Remove configurator dependency * Strip whitespace from device model in case Spotify product is missing * Ensure domain dict exists in hass data on setup entry * Simply config validation for client id and secret * Abort flow on any exception from spotipy * Add tests for config flow * Gen requirements all * Add test package __init__ * Remove Spotify from coveragerc * Made alias handling more robuust * Fix supported_features for Spotify free and open accounts * Improve error message in the logs * Re-implement Spotify media_player * Change media content type when play a playlist * Process review suggestions * Move Spotify init, static current user and supported_features * Remove unneeded me call * Remove playlist content type due to frontend issues * Improve playlist handling, when context is missing * Handle entity disabled correctly * Handle being offline/unavailable correctly * Bump Spotipy to 2.7.1 * Update coverage RC, mark integration silver * Remove URI limitation, lib supports all Spotify URI's now * Final cleanup * Addresses Pylint error Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/spotify/.translations/en.json | 18 + homeassistant/components/spotify/__init__.py | 98 ++- .../components/spotify/config_flow.py | 57 ++ homeassistant/components/spotify/const.py | 10 + .../components/spotify/manifest.json | 9 +- .../components/spotify/media_player.py | 588 ++++++++---------- homeassistant/components/spotify/strings.json | 18 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/spotify/__init__.py | 1 + tests/components/spotify/test_config_flow.py | 139 +++++ 15 files changed, 625 insertions(+), 324 deletions(-) create mode 100644 homeassistant/components/spotify/.translations/en.json create mode 100644 homeassistant/components/spotify/config_flow.py create mode 100644 homeassistant/components/spotify/const.py create mode 100644 homeassistant/components/spotify/strings.json create mode 100644 tests/components/spotify/__init__.py create mode 100644 tests/components/spotify/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 91f99fe84d4..daf94cf03b3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -662,6 +662,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/* diff --git a/CODEOWNERS b/CODEOWNERS index 5cc80797c52..fba45fba5ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -315,6 +315,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 diff --git a/homeassistant/components/spotify/.translations/en.json b/homeassistant/components/spotify/.translations/en.json new file mode 100644 index 00000000000..316fbd946db --- /dev/null +++ b/homeassistant/components/spotify/.translations/en.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/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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2a013b16ae2..31c326f4d13 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -78,6 +78,7 @@ FLOWS = [ "soma", "somfy", "sonos", + "spotify", "starline", "tellduslive", "tesla", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8d3bff42d12..9817dd69f81 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -27,6 +27,9 @@ ZEROCONF = { "_printer._tcp.local.": [ "brother" ], + "_spotify-connect._tcp.local.": [ + "spotify" + ], "_viziocast._tcp.local.": [ "vizio" ], diff --git a/requirements_all.txt b/requirements_all.txt index 643972487e8..93d879f6629 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1884,7 +1884,7 @@ 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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1451da94ff0..0ed261e21ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -605,6 +605,9 @@ 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.13 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" From 98bac43228ed920e4eb516aaabf050065a6a5106 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jan 2020 10:25:46 -0800 Subject: [PATCH 250/393] Validate coveragerc with hassfest (#31112) * Validate coveragerc * Test if files exists * Print progress * Flush --- .coveragerc | 9 ------- script/hassfest/__main__.py | 15 ++++++++++- script/hassfest/coverage.py | 50 +++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 script/hassfest/coverage.py diff --git a/.coveragerc b/.coveragerc index daf94cf03b3..c24cb43ffb9 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 @@ -261,7 +259,6 @@ omit = 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 @@ -306,7 +303,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 @@ -534,7 +530,6 @@ 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 @@ -728,7 +723,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 @@ -753,7 +747,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/* @@ -831,7 +824,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 @@ -839,7 +831,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/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)) From 9795449d22783e77a0ca7b745f15c89a830c5cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 24 Jan 2020 23:27:15 +0200 Subject: [PATCH 251/393] Make pylint fail on informational messages too (#31136) Refs https://github.com/PyCQA/pylint/issues/3250 --- pylintrc | 1 + requirements_test.txt | 1 + 2 files changed, 2 insertions(+) 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_test.txt b/requirements_test.txt index 030e3dc60ce..34582e4f773 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,6 +10,7 @@ mypy==0.761 pre-commit==1.21.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 From c0bc4bb550b8bb9a48903aadce878736897c3aec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jan 2020 23:36:22 +0100 Subject: [PATCH 252/393] Add logo & icon support to Manifest (#31131) * Add logo & icon support to Manifest * Add URL validation --- homeassistant/loader.py | 10 ++++++++++ script/hassfest/manifest.py | 2 ++ 2 files changed, 12 insertions(+) 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/script/hassfest/manifest.py b/script/hassfest/manifest.py index e6bd6551786..5ce9a1f75d5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -29,6 +29,8 @@ MANIFEST_SCHEMA = vol.Schema( 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 } ) From f4626375f3e5a44d62fc08f2b9e833e456c61147 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jan 2020 14:59:54 -0800 Subject: [PATCH 253/393] Fix when you have two wemo devices (#31139) --- homeassistant/components/wemo/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 3e4081ae300..9cac85dee09 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -149,7 +149,7 @@ async def async_setup_entry(hass, entry): ) elif component in hass.data[DOMAIN]["pending"]: - hass.data[DOMAIN]["pending"].append(device) + hass.data[DOMAIN]["pending"][component].append(device) else: async_dispatcher_send( From 71ae4b262352efe8e5242b960169b9064aa3dd7e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 25 Jan 2020 00:31:41 +0000 Subject: [PATCH 254/393] [ci skip] Translation update --- .../components/brother/.translations/fr.json | 5 +++ .../components/brother/.translations/pl.json | 8 +++++ .../components/spotify/.translations/da.json | 18 ++++++++++ .../components/spotify/.translations/en.json | 34 +++++++++---------- .../components/spotify/.translations/fr.json | 17 ++++++++++ .../components/spotify/.translations/ru.json | 18 ++++++++++ 6 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/spotify/.translations/da.json create mode 100644 homeassistant/components/spotify/.translations/fr.json create mode 100644 homeassistant/components/spotify/.translations/ru.json diff --git a/homeassistant/components/brother/.translations/fr.json b/homeassistant/components/brother/.translations/fr.json index db3c7f48ce7..99d49cc3bd8 100644 --- a/homeassistant/components/brother/.translations/fr.json +++ b/homeassistant/components/brother/.translations/fr.json @@ -17,6 +17,11 @@ }, "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" 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/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 index 316fbd946db..b26b2b6daf5 100644 --- a/homeassistant/components/spotify/.translations/en.json +++ b/homeassistant/components/spotify/.translations/en.json @@ -1,18 +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" - } -} + "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/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/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 From 0eee152386d4cb6c11445b40af9c0539c55dc594 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 24 Jan 2020 20:57:58 -0500 Subject: [PATCH 255/393] Include supported states in Alexa SecurityPanelController configuration object (#31120) * Update Security Panel Controller. * Update Security Panel Controller. * Sort imports. --- .../components/alexa/capabilities.py | 22 +++++++++++++++++-- homeassistant/components/alexa/handlers.py | 12 ++++++++-- tests/components/alexa/test_smart_home.py | 14 ++++++++++-- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 080a8c39147..8b93b911fc4 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -10,6 +10,11 @@ from homeassistant.components import ( 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 ( @@ -1082,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): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 8bd52b1e40b..f67e2e259d0 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -908,8 +908,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 +931,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 +950,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", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 161f69287d4..1510474aa6e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2353,6 +2353,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) @@ -2369,6 +2370,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") @@ -2420,6 +2425,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) @@ -2458,11 +2464,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) From cf165cc35fe4ffa80ab4aea0b85e85ee85903168 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 24 Jan 2020 21:19:40 -0700 Subject: [PATCH 256/393] Make SimpliSafe integration more resilient to SimpliSafe cloud issues (#31116) * Make SimpliSafe integration more resilient to SimpliSafe cloud issues * Clear emergency refresh token * Stop listening when appropriate * Cleanup * Saving refresh token should happen after all updates * Code review --- .../components/simplisafe/__init__.py | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index d5538e6a372..b55489f4d67 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -307,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 @@ -316,6 +317,28 @@ class SimpliSafe: 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 + 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 @@ -327,10 +350,10 @@ class SimpliSafe: self.last_event_data[system.system_id] = 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.""" @@ -338,6 +361,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.""" From 550aa6a0a55c6912a65edf011fe2e42b15a99c88 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 24 Jan 2020 22:31:14 -0700 Subject: [PATCH 257/393] Add smarter API usage for RainMachine (#31115) * Make RainMachine smarter with API usage * Remove debug statements * Fix deregistration * Code review comments * Code review * Use an asyncio.Lock * Remove unnecessary guard clause * Ensure registation lock per API category --- .../components/rainmachine/__init__.py | 92 ++++++++++++++----- .../components/rainmachine/binary_sensor.py | 67 ++++++++++---- .../components/rainmachine/sensor.py | 33 +++++-- 3 files changed, 142 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 5e95b11f2e4..e2602e376a6 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SSL, ) +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 @@ -127,7 +128,6 @@ 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) @@ -141,9 +141,11 @@ async def async_setup_entry(hass, config_entry): ssl=config_entry.data[CONF_SSL], ) rainmachine = RainMachine( - client, config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), + hass, + client, + config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), + config_entry.data[CONF_SCAN_INTERVAL], ) - await rainmachine.async_update() except RainMachineError as err: _LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady @@ -155,16 +157,6 @@ 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.""" @@ -271,30 +263,86 @@ async def async_unload_entry(hass, config_entry): class RainMachine: """Define a generic RainMachine object.""" - def __init__(self, client, default_zone_runtime): + def __init__(self, hass, client, default_zone_runtime, scan_interval): """Initialize.""" + self._async_unsub_dispatcher_connect = None + self._scan_interval_seconds = scan_interval self.client = client self.data = {} self.default_zone_runtime = default_zone_runtime self.device_mac = self.client.mac + self.hass = hass + + self._api_category_count = { + PROVISION_SETTINGS: 0, + RESTRICTIONS_CURRENT: 0, + RESTRICTIONS_UNIVERSAL: 0, + } + self._api_category_locks = { + PROVISION_SETTINGS: asyncio.Lock(), + RESTRICTIONS_CURRENT: asyncio.Lock(), + RESTRICTIONS_UNIVERSAL: asyncio.Lock(), + } + + async def _async_fetch_from_api(self, api_category): + """Execute the appropriate coroutine to fetch particular data from the API.""" + if api_category == PROVISION_SETTINGS: + data = await self.client.provisioning.settings() + elif api_category == RESTRICTIONS_CURRENT: + data = await self.client.restrictions.current() + elif api_category == RESTRICTIONS_UNIVERSAL: + data = await self.client.restrictions.universal() + + return data + + @callback + def async_deregister_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_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + return + self._api_category_count[api_category] += 1 + + async def async_register_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_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect = async_track_time_interval( + self.hass, + self.async_update, + timedelta(seconds=self._scan_interval_seconds), + ) + + self._api_category_count[api_category] += 1 + + # Lock API updates in case multiple entities are trying to call the same API + # endpoint at once: + async with self._api_category_locks[api_category]: + if api_category not in self.data: + self.data[api_category] = await self._async_fetch_from_api(api_category) async def async_update(self): """Update sensor/binary sensor data.""" - tasks = { - PROVISION_SETTINGS: self.client.provisioning.settings(), - RESTRICTIONS_CURRENT: self.client.restrictions.current(), - RESTRICTIONS_UNIVERSAL: self.client.restrictions.universal(), - } + tasks = {} + 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[api_category] = result - self.data[operation] = result + async_dispatcher_send(self.hass, SENSOR_UPDATE_TOPIC) class RainMachineEntity(Entity): diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 8362c31b11f..ace977ca356 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -28,40 +28,64 @@ TYPE_RAINSENSOR = "rainsensor" TYPE_WEEKDAY = "weekday" BINARY_SENSORS = { - TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True), - TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True), - TYPE_FREEZE_PROTECTION: ("Freeze Protection", "mdi:weather-snowy", True), - TYPE_HOT_DAYS: ("Extra Water on Hot Days", "mdi:thermometer-lines", True), - TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel", False), - TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False), - TYPE_RAINDELAY: ("Rain Delay Restrictions", "mdi:cancel", False), - TYPE_RAINSENSOR: ("Rain Sensor Restrictions", "mdi:cancel", False), - TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel", False), + TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, PROVISION_SETTINGS), + TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, RESTRICTIONS_CURRENT), + TYPE_FREEZE_PROTECTION: ( + "Freeze Protection", + "mdi:weather-snowy", + True, + RESTRICTIONS_UNIVERSAL, + ), + TYPE_HOT_DAYS: ( + "Extra Water on Hot Days", + "mdi:thermometer-lines", + True, + RESTRICTIONS_UNIVERSAL, + ), + TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT), + TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT), + TYPE_RAINDELAY: ( + "Rain Delay Restrictions", + "mdi:cancel", + False, + RESTRICTIONS_CURRENT, + ), + TYPE_RAINSENSOR: ( + "Rain Sensor Restrictions", + "mdi:cancel", + False, + RESTRICTIONS_CURRENT, + ), + TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel", False, 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, (name, icon, enabled_by_default) in BINARY_SENSORS.items(): - binary_sensors.append( + async_add_entities( + [ RainMachineBinarySensor( - rainmachine, sensor_type, name, icon, enabled_by_default + rainmachine, sensor_type, name, icon, enabled_by_default, api_category ) - ) - - async_add_entities(binary_sensors, True) + 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, enabled_by_default): + 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 @@ -106,6 +130,8 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): self._dispatcher_handlers.append( async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update) ) + await self.rainmachine.async_register_api_interest(self._api_category) + await self.async_update() async def async_update(self): """Update the state.""" @@ -133,3 +159,8 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["rainSensor"] elif self._sensor_type == TYPE_WEEKDAY: self._state = self.rainmachine.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_api_interest(self._api_category) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 30acacafad0..957ad7bda21 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -28,6 +28,7 @@ SENSORS = { "clicks/m^3", None, False, + PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( "Flow Sensor Consumed Liters", @@ -35,6 +36,7 @@ SENSORS = { "liter", None, False, + PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_START_INDEX: ( "Flow Sensor Start Index", @@ -42,6 +44,7 @@ SENSORS = { "index", None, False, + PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_WATERING_CLICKS: ( "Flow Sensor Clicks", @@ -49,6 +52,7 @@ SENSORS = { "clicks", None, False, + PROVISION_SETTINGS, ), TYPE_FREEZE_TEMP: ( "Freeze Protect Temperature", @@ -56,6 +60,7 @@ SENSORS = { "°C", "temperature", True, + RESTRICTIONS_UNIVERSAL, ), } @@ -63,13 +68,8 @@ SENSORS = { 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, - (name, icon, unit, device_class, enabled_by_default), - ) in SENSORS.items(): - sensors.append( + async_add_entities( + [ RainMachineSensor( rainmachine, sensor_type, @@ -78,10 +78,14 @@ async def async_setup_entry(hass, entry, async_add_entities): unit, device_class, enabled_by_default, + api_category, ) - ) - - async_add_entities(sensors, True) + for ( + sensor_type, + (name, icon, unit, device_class, enabled_by_default, api_category), + ) in SENSORS.items() + ], + ) class RainMachineSensor(RainMachineEntity): @@ -96,10 +100,12 @@ class RainMachineSensor(RainMachineEntity): 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 @@ -151,6 +157,8 @@ class RainMachineSensor(RainMachineEntity): self._dispatcher_handlers.append( async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, update) ) + await self.rainmachine.async_register_api_interest(self._api_category) + await self.async_update() async def async_update(self): """Update the sensor's state.""" @@ -182,3 +190,8 @@ class RainMachineSensor(RainMachineEntity): self._state = self.rainmachine.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_api_interest(self._api_category) From a007835293b4ac9f7e066e103240fd573813545b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 24 Jan 2020 23:42:59 -0700 Subject: [PATCH 258/393] Fix RainMachine update action (#31147) --- homeassistant/components/rainmachine/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index e2602e376a6..1e0421385ab 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -295,6 +295,10 @@ class RainMachine: return data + 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_api_interest(self, api_category): """Decrement the number of entities with data needs from an API category.""" @@ -313,7 +317,7 @@ class RainMachine: if not self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect = async_track_time_interval( self.hass, - self.async_update, + self._async_update_listener_action, timedelta(seconds=self._scan_interval_seconds), ) From 98ac84349c022aa27d05c0f2f6338681fd39d5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 25 Jan 2020 13:09:43 +0200 Subject: [PATCH 259/393] Fix Huawei LTE SMS recipient setting from options UI (#31117) * Fix Huawei LTE SMS recipient setting from options UI Refs https://github.com/home-assistant/home-assistant/issues/30827 * Use core interfaces in tests * ...more --- .../components/huawei_lte/__init__.py | 13 +++ .../components/huawei_lte/config_flow.py | 13 ++- .../components/huawei_lte/test_config_flow.py | 101 ++++++++++++------ 3 files changed, 92 insertions(+), 35 deletions(-) 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/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] From 80a55360dc0cba19f2b7ccfdc84ea369ae368f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 25 Jan 2020 13:48:32 +0200 Subject: [PATCH 260/393] Remove no longer used Hound config (#31154) --- .hound.yml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .hound.yml 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 From 217e280f8bdc72b72b0e30594546c2b7b26d17a7 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 25 Jan 2020 12:42:19 -0500 Subject: [PATCH 261/393] Update ZHA remotes registry to proper identify "remotes (#31146) * Update ZHA remote device types. * Remove `binary_sensor` entity from affected devices. * Update ZHA Profiles. Prevent DeviceType.ON_OFF_LIGHT_SWITCH from creating entities. * Update tests and remove unused entities. --- .../components/zha/core/registries.py | 4 +++- tests/components/zha/zha_devices_list.py | 23 ++++--------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e89c0b8189b..311f8fa275f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -44,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, ], @@ -104,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/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index d5875edc9e2..5475a5cb2f7 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -45,10 +45,7 @@ DEVICES = [ "profile_id": 260, } }, - "entities": [ - "binary_sensor.centralite_3130_77665544_on_off", - "sensor.centralite_3130_77665544_power", - ], + "entities": ["sensor.centralite_3130_77665544_power"], "event_channels": [6, 8], "manufacturer": "CentraLite", "model": "3130", @@ -553,7 +550,6 @@ DEVICES = [ }, "entities": [ "sensor.jasco_products_45856_77665544_smartenergy_metering", - "switch.jasco_products_45856_77665544_on_off", "light.jasco_products_45856_77665544_on_off", ], "event_channels": [6], @@ -1001,10 +997,7 @@ DEVICES = [ "profile_id": -1, }, }, - "entities": [ - "sensor.lumi_lumi_remote_b486opcn01_77665544_power", - "switch.lumi_lumi_remote_b486opcn01_77665544_on_off", - ], + "entities": ["sensor.lumi_lumi_remote_b486opcn01_77665544_power"], "event_channels": [6, 8, 768, 6], "manufacturer": "LUMI", "model": "lumi.remote.b486opcn01", @@ -1054,10 +1047,7 @@ DEVICES = [ "profile_id": None, }, }, - "entities": [ - "sensor.lumi_lumi_remote_b686opcn01_77665544_power", - "switch.lumi_lumi_remote_b686opcn01_77665544_on_off", - ], + "entities": ["sensor.lumi_lumi_remote_b686opcn01_77665544_power"], "event_channels": [6, 8, 768, 6], "manufacturer": "LUMI", "model": "lumi.remote.b686opcn01", @@ -1372,10 +1362,7 @@ DEVICES = [ "profile_id": 260, } }, - "entities": [ - "binary_sensor.osram_lightify_dimming_switch_77665544_on_off", - "sensor.osram_lightify_dimming_switch_77665544_power", - ], + "entities": ["sensor.osram_lightify_dimming_switch_77665544_power"], "event_channels": [6, 8], "manufacturer": "OSRAM", "model": "LIGHTIFY Dimming Switch", @@ -1593,7 +1580,6 @@ 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", @@ -1643,7 +1629,6 @@ DEVICES = [ "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", ], "event_channels": [6], From e16e192b3cf9a75ce5f14ff726b5ef9ed46c31fc Mon Sep 17 00:00:00 2001 From: Bill Durr Date: Sat, 25 Jan 2020 13:20:59 -0500 Subject: [PATCH 262/393] improvements to zha cover (#31144) --- homeassistant/components/zha/cover.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 5b83b8cefcb..d4fff97c021 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -95,6 +95,16 @@ class ZhaCover(ZhaEntity, CoverDevice): 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. @@ -133,7 +143,7 @@ class ZhaCover(ZhaEntity, CoverDevice): async def async_set_cover_position(self, **kwargs): """Move the roller shutter to a specific position.""" - new_pos = kwargs.get(ATTR_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( From 6f1c45257a6eaa0a096dc0e050cbcbcf3c37ff79 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sat, 25 Jan 2020 19:24:21 +0100 Subject: [PATCH 263/393] Fix state handling for older webos versions (#31099) * fix state handling for older webos versions * update aiopylgtv to 0.3.2 --- homeassistant/components/webostv/manifest.json | 2 +- homeassistant/components/webostv/media_player.py | 7 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 4328ff96b56..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.3.0"], + "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 c34fb376d31..72e5d16cfe3 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -227,11 +227,10 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): @property def state(self): """Return the state of the device.""" - client_state = self._client.power_state.get("state") - if client_state in [None, "Power Off", "Suspend", "Active Standby"]: - return STATE_OFF + if self._client.is_on: + return STATE_ON - return STATE_ON + return STATE_OFF @property def is_volume_muted(self): diff --git a/requirements_all.txt b/requirements_all.txt index 93d879f6629..6033b578fda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,7 +190,7 @@ aionotion==1.1.0 aiopvapi==1.6.14 # homeassistant.components.webostv -aiopylgtv==0.3.0 +aiopylgtv==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ed261e21ab..7581173b534 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -69,7 +69,7 @@ aiohue==1.10.1 aionotion==1.1.0 # homeassistant.components.webostv -aiopylgtv==0.3.0 +aiopylgtv==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2019.4.26 From c481a48e3a85b6bed55b5898a66502f0ea3d732d Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 25 Jan 2020 19:24:50 +0100 Subject: [PATCH 264/393] Separate iCloud class (#31022) * Separate iCloud class * Update .coveragerc * Fix pipe --- .coveragerc | 1 + homeassistant/components/icloud/__init__.py | 395 +--------------- homeassistant/components/icloud/account.py | 423 ++++++++++++++++++ .../components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 5 files changed, 428 insertions(+), 395 deletions(-) create mode 100644 homeassistant/components/icloud/account.py diff --git a/.coveragerc b/.coveragerc index c24cb43ffb9..8c53f5c1344 100644 --- a/.coveragerc +++ b/.coveragerc @@ -319,6 +319,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 diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 525831ce214..2e1bdf9e82b 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,51 +1,22 @@ """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_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, - SERVICE_UPDATE, STORAGE_KEY, STORAGE_VERSION, ) @@ -236,365 +207,3 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.services.async_register( DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA ) - - return True - - -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 - - 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..afa1ad092a2 --- /dev/null +++ b/homeassistant/components/icloud/account.py @@ -0,0 +1,423 @@ +"""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 + + 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/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 79627eec4aa..00f35fbee85 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -9,7 +9,7 @@ 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, diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index f6c87ed12d0..e24016795d3 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -9,7 +9,7 @@ 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 .account import IcloudDevice from .const import DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) From e4832ee4d05d7274b7faf64358894ee1e23ce373 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sat, 25 Jan 2020 14:08:36 -0600 Subject: [PATCH 265/393] Remove Owlet component (#31160) * Remove owlet component * Remove owlet from requirements_all.txt --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/owlet/__init__.py | 78 -------------- .../components/owlet/binary_sensor.py | 77 -------------- homeassistant/components/owlet/const.py | 6 -- homeassistant/components/owlet/manifest.json | 8 -- homeassistant/components/owlet/sensor.py | 100 ------------------ requirements_all.txt | 3 - 8 files changed, 274 deletions(-) delete mode 100644 homeassistant/components/owlet/__init__.py delete mode 100644 homeassistant/components/owlet/binary_sensor.py delete mode 100644 homeassistant/components/owlet/const.py delete mode 100644 homeassistant/components/owlet/manifest.json delete mode 100644 homeassistant/components/owlet/sensor.py diff --git a/.coveragerc b/.coveragerc index 8c53f5c1344..a9bd77748c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -511,7 +511,6 @@ omit = 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 diff --git a/CODEOWNERS b/CODEOWNERS index fba45fba5ce..49993f5d5ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -248,7 +248,6 @@ homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff 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 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/requirements_all.txt b/requirements_all.txt index 6033b578fda..89058cd98a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1427,9 +1427,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 From 353a0144960dcb7c4f6b81459f76937ce078c1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20=C3=98stergaard=20Nielsen?= Date: Sun, 26 Jan 2020 00:37:31 +0100 Subject: [PATCH 266/393] Fix the ihc.set_runtime_value_int service function not working with templates (#31145) * Make the set_runtime_value_int function work with template values * Use newest version of the ihcsdk library * Make the set_runtime_value_int function work with template values * Use newest version of the ihcsdk library * Updated to the newest ihcsdk 2.5.0 * Formatted changes to make it pass CI tests --- homeassistant/components/ihc/__init__.py | 5 ++++- homeassistant/components/ihc/manifest.json | 5 ++++- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 89058cd98a5..6e8536bd2c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -718,7 +718,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 From d3511a349621aacb902bf910ca194074a30b4046 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 26 Jan 2020 00:33:06 +0000 Subject: [PATCH 267/393] [ci skip] Translation update --- .../components/almond/.translations/es.json | 4 ++++ .../components/brother/.translations/es.json | 8 ++++++++ .../components/spotify/.translations/es.json | 18 ++++++++++++++++++ .../components/spotify/.translations/no.json | 13 +++++++++++++ .../components/vizio/.translations/da.json | 4 ++-- .../components/vizio/.translations/es.json | 1 + .../components/withings/.translations/da.json | 2 +- .../components/withings/.translations/es.json | 5 +++++ 8 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/spotify/.translations/es.json create mode 100644 homeassistant/components/spotify/.translations/no.json 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/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/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/no.json b/homeassistant/components/spotify/.translations/no.json new file mode 100644 index 00000000000..4756675bf11 --- /dev/null +++ b/homeassistant/components/spotify/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "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/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index dc5de132cc2..7a6dda98270 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -11,8 +11,8 @@ }, "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": "V\u00e6rt er allerede konfigureret.", - "name_exists": "Navn er allerede konfigureret.", + "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": { diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json index 997dde7088a..009f93a50c6 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -3,6 +3,7 @@ "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.", diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index 7b51cec402d..72d851ad873 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -6,7 +6,7 @@ "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": { 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" From 37d1cdc4cb7ddf74039cf99833ee74620d35db1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 25 Jan 2020 18:41:49 -0700 Subject: [PATCH 268/393] Add additional alarm states to SimpliSafe (#31060) * Add additional alarm states to SimpliSafe * Remove unused constant * Remove redundant local variable --- .../simplisafe/alarm_control_panel.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 37aa2d84585..362c0244749 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -19,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 @@ -28,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" @@ -80,7 +81,6 @@ 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( { @@ -157,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 @@ -168,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: From 4c4f7263238ee4f1273367d425168d8fa4ecba1c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 25 Jan 2020 20:27:35 -0700 Subject: [PATCH 269/393] Refactor RainMachine switch platform (#31148) * Import constants sanely * Linting * Rename data constants for consistency * Refactor RainMachine switch platform * Comments * Cleanup * Refactor switch and sensor API calls to be separate * Linting * Make sure zones are updated in appropriate service calls * Correctly decrement * Linting * Don't do weird inheritance * Ensure service calls update data properly * Docstring * Docstring * Errors can be logged without string conversion * Code review comments --- .../components/rainmachine/__init__.py | 158 +++++++---- .../components/rainmachine/binary_sensor.py | 74 +++-- homeassistant/components/rainmachine/const.py | 14 +- .../components/rainmachine/sensor.py | 52 ++-- .../components/rainmachine/switch.py | 257 +++++++++--------- 5 files changed, 295 insertions(+), 260 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 1e0421385ab..20b74f4f66e 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -27,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" @@ -150,6 +152,9 @@ async def async_setup_entry(hass, config_entry): _LOGGER.error("An error occurred: %s", err) raise ConfigEntryNotReady + # 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"): @@ -161,37 +166,37 @@ async def async_setup_entry(hass, config_entry): 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.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.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.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.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.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.async_update_programs_and_zones() @_verify_domain_control async def start_zone(call): @@ -199,31 +204,31 @@ async def async_setup_entry(hass, config_entry): await rainmachine.client.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.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.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.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.async_update_programs_and_zones() for service, method, schema in [ ("disable_program", disable_program, SERVICE_ALTER_PROGRAM), @@ -265,7 +270,7 @@ class RainMachine: def __init__(self, hass, client, default_zone_runtime, scan_interval): """Initialize.""" - self._async_unsub_dispatcher_connect = None + self._async_cancel_time_interval_listener = None self._scan_interval_seconds = scan_interval self.client = client self.data = {} @@ -274,48 +279,58 @@ class RainMachine: self.hass = hass self._api_category_count = { - PROVISION_SETTINGS: 0, - RESTRICTIONS_CURRENT: 0, - RESTRICTIONS_UNIVERSAL: 0, + DATA_PROVISION_SETTINGS: 0, + DATA_RESTRICTIONS_CURRENT: 0, + DATA_RESTRICTIONS_UNIVERSAL: 0, } self._api_category_locks = { - PROVISION_SETTINGS: asyncio.Lock(), - RESTRICTIONS_CURRENT: asyncio.Lock(), - RESTRICTIONS_UNIVERSAL: asyncio.Lock(), + DATA_PROVISION_SETTINGS: asyncio.Lock(), + DATA_RESTRICTIONS_CURRENT: asyncio.Lock(), + DATA_RESTRICTIONS_UNIVERSAL: asyncio.Lock(), } - async def _async_fetch_from_api(self, api_category): - """Execute the appropriate coroutine to fetch particular data from the API.""" - if api_category == PROVISION_SETTINGS: - data = await self.client.provisioning.settings() - elif api_category == RESTRICTIONS_CURRENT: - data = await self.client.restrictions.current() - elif api_category == RESTRICTIONS_UNIVERSAL: - data = await self.client.restrictions.universal() - - return data - 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_api_interest(self, api_category): + 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_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - self._async_unsub_dispatcher_connect = None + 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_register_api_interest(self, api_category): + 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.client.programs.all(include_inactive=True) + elif api_category == DATA_PROVISION_SETTINGS: + data = await self.client.provisioning.settings() + elif api_category == DATA_RESTRICTIONS_CURRENT: + data = await self.client.restrictions.current() + elif api_category == DATA_RESTRICTIONS_UNIVERSAL: + data = await self.client.restrictions.universal() + elif api_category == DATA_ZONES: + data = await self.client.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.client.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_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect = async_track_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), @@ -323,19 +338,27 @@ class RainMachine: self._api_category_count[api_category] += 1 - # Lock API updates in case multiple entities are trying to call the same API - # endpoint at once: + # 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: - self.data[api_category] = await self._async_fetch_from_api(api_category) + 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 = {} for category, count in self._api_category_count.items(): if count == 0: continue - tasks[category] = self._async_fetch_from_api(category) + tasks[category] = self.async_fetch_from_api(category) results = await asyncio.gather(*tasks.values(), return_exceptions=True) for api_category, result in zip(tasks, results): @@ -344,10 +367,37 @@ class RainMachine: "There was an error while updating %s: %s", api_category, result ) continue - self.data[api_category] = 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): """Define a generic RainMachine entity.""" @@ -389,6 +439,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 ace977ca356..34b8de80b88 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -2,17 +2,16 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -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_CURRENT, + DATA_RESTRICTIONS_UNIVERSAL, DOMAIN as RAINMACHINE_DOMAIN, - PROVISION_SETTINGS, - RESTRICTIONS_CURRENT, - RESTRICTIONS_UNIVERSAL, SENSOR_UPDATE_TOPIC, - RainMachineEntity, ) _LOGGER = logging.getLogger(__name__) @@ -28,35 +27,45 @@ TYPE_RAINSENSOR = "rainsensor" TYPE_WEEKDAY = "weekday" BINARY_SENSORS = { - TYPE_FLOW_SENSOR: ("Flow Sensor", "mdi:water-pump", True, PROVISION_SETTINGS), - TYPE_FREEZE: ("Freeze Restrictions", "mdi:cancel", True, RESTRICTIONS_CURRENT), + 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, - RESTRICTIONS_UNIVERSAL, + DATA_RESTRICTIONS_UNIVERSAL, ), TYPE_HOT_DAYS: ( "Extra Water on Hot Days", "mdi:thermometer-lines", True, - RESTRICTIONS_UNIVERSAL, + DATA_RESTRICTIONS_UNIVERSAL, ), - TYPE_HOURLY: ("Hourly Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT), - TYPE_MONTH: ("Month Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT), + 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, - RESTRICTIONS_CURRENT, + DATA_RESTRICTIONS_CURRENT, ), TYPE_RAINSENSOR: ( "Rain Sensor Restrictions", "mdi:cancel", False, - RESTRICTIONS_CURRENT, + DATA_RESTRICTIONS_CURRENT, + ), + TYPE_WEEKDAY: ( + "Weekday Restrictions", + "mdi:cancel", + False, + DATA_RESTRICTIONS_CURRENT, ), - TYPE_WEEKDAY: ("Weekday Restrictions", "mdi:cancel", False, RESTRICTIONS_CURRENT), } @@ -107,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.""" @@ -121,46 +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_api_interest(self._api_category) + 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_api_interest(self._api_category) + 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 957ad7bda21..8487628a32b 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,16 +1,15 @@ """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, - RainMachineEntity, ) _LOGGER = logging.getLogger(__name__) @@ -28,7 +27,7 @@ SENSORS = { "clicks/m^3", None, False, - PROVISION_SETTINGS, + DATA_PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( "Flow Sensor Consumed Liters", @@ -36,7 +35,7 @@ SENSORS = { "liter", None, False, - PROVISION_SETTINGS, + DATA_PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_START_INDEX: ( "Flow Sensor Start Index", @@ -44,7 +43,7 @@ SENSORS = { "index", None, False, - PROVISION_SETTINGS, + DATA_PROVISION_SETTINGS, ), TYPE_FLOW_SENSOR_WATERING_CLICKS: ( "Flow Sensor Clicks", @@ -52,7 +51,7 @@ SENSORS = { "clicks", None, False, - PROVISION_SETTINGS, + DATA_PROVISION_SETTINGS, ), TYPE_FREEZE_TEMP: ( "Freeze Protect Temperature", @@ -60,7 +59,7 @@ SENSORS = { "°C", "temperature", True, - RESTRICTIONS_UNIVERSAL, + DATA_RESTRICTIONS_UNIVERSAL, ), } @@ -124,11 +123,6 @@ class RainMachineSensor(RainMachineEntity): """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.""" @@ -148,50 +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_api_interest(self._api_category) + 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_api_interest(self._api_category) + 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 8da2cc4ee45..ff706cd7be5 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,22 +93,19 @@ VEGETATION_MAP = { 99: "Other", } +SWITCH_TYPE_PROGRAM = "program" +SWITCH_TYPE_ZONE = "zone" + async def async_setup_entry(hass, entry, async_add_entities): """Set up RainMachine switches based on a config entry.""" 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) @@ -117,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.""" @@ -145,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.client.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.client.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.client.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.client.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")), + } + ) From cd72128a8011369e1bf702d2b6af8f1f17543754 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 26 Jan 2020 01:39:19 -0800 Subject: [PATCH 270/393] Implement 'volume_set' service for Android TV devices (#31161) --- .../components/androidtv/manifest.json | 2 +- .../components/androidtv/media_player.py | 7 ++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/androidtv/test_media_player.py | 23 +++++++++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) 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..ff6359f54b3 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 ) @@ -631,6 +633,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.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6e8536bd2c3..ab4afb72bad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7581173b534..a3e2f8a03cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ 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 diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 0aaa870c57b..f076b461119 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PLATFORM, + SERVICE_VOLUME_SET, STATE_IDLE, STATE_OFF, STATE_PLAYING, @@ -820,3 +821,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) From bc196a3c9fbddaf7a00d6d015bb3c84505e780bf Mon Sep 17 00:00:00 2001 From: Malachi Soord Date: Sun, 26 Jan 2020 14:28:42 +0100 Subject: [PATCH 271/393] Introduce unique_id for lastfm to allow changing entity_id in backwards compatible way (#31163) * Replace . with _ for lastfm entity_id * lint * double quotes * Rollback change, add unique_id * Expose prop * Generate unique ID from user * Linty * FIx linter * Revert changes for splitting entity_id * Simplify --- homeassistant/components/lastfm/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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): From 3f03848a07c8cf365deeda20acaebc37fd5b3318 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 26 Jan 2020 14:36:29 +0100 Subject: [PATCH 272/393] Fix Velbus covers (includes velbus lib upgrade) (#31153) * Fix velbus covers * Update python-velbus lib * flake8 and black fixes * Fix comments * fix codeowner --- CODEOWNERS | 2 +- homeassistant/components/velbus/cover.py | 23 ++++++++++++++----- homeassistant/components/velbus/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 49993f5d5ad..0a1f92290d9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -368,7 +368,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 homeassistant/components/velux/* @Julius2342 homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index 3e7df39b333..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, ) @@ -33,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.""" @@ -72,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/manifest.json b/homeassistant/components/velbus/manifest.json index 250b2c01e4e..548dd0e6356 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"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab4afb72bad..fab2f1c3baa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1629,7 +1629,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3e2f8a03cc..83353008840 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -540,7 +540,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 From 1b3c4ed4b3250e4c8fcf6bebab4c36fc5bf0da7c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 26 Jan 2020 14:54:33 +0100 Subject: [PATCH 273/393] Use unique_id for config_entries of HomematicIP Cloud (#31133) * use hapid as unique_id for config_entry of HomematicIP Cloud * Add qualita_scale to manifest * Update config_flow * use core interface for config_flow tests * refactor to fail earlier * use asynctest * rewrite tests * rewrite tests * fix test * add assert --- .../components/homematicip_cloud/__init__.py | 22 +- .../homematicip_cloud/alarm_control_panel.py | 4 +- .../homematicip_cloud/binary_sensor.py | 4 +- .../components/homematicip_cloud/climate.py | 4 +- .../homematicip_cloud/config_flow.py | 26 +-- .../components/homematicip_cloud/cover.py | 4 +- .../components/homematicip_cloud/hap.py | 5 +- .../components/homematicip_cloud/light.py | 4 +- .../homematicip_cloud/manifest.json | 3 +- .../components/homematicip_cloud/sensor.py | 4 +- .../components/homematicip_cloud/switch.py | 4 +- .../components/homematicip_cloud/weather.py | 4 +- .../components/homematicip_cloud/conftest.py | 1 + .../homematicip_cloud/test_config_flow.py | 218 +++++++++++------- .../components/homematicip_cloud/test_init.py | 197 +++++++--------- 15 files changed, 254 insertions(+), 250 deletions(-) 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 dea84b90bd6..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__) @@ -30,7 +30,7 @@ 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 5a679626679..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__) @@ -79,7 +79,7 @@ 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 d932d5c3f0a..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} @@ -47,7 +47,7 @@ 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 2e6d8b546bc..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__) @@ -31,7 +31,7 @@ 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 f35118d0d84..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__) @@ -38,7 +38,7 @@ 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 50a1c4ae34a..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 @@ -60,7 +60,7 @@ 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 bce85592891..45adf54df2b 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ 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 @@ -30,7 +30,7 @@ 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 f6ea95ab117..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__) @@ -41,7 +41,7 @@ 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/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index fa19f573c7c..a37583cc139 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -49,6 +49,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: version=1, domain=HMIPC_DOMAIN, title=HAPID, + unique_id=HAPID, data=entry_data, source="import", connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index afaf71c67b5..bf1d628d9c2 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,34 +1,58 @@ """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 import const -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry async def test_flow_works(hass): - """Test config flow works.""" + """Test config flow.""" config = { const.HMIPC_HAPID: "ABC123", const.HMIPC_PIN: "123", const.HMIPC_NAME: "hmip", } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass - 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( + const.DOMAIN, context={"source": "user"}, data=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): @@ -38,14 +62,17 @@ async def test_flow_init_connection_error(hass): 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( + const.DOMAIN, context={"source": "user"}, data=config + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" async def test_flow_link_connection_error(hass): @@ -55,18 +82,23 @@ async def test_flow_link_connection_error(hass): 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( + const.DOMAIN, context={"source": "user"}, data=config + ) + + assert result["type"] == "abort" + assert result["reason"] == "connection_aborted" async def test_flow_link_press_button(hass): @@ -76,92 +108,104 @@ async def test_flow_link_press_button(hass): 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( + const.DOMAIN, context={"source": "user"}, data=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( + const.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) + MockConfigEntry(domain=const.DOMAIN, unique_id="ABC123").add_to_hass(hass) config = { const.HMIPC_HAPID: "ABC123", const.HMIPC_PIN: "123", const.HMIPC_NAME: "hmip", } - flow = config_flow.HomematicipCloudFlowHandler() - flow.hass = hass + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": "user"}, data=config + ) - 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 + config = { + const.HMIPC_HAPID: "ABC123", + const.HMIPC_AUTHTOKEN: "123", + const.HMIPC_NAME: "hmip", + } - 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( + const.DOMAIN, context={"source": "import"}, data=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, unique_id="ABC123").add_to_hass(hass) + config = { + const.HMIPC_HAPID: "ABC123", + const.HMIPC_AUTHTOKEN: "123", + const.HMIPC_NAME: "hmip", + } - 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", - } - ) + 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( + const.DOMAIN, context={"source": "import"}, data=config + ) assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index eb51c3ece38..c1ce12d4bfc 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,154 +1,115 @@ """Test HomematicIP Cloud setup process.""" -from unittest.mock import patch +from asynctest import CoroutineMock, patch from homeassistant.components import homematicip_cloud as hmipc from homeassistant.setup import async_setup_component -from tests.common import Mock, MockConfigEntry, mock_coro +from tests.common import Mock, 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 = { + hmipc.CONF_ACCESSPOINT: "ABC123", + hmipc.CONF_AUTHTOKEN: "123", + hmipc.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"], hmipc.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 - - -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", + mock_config = { hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_NAME: "hmip", + hmipc.HMIPC_HAPID: "ABC123", + hmipc.HMIPC_NAME: "name", } + MockConfigEntry(domain=hmipc.DOMAIN, data=mock_config).add_to_hass(hass) + + # 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 = { + hmipc.CONF_ACCESSPOINT: "ABC123", + hmipc.CONF_AUTHTOKEN: "123", + hmipc.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.HMIPC_AUTHTOKEN: "123", + hmipc.HMIPC_HAPID: "ABC123", + hmipc.HMIPC_NAME: "name", + } + MockConfigEntry(domain=hmipc.DOMAIN, data=mock_config).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.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 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]["ABC123"] + config_entries = hass.config_entries.async_entries(hmipc.DOMAIN) + assert len(config_entries) == 1 + + await hass.config_entries.async_unload(config_entries[0].entry_id) + + # entry is unloaded assert hass.data[hmipc.DOMAIN] == {} From 51e032a7ca8d7af87215b60850f5b90ba381b51a Mon Sep 17 00:00:00 2001 From: brefra Date: Sun, 26 Jan 2020 18:48:20 +0100 Subject: [PATCH 274/393] Add counters sensors for Velbus (#31165) * Add counter sensor * Apply black formatting * Move all counter logic to sensor.py * Code cleanup --- homeassistant/components/velbus/__init__.py | 4 --- homeassistant/components/velbus/sensor.py | 32 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) 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/sensor.py b/homeassistant/components/velbus/sensor.py index 7ebdda2d781..d8644b4569a 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,6 +1,8 @@ """Support for Velbus sensors.""" import logging +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR + from . import VelbusEntity from .const import DOMAIN @@ -15,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 From 4311b1ae654c3c8ae3d7013a97147eaf4f7712db Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 26 Jan 2020 13:53:31 -0500 Subject: [PATCH 275/393] Bump insteonplm to 0.16.6 (#31182) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index fab2f1c3baa..6005c89e6c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -727,7 +727,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 From aa390efd694dea5ea62319621959120218b3ee06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 26 Jan 2020 21:32:20 +0100 Subject: [PATCH 276/393] Add hassfest URL validation to documentation link (#31143) --- homeassistant/components/soma/manifest.json | 2 +- script/hassfest/manifest.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5ce9a1f75d5..2166830d9e4 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -23,7 +23,9 @@ 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.Url(), # pylint: disable=no-value-for-parameter vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Required("requirements"): [str], vol.Required("dependencies"): [str], From 9ba1a4a91a38a9cf27ec931095914080b63d2b92 Mon Sep 17 00:00:00 2001 From: finnysamuel Date: Sun, 26 Jan 2020 17:25:42 -0600 Subject: [PATCH 277/393] Bump aiobotocore to 0.11.1 (#30951) * Bumped aiobotocore version to 0.11.1 * New dependencies are added to requirements_all.txt --- homeassistant/components/aws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 6005c89e6c0..0b85db6877e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83353008840..a530d711ad4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From ac2172333c20a8de8784c045eaaa733e3a922ace Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 26 Jan 2020 18:01:59 -0700 Subject: [PATCH 278/393] Use non-deprecated method of instantiating RainMachine client (#31149) --- .../components/rainmachine/__init__.py | 73 ++++++++++--------- .../components/rainmachine/switch.py | 8 +- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 20b74f4f66e..53f33f68eb9 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -3,7 +3,7 @@ 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 @@ -133,24 +133,29 @@ async def async_setup_entry(hass, 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( - hass, - client, - config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), - config_entry.data[CONF_SCAN_INTERVAL], - ) 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: @@ -165,43 +170,43 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def disable_program(call): """Disable a program.""" - await rainmachine.client.programs.disable(call.data[CONF_PROGRAM_ID]) + 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]) + 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]) + 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]) + 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]) + 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]) + 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] ) await rainmachine.async_update_programs_and_zones() @@ -209,25 +214,25 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def stop_all(call): """Stop all watering.""" - await rainmachine.client.watering.stop_all() + 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]) + 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]) + 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() + await rainmachine.controller.watering.unpause_all() await rainmachine.async_update_programs_and_zones() for service, method, schema in [ @@ -268,14 +273,14 @@ async def async_unload_entry(hass, config_entry): class RainMachine: """Define a generic RainMachine object.""" - def __init__(self, hass, client, default_zone_runtime, scan_interval): + def __init__(self, hass, controller, default_zone_runtime, scan_interval): """Initialize.""" self._async_cancel_time_interval_listener = None self._scan_interval_seconds = scan_interval - self.client = client + self.controller = controller self.data = {} self.default_zone_runtime = default_zone_runtime - self.device_mac = self.client.mac + self.device_mac = controller.mac self.hass = hass self._api_category_count = { @@ -309,20 +314,20 @@ class RainMachine: 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.client.programs.all(include_inactive=True) + data = await self.controller.programs.all(include_inactive=True) elif api_category == DATA_PROVISION_SETTINGS: - data = await self.client.provisioning.settings() + data = await self.controller.provisioning.settings() elif api_category == DATA_RESTRICTIONS_CURRENT: - data = await self.client.restrictions.current() + data = await self.controller.restrictions.current() elif api_category == DATA_RESTRICTIONS_UNIVERSAL: - data = await self.client.restrictions.universal() + data = await self.controller.restrictions.universal() elif api_category == DATA_ZONES: - data = await self.client.zones.all(include_inactive=True) + 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.client.zones.all(details=True, include_inactive=True) + data = await self.controller.zones.all(details=True, include_inactive=True) self.data[api_category] = data @@ -419,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 diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ff706cd7be5..2bf63dbf495 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -196,13 +196,13 @@ class RainMachineProgram(RainMachineSwitch): async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" await self._async_run_switch_coroutine( - self.rainmachine.client.programs.stop(self._rainmachine_entity_id) + self.rainmachine.controller.programs.stop(self._rainmachine_entity_id) ) async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" await self._async_run_switch_coroutine( - self.rainmachine.client.programs.start(self._rainmachine_entity_id) + self.rainmachine.controller.programs.start(self._rainmachine_entity_id) ) async def async_update(self) -> None: @@ -258,13 +258,13 @@ class RainMachineZone(RainMachineSwitch): async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" await self._async_run_switch_coroutine( - self.rainmachine.client.zones.stop(self._rainmachine_entity_id) + self.rainmachine.controller.zones.stop(self._rainmachine_entity_id) ) async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" await self._async_run_switch_coroutine( - self.rainmachine.client.zones.start( + self.rainmachine.controller.zones.start( self._rainmachine_entity_id, self.rainmachine.default_zone_runtime ) ) From 8fff6462a1ace9969522ba65871401597688bdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 27 Jan 2020 08:50:01 +0200 Subject: [PATCH 279/393] Upgrade huawei-lte-api to 1.4.7 (#31155) https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.7 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 5b930802c61..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.6", + "huawei-lte-api==1.4.7", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 0b85db6877e..d0f7ebe20d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -695,7 +695,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.6 +huawei-lte-api==1.4.7 # homeassistant.components.hydrawise hydrawiser==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a530d711ad4..97501cc52f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ homematicip==0.10.15 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.6 +huawei-lte-api==1.4.7 # homeassistant.components.iaqualink iaqualink==0.3.0 From 1f0f62de7f1ffdaf460d7969e543e8c8688fecb6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Jan 2020 23:01:35 -0800 Subject: [PATCH 280/393] Add unique IDs to automation/scenes (#31150) * Add unique IDs to automation and scenes * Fix typo --- .../components/automation/__init__.py | 5 +++ homeassistant/components/config/__init__.py | 8 +++- homeassistant/components/config/automation.py | 18 +++++++-- homeassistant/components/config/customize.py | 2 +- homeassistant/components/config/group.py | 2 +- homeassistant/components/config/scene.py | 19 ++++++++-- homeassistant/components/config/script.py | 2 +- .../components/homeassistant/scene.py | 5 +++ homeassistant/components/scene/__init__.py | 5 +-- tests/components/config/test_automation.py | 38 ++++++++++++++++--- tests/components/config/test_customize.py | 6 ++- tests/components/config/test_group.py | 1 + tests/components/config/test_scene.py | 29 +++++++++++--- tests/components/scene/test_init.py | 10 ++++- 14 files changed, 123 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 70b8b26fa2c..52769063b7e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -189,6 +189,11 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """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.""" 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/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index a142c787506..af5f4cea828 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -261,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/scene/__init__.py b/homeassistant/components/scene/__init__.py index 75ec2bfd875..8b530e1e728 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 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/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") From 1278f323067dfcab61af40d001bc6a4e1e0cfd3b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 27 Jan 2020 10:40:48 +0100 Subject: [PATCH 281/393] Add webostv sound_output capability (#31121) * Webostv: add sound_output capability Add the ability to read and set the sound_output * Webostv: add sound_output capability * Webostv: add sound_output capability * fix blank spaces * fix to long line * add , * Import ATTR_SOUND_OUTPUT Do not have the ability to test this change right now * Add white space * Create const.py * Use const import * Use import from const.py * Add docstring * Change order * Change order * Fix import * Fix import * Fix typo * Change order again * Change order again * Change order of attributes --- homeassistant/components/webostv/__init__.py | 10 ++++++++ homeassistant/components/webostv/const.py | 4 ++++ .../components/webostv/media_player.py | 23 +++++++++++++------ .../components/webostv/services.yaml | 9 ++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/webostv/const.py diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 13f3d9e8f8d..9dec8fe0c71 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -17,6 +17,8 @@ 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" @@ -30,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])} ) @@ -60,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__) 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/media_player.py b/homeassistant/components/webostv/media_player.py index 72e5d16cfe3..0e98bd8e703 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 @@ -59,8 +57,6 @@ SUPPORT_WEBOSTV = ( MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -LIVE_TV_APP_ID = "com.webos.app.livetv" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the LG WebOS TV platform.""" @@ -289,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.""" @@ -320,6 +324,11 @@ 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): """Simulate play pause media player.""" @@ -396,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() @@ -405,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/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' From 52c1bc9c26f7d936e2dcc3c1c70ce98172af31ba Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Mon, 27 Jan 2020 01:42:26 -0800 Subject: [PATCH 282/393] Check that documentation urls are valid (#31188) * Check that documentation urls are valid * Validate documentation url in pieces --- .../components/solarlog/manifest.json | 2 +- script/hassfest/manifest.py | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) 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/script/hassfest/manifest.py b/script/hassfest/manifest.py index 2166830d9e4..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,9 +48,9 @@ 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" - ): vol.Url(), # pylint: disable=no-value-for-parameter + 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], From 050e4afdc0c4b7c12f67f52066488360f71f7d87 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Jan 2020 11:13:36 +0100 Subject: [PATCH 283/393] Disable failing dsmr tests (#31202) * Disable failing dsmr tests * Disable module, disable import of missing dep --- tests/components/dsmr/test_sensor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 81249c04046..7000e2ab565 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -14,10 +14,16 @@ import asynctest import pytest from homeassistant.bootstrap import async_setup_component -from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from tests.common import assert_setup_component +# Imports disabled due to missing PyCRC on PyPi +# Also disabled pylint/flake8 where this is used below +# from homeassistant.components.dsmr.sensor import DerivativeDSMREntity + + +pytest.skip("Dependency missing on PyPi", allow_module_level=True) + @pytest.fixture def mock_connection_factory(monkeypatch): @@ -97,7 +103,9 @@ async def test_derivative(): config = {"platform": "dsmr"} - entity = DerivativeDSMREntity("test", "1.0.0", config) + # Disabled to satisfy pylint & flake8 caused by disabled import + # pylint: disable=undefined-variable + entity = DerivativeDSMREntity("test", "1.0.0", config) # noqa: F821 await entity.async_update() assert entity.state is None, "initial state not unknown" From 4f07ccd350ede47dac291bd5a2cc345acf322c2e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 27 Jan 2020 02:30:35 -0800 Subject: [PATCH 284/393] Fix unnecessary regeneration of access token in Tesla component (#31193) * Fix unnecessary regeneration of access token * Add manifest.json --- homeassistant/components/tesla/__init__.py | 1 + homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 1ae65f66821..729a449c6ff 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -118,6 +118,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() 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/requirements_all.txt b/requirements_all.txt index d0f7ebe20d4..49e20b9ce3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1954,7 +1954,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 97501cc52f3..a41c57bb3d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -631,7 +631,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 From 6be9a4533305b2a9042b0c894bc8e1a5695063a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 27 Jan 2020 11:42:09 +0100 Subject: [PATCH 285/393] Upgrade HAP-python to 2.7.0 (#31201) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 49e20b9ce3a..7a3668b2731 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a41c57bb3d8..2e632fdcd62 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 From 50b0e938e187b7180e9dc084ba86d7a73e72645e Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 27 Jan 2020 12:52:58 +0100 Subject: [PATCH 286/393] Added missing file (#31189) --- tests/components/netatmo/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/components/netatmo/__init__.py 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.""" From 7d9c8fdfa09323eef42a73dca295ab5fabe1d833 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 27 Jan 2020 08:54:31 -0500 Subject: [PATCH 287/393] update remove service (#31164) --- homeassistant/components/zha/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 3871a26c9d7..ac88b7c1179 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -908,10 +908,10 @@ 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_coordinator: + 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) From d3ac3e48a3188a4624f86a2465a8024639319fea Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Mon, 27 Jan 2020 08:32:14 -0700 Subject: [PATCH 288/393] Fix ps4 errors if pin begins with a 0 (#31198) * Fix errors if pin begins with a 0 * Test PIN leading with zero * Edit tests --- homeassistant/components/ps4/config_flow.py | 7 ++- tests/components/ps4/test_config_flow.py | 49 +++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) 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/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() From a73a1a44894cab64bd8e6ae2eaf61eeff6adb2ff Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 27 Jan 2020 17:57:36 +0100 Subject: [PATCH 289/393] Use config_entry.unique_id in Linky (#31051) * Use config_entry.unique_id in Linky * Reviews * _show_setup_form not async --- .../components/linky/.translations/en.json | 3 +- homeassistant/components/linky/__init__.py | 6 + homeassistant/components/linky/config_flow.py | 49 ++--- homeassistant/components/linky/strings.json | 3 +- tests/components/linky/test_config_flow.py | 201 ++++++++++-------- 5 files changed, 131 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/linky/.translations/en.json b/homeassistant/components/linky/.translations/en.json index 6c655b83581..13d2553b0c7 100644 --- a/homeassistant/components/linky/.translations/en.json +++ b/homeassistant/components/linky/.translations/en.json @@ -1,13 +1,12 @@ { "config": { "abort": { - "username_exists": "Account already configured" + "already_configured": "Account already configured" }, "error": { "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)", "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", - "username_exists": "Account already configured", "wrong_login": "Login error: please check your email & password" }, "step": { 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/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/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"]) From 4e2737bfb782d6ea42cb66b7390016b5d751d6f7 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Mon, 27 Jan 2020 18:12:18 +0100 Subject: [PATCH 290/393] Add Garmin Connect integration (#30792) * Added code files * Correctly name init file * Update codeowners * Update requirements * Added code files * Correctly name init file * Update codeowners * Update requirements * Black changes, added to coveragerc * Removed documentation location for now * Added documentation url * Fixed merge * Fixed flake8 syntax * Fixed isort * Removed false check and double throttle, applied time format change * Renamed email to username, used dict, deleted unused type, changed attr name * Async and ConfigFlow code * Fixes * Added device_class and misc fixes * isort and pylint fixes * Removed from test requirements * Fixed isort checkblack * Removed host field * Fixed coveragerc * Start working test file * Added more config_flow tests * Enable only most used sensors by default * Added more default enabled sensors, fixed tests * Fixed isort * Test config_flow improvements * Remove unused import * Removed redundant patch calls * Fixed mock return value * Updated to garmin_connect 0.1.8 fixed exceptions * Quick fix test patch to see if rest is error free * Fixed mock routine * Code improvements from PR feedback * Fix entity indentifier * Reverted device identifier * Fixed abort message * Test fix * Fixed unique_id MockConfigEntry --- .coveragerc | 3 + CODEOWNERS | 1 + .../garmin_connect/.translations/en.json | 24 ++ .../components/garmin_connect/__init__.py | 108 +++++++ .../components/garmin_connect/config_flow.py | 72 +++++ .../components/garmin_connect/const.py | 288 ++++++++++++++++++ .../components/garmin_connect/manifest.json | 9 + .../components/garmin_connect/sensor.py | 177 +++++++++++ .../components/garmin_connect/strings.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/garmin_connect/__init__py | 1 + .../garmin_connect/test_config_flow.py | 100 ++++++ 14 files changed, 814 insertions(+) create mode 100644 homeassistant/components/garmin_connect/.translations/en.json create mode 100644 homeassistant/components/garmin_connect/__init__.py create mode 100644 homeassistant/components/garmin_connect/config_flow.py create mode 100644 homeassistant/components/garmin_connect/const.py create mode 100644 homeassistant/components/garmin_connect/manifest.json create mode 100644 homeassistant/components/garmin_connect/sensor.py create mode 100644 homeassistant/components/garmin_connect/strings.json create mode 100644 tests/components/garmin_connect/__init__py create mode 100644 tests/components/garmin_connect/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a9bd77748c7..bfefbdd116e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -253,6 +253,9 @@ 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 diff --git a/CODEOWNERS b/CODEOWNERS index 0a1f92290d9..69572c8b5c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -115,6 +115,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 diff --git a/homeassistant/components/garmin_connect/.translations/en.json b/homeassistant/components/garmin_connect/.translations/en.json new file mode 100644 index 00000000000..faf463ea8db --- /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" + } +} 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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 31c326f4d13..70fc4355061 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", diff --git a/requirements_all.txt b/requirements_all.txt index 7a3668b2731..893fc5e7fb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e632fdcd62..df7a2f7dfee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 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" From f95a072877f1befd5759603263f1e0b4e1575a9d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 27 Jan 2020 10:50:16 -0700 Subject: [PATCH 291/393] Constrain SimpliSafe's check for emergency token usage (#31214) --- homeassistant/components/simplisafe/__init__.py | 7 ++++--- homeassistant/components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index b55489f4d67..b3d3baff16f 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -316,7 +316,6 @@ 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 @@ -338,7 +337,9 @@ class SimpliSafe: _LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") self._emergency_refresh_token_used = True - await self._api.refresh_access_token(self._config_entry.data[CONF_TOKEN]) + 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 @@ -348,7 +349,7 @@ 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 we've reached this point using an emergency refresh token, we're in the # clear and we can discard it: diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index f7f6fce0c74..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==6.0.0"], + "requirements": ["simplisafe-python==6.1.0"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 893fc5e7fb0..a53825d083d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1816,7 +1816,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==6.0.0 +simplisafe-python==6.1.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df7a2f7dfee..8b8a64fd24c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -591,7 +591,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==6.0.0 +simplisafe-python==6.1.0 # homeassistant.components.sleepiq sleepyq==0.7 From ab8b94382eb6267d5e356a6c2f859a73db0f544f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Jan 2020 09:54:38 -0800 Subject: [PATCH 292/393] Update Hue discovery (#31215) --- homeassistant/components/hue/manifest.json | 4 ++++ homeassistant/generated/ssdp.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index f8d7295a173..ea01da0980f 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -12,6 +12,10 @@ { "manufacturer": "Royal Philips Electronics", "modelName": "Philips hue bridge 2015" + }, + { + "manufacturer": "Signify", + "modelName": "Philips hue bridge 2015" } ], "homekit": { diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 5e09a241a9e..83f375f031b 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -30,6 +30,10 @@ SSDP = { { "manufacturer": "Royal Philips Electronics", "modelName": "Philips hue bridge 2015" + }, + { + "manufacturer": "Signify", + "modelName": "Philips hue bridge 2015" } ], "samsungtv": [ From 1d537ad416fde48293f1e6b4f304b838d976e4b2 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 27 Jan 2020 19:56:26 +0100 Subject: [PATCH 293/393] Fix typo: serivce --> service (#31217) --- .../templates/device_condition/tests/test_device_condition.py | 2 +- .../templates/device_trigger/tests/test_device_trigger.py | 2 +- tests/components/automation/test_event.py | 2 +- tests/components/automation/test_geo_location.py | 2 +- tests/components/automation/test_init.py | 2 +- tests/components/automation/test_litejet.py | 2 +- tests/components/automation/test_mqtt.py | 2 +- tests/components/automation/test_numeric_state.py | 2 +- tests/components/automation/test_state.py | 2 +- tests/components/automation/test_template.py | 2 +- tests/components/automation/test_time.py | 2 +- tests/components/automation/test_time_pattern.py | 2 +- tests/components/automation/test_zone.py | 2 +- tests/components/binary_sensor/test_device_condition.py | 2 +- tests/components/binary_sensor/test_device_trigger.py | 2 +- tests/components/climate/test_device_condition.py | 2 +- tests/components/climate/test_device_trigger.py | 2 +- tests/components/cover/test_device_condition.py | 2 +- tests/components/cover/test_device_trigger.py | 2 +- tests/components/device_automation/test_init.py | 2 +- tests/components/device_tracker/test_device_condition.py | 2 +- tests/components/fan/test_device_condition.py | 2 +- tests/components/fan/test_device_trigger.py | 2 +- tests/components/light/test_device_action.py | 2 +- tests/components/light/test_device_condition.py | 2 +- tests/components/light/test_device_trigger.py | 2 +- tests/components/lock/test_device_condition.py | 2 +- tests/components/lock/test_device_trigger.py | 2 +- tests/components/media_player/test_device_condition.py | 2 +- tests/components/sensor/test_device_condition.py | 2 +- tests/components/sensor/test_device_trigger.py | 2 +- tests/components/switch/test_device_action.py | 2 +- tests/components/switch/test_device_condition.py | 2 +- tests/components/switch/test_device_trigger.py | 2 +- tests/components/template/test_cover.py | 2 +- tests/components/template/test_fan.py | 2 +- tests/components/vacuum/test_device_condition.py | 2 +- tests/components/vacuum/test_device_trigger.py | 2 +- tests/components/zha/test_device_action.py | 2 +- tests/components/zha/test_device_trigger.py | 2 +- 40 files changed, 40 insertions(+), 40 deletions(-) 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/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 49707ac66b0..83db0cdf7dd 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -29,7 +29,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_litejet.py b/tests/components/automation/test_litejet.py index 294b15baf91..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") 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/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/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/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/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/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/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/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/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/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_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/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/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") From 1dbfc66669b3238bbeffc704196d1a6b696237f3 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 27 Jan 2020 21:34:15 +0100 Subject: [PATCH 294/393] Cleanup of HomematicIP Cloud tests (#31181) * CleanUp tests for HomematicIP_Cloud * Remove not required CoroutineMock * remove None return in mocks, add asserts * rewrite test --- .../components/homematicip_cloud/conftest.py | 43 ++--- .../homematicip_cloud/test_config_flow.py | 73 +++------ .../components/homematicip_cloud/test_hap.py | 154 +++++++----------- .../components/homematicip_cloud/test_init.py | 75 +++++---- 4 files changed, 145 insertions(+), 200 deletions(-) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index a37583cc139..9d70464f842 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,19 @@ 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.helpers.typing import ConfigType, HomeAssistantType 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 +35,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,10 +45,10 @@ 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, @@ -68,7 +73,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) @@ -77,17 +82,17 @@ 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) + 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)): + with patch.object(hap, "get_hap", return_value=mock_home): assert await hap.async_setup() mock_home.on_update(hap.async_update) mock_home.on_create(hap.async_create_entity) @@ -104,10 +109,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]} @@ -122,7 +127,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 bf1d628d9c2..01e820e7565 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,25 +1,30 @@ """Tests for HomematicIP Cloud config flow.""" from asynctest import patch -from homeassistant.components.homematicip_cloud import const +from homeassistant.components.homematicip_cloud.const import ( + DOMAIN as HMIPC_DOMAIN, + HMIPC_AUTHTOKEN, + HMIPC_HAPID, + HMIPC_NAME, + HMIPC_PIN, +) 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.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_PIN: "123", - const.HMIPC_NAME: "hmip", - } with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", return_value=False, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"}, data=config + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG ) assert result["type"] == "form" @@ -57,18 +62,12 @@ async def test_flow_works(hass): 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", - } - with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_setup", return_value=False, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"}, data=config + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG ) assert result["type"] == "form" @@ -77,12 +76,6 @@ async def test_flow_init_connection_error(hass): 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", - } - with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", return_value=True, @@ -94,7 +87,7 @@ async def test_flow_link_connection_error(hass): return_value=False, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"}, data=config + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG ) assert result["type"] == "abort" @@ -103,12 +96,6 @@ async def test_flow_link_connection_error(hass): 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", - } - with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", return_value=False, @@ -117,7 +104,7 @@ async def test_flow_link_press_button(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"}, data=config + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG ) assert result["type"] == "form" @@ -129,7 +116,7 @@ async def test_init_flow_show_form(hass): """Test config flow shows up with a form.""" result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "user"} + HMIPC_DOMAIN, context={"source": "user"} ) assert result["type"] == "form" assert result["step_id"] == "init" @@ -137,19 +124,13 @@ async def test_init_flow_show_form(hass): async def test_init_already_configured(hass): """Test accesspoint is already configured.""" - MockConfigEntry(domain=const.DOMAIN, unique_id="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( - const.DOMAIN, context={"source": "user"}, data=config + HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG ) assert result["type"] == "abort" @@ -158,12 +139,6 @@ async def test_init_already_configured(hass): async def test_import_config(hass): """Test importing a host with an existing config file.""" - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_AUTHTOKEN: "123", - const.HMIPC_NAME: "hmip", - } - with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", return_value=True, @@ -175,7 +150,7 @@ async def test_import_config(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "import"}, data=config + HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG ) assert result["type"] == "create_entry" @@ -186,13 +161,7 @@ async def test_import_config(hass): async def test_import_existing_config(hass): """Test abort of an existing accesspoint from config.""" - MockConfigEntry(domain=const.DOMAIN, unique_id="ABC123").add_to_hass(hass) - config = { - const.HMIPC_HAPID: "ABC123", - const.HMIPC_AUTHTOKEN: "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, @@ -204,7 +173,7 @@ async def test_import_existing_config(hass): return_value=True, ): result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": "import"}, data=config + HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG ) assert result["type"] == "abort" diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 324649ef515..765bee4a75d 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -5,81 +5,71 @@ 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_LOADED, ENTRY_STATE_NOT_LOADED from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN -from tests.common import mock_coro, mock_coro_func +from tests.common import MockConfigEntry 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): @@ -87,13 +77,9 @@ async def test_hap_setup_works(aioclient_mock): 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 +98,34 @@ 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, hmip_config): """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() + MockConfigEntry( + domain=HMIPC_DOMAIN, + unique_id=HAPID, + data=hmip_config[HMIPC_DOMAIN][0], + state=ENTRY_STATE_LOADED, + ).add_to_hass(hass) - 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 +136,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 +161,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 +176,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 c1ce12d4bfc..ee63dba3c97 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,33 +1,43 @@ """Test HomematicIP Cloud setup process.""" -from asynctest import CoroutineMock, 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 +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.""" entry_config = { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "name", + CONF_ACCESSPOINT: "ABC123", + CONF_AUTHTOKEN: "123", + CONF_NAME: "name", } # no config_entry exists - assert len(hass.config_entries.async_entries(hmipc.DOMAIN)) == 0 + assert len(hass.config_entries.async_entries(HMIPC_DOMAIN)) == 0 # no acccesspoint exists - assert not hass.data.get(hmipc.DOMAIN) + assert not hass.data.get(HMIPC_DOMAIN) assert ( - await async_setup_component(hass, hmipc.DOMAIN, {hmipc.DOMAIN: entry_config}) + 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) + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data == { "authtoken": "123", @@ -35,21 +45,17 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass): "name": "name", } # defined access_point created for config_entry - assert isinstance(hass.data[hmipc.DOMAIN]["ABC123"], hmipc.HomematicipHAP) + 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.""" - mock_config = { - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_NAME: "name", - } - MockConfigEntry(domain=hmipc.DOMAIN, data=mock_config).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) # one config_entry exists - config_entries = hass.config_entries.async_entries(hmipc.DOMAIN) + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data == { "authtoken": "123", @@ -60,17 +66,17 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): assert not config_entries[0].unique_id entry_config = { - hmipc.CONF_ACCESSPOINT: "ABC123", - hmipc.CONF_AUTHTOKEN: "123", - hmipc.CONF_NAME: "name", + CONF_ACCESSPOINT: "ABC123", + CONF_AUTHTOKEN: "123", + CONF_NAME: "name", } assert ( - await async_setup_component(hass, hmipc.DOMAIN, {hmipc.DOMAIN: entry_config}) + 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) + config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 assert config_entries[0].data == { "authtoken": "123", @@ -83,14 +89,10 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): async def test_unload_entry(hass): """Test being able to unload an entry.""" - mock_config = { - hmipc.HMIPC_AUTHTOKEN: "123", - hmipc.HMIPC_HAPID: "ABC123", - hmipc.HMIPC_NAME: "name", - } - MockConfigEntry(domain=hmipc.DOMAIN, data=mock_config).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 = CoroutineMock(return_value=True) instance.home.id = "1" @@ -99,18 +101,19 @@ async def test_unload_entry(hass): 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 mock_hap.return_value.mock_calls[0][0] == "async_setup" - assert hass.data[hmipc.DOMAIN]["ABC123"] - config_entries = hass.config_entries.async_entries(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] == {} + assert hass.data[HMIPC_DOMAIN] == {} async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): From 1f7ab9091b188948c1f8989d294a106b070ecca9 Mon Sep 17 00:00:00 2001 From: aaska Date: Mon, 27 Jan 2020 22:38:00 +0100 Subject: [PATCH 295/393] Bump python-synology to 0.4.0 : Add support for DSM v5 + fix sensors unknown for 15 min (#31049) * updating new api version * Added new configuration option for updated API --- .../components/synologydsm/manifest.json | 2 +- homeassistant/components/synologydsm/sensor.py | 17 ++++++++++++++--- requirements_all.txt | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) 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 3f459af9887..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") diff --git a/requirements_all.txt b/requirements_all.txt index a53825d083d..047585515b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1617,7 +1617,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 From ed970797be8f1ee178556458d91a941e432675b9 Mon Sep 17 00:00:00 2001 From: Paul Daumlechner Date: Tue, 28 Jan 2020 01:28:40 +0100 Subject: [PATCH 296/393] Fix attribute in Alexa service call for cover tilt (#31223) * Update attribute in Alexa service call for cover tilt * Update Tests to fix Tilt Position call. Co-authored-by: ochlocracy <5885236+ochlocracy@users.noreply.github.com> --- homeassistant/components/alexa/handlers.py | 2 +- tests/components/alexa/test_smart_home.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f67e2e259d0..77e3c7f7d38 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1137,7 +1137,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}": diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1510474aa6e..3d1e2b58d89 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2774,10 +2774,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", @@ -2785,7 +2785,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] @@ -2799,7 +2799,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] @@ -2813,7 +2813,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.open_cover_tilt", hass, - payload={"rangeValueDelta": "99"}, + payload={"rangeValueDelta": 99}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] @@ -2827,7 +2827,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.close_cover_tilt", hass, - payload={"rangeValueDelta": "-99"}, + payload={"rangeValueDelta": -99}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] From 6daec557b4d7f856c860183925fc478a34350a3e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 28 Jan 2020 00:31:59 +0000 Subject: [PATCH 297/393] [ci skip] Translation update --- .../components/adguard/.translations/cs.json | 10 +++++ .../components/almond/.translations/cs.json | 10 +++++ .../components/brother/.translations/cs.json | 14 +++++++ .../components/brother/.translations/hu.json | 15 +++++++ .../components/deconz/.translations/cs.json | 4 ++ .../components/deconz/.translations/hu.json | 5 +++ .../garmin_connect/.translations/cs.json | 24 +++++++++++ .../garmin_connect/.translations/en.json | 42 +++++++++---------- .../geonetnz_volcano/.translations/hu.json | 9 ++++ .../components/linky/.translations/cs.json | 7 ++++ .../components/linky/.translations/en.json | 4 +- .../components/netatmo/.translations/cs.json | 7 ++++ .../samsungtv/.translations/hu.json | 14 +++++++ .../components/soma/.translations/cs.json | 3 ++ .../components/somfy/.translations/cs.json | 3 ++ .../components/spotify/.translations/cs.json | 18 ++++++++ .../components/spotify/.translations/hu.json | 18 ++++++++ .../components/spotify/.translations/it.json | 18 ++++++++ .../components/spotify/.translations/no.json | 5 +++ .../components/starline/.translations/hu.json | 16 +++++++ .../tellduslive/.translations/cs.json | 7 ++++ .../components/vizio/.translations/it.json | 4 +- .../components/withings/.translations/cs.json | 7 ++++ .../components/withings/.translations/hu.json | 13 ++++++ .../components/withings/.translations/it.json | 2 +- 25 files changed, 254 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/cs.json create mode 100644 homeassistant/components/almond/.translations/cs.json create mode 100644 homeassistant/components/brother/.translations/cs.json create mode 100644 homeassistant/components/brother/.translations/hu.json create mode 100644 homeassistant/components/garmin_connect/.translations/cs.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/hu.json create mode 100644 homeassistant/components/linky/.translations/cs.json create mode 100644 homeassistant/components/netatmo/.translations/cs.json create mode 100644 homeassistant/components/samsungtv/.translations/hu.json create mode 100644 homeassistant/components/spotify/.translations/cs.json create mode 100644 homeassistant/components/spotify/.translations/hu.json create mode 100644 homeassistant/components/spotify/.translations/it.json create mode 100644 homeassistant/components/starline/.translations/hu.json create mode 100644 homeassistant/components/tellduslive/.translations/cs.json create mode 100644 homeassistant/components/withings/.translations/hu.json 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/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/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/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/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/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/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/en.json b/homeassistant/components/garmin_connect/.translations/en.json index faf463ea8db..5dac9131fb0 100644 --- a/homeassistant/components/garmin_connect/.translations/en.json +++ b/homeassistant/components/garmin_connect/.translations/en.json @@ -1,24 +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" + "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" + } }, - "description": "Enter your credentials.", "title": "Garmin Connect" - } - }, - "title": "Garmin Connect" - } -} + } +} \ No newline at end of file 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/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/en.json b/homeassistant/components/linky/.translations/en.json index 13d2553b0c7..95964cb7805 100644 --- a/homeassistant/components/linky/.translations/en.json +++ b/homeassistant/components/linky/.translations/en.json @@ -1,12 +1,14 @@ { "config": { "abort": { - "already_configured": "Account already configured" + "already_configured": "Account already configured", + "username_exists": "Account already configured" }, "error": { "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)", "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", + "username_exists": "Account already configured", "wrong_login": "Login error: please check your email & password" }, "step": { 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/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/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/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/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/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/no.json b/homeassistant/components/spotify/.translations/no.json index 4756675bf11..69b046cad0c 100644 --- a/homeassistant/components/spotify/.translations/no.json +++ b/homeassistant/components/spotify/.translations/no.json @@ -1,5 +1,10 @@ { "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." }, 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/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/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index 83c72912618..f3cb7b83026 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -11,8 +11,8 @@ }, "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": "Host gi\u00e0 configurato.", - "name_exists": "Nome gi\u00e0 configurato.", + "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": { 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/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 cc7a941813d..4a6f5e67965 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -6,7 +6,7 @@ "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": { From 9d8b4de09c856bc9d8e6963b58b150bcc548d2f3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 27 Jan 2020 19:43:26 -0500 Subject: [PATCH 298/393] Update ZHA entity discovery tests (#31191) * ZHA registry test fixtures refactoring. * Update ZHA test devices list. * Use channel.id for event relay channels test. * Add zigpy_device factory fixture. * Add node descriptor to mock zigpy devices. * Use node descriptor for ZHA device tests. * Update ZHA discovery test. check for unique_id and channels passed into each entity. * Address comments. --- tests/components/zha/common.py | 5 +- tests/components/zha/conftest.py | 52 +- tests/components/zha/test_channels.py | 14 +- tests/components/zha/test_discover.py | 48 +- tests/components/zha/test_registries.py | 10 +- tests/components/zha/zha_devices_list.py | 2142 +++++++++++++++++++--- 6 files changed, 2012 insertions(+), 259 deletions(-) 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..32e602c1431 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" @@ -70,3 +71,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_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_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/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 5475a5cb2f7..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], @@ -46,13 +70,23 @@ DEVICES = [ } }, "entities": ["sensor.centralite_3130_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.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], @@ -61,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], @@ -80,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], @@ -107,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], @@ -133,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], @@ -159,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], @@ -184,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": [], @@ -214,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], @@ -231,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], @@ -248,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], @@ -272,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], @@ -286,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], @@ -305,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], @@ -334,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], @@ -351,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], @@ -368,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], @@ -385,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], @@ -402,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], @@ -417,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], @@ -435,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], @@ -450,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], @@ -465,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], @@ -487,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], @@ -502,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], @@ -524,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], @@ -549,23 +951,37 @@ DEVICES = [ }, }, "entities": [ - "sensor.jasco_products_45856_77665544_smartenergy_metering", "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], @@ -574,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": [ @@ -607,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": [ @@ -642,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": [ @@ -677,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], @@ -698,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], @@ -715,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], @@ -730,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], @@ -745,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], @@ -760,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], @@ -824,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], @@ -860,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], @@ -894,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": [], @@ -947,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": [], @@ -997,49 +1695,70 @@ DEVICES = [ "profile_id": -1, }, }, - "entities": ["sensor.lumi_lumi_remote_b486opcn01_77665544_power"], - "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": [], @@ -1047,14 +1766,41 @@ DEVICES = [ "profile_id": None, }, }, - "entities": ["sensor.lumi_lumi_remote_b686opcn01_77665544_power"], - "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], @@ -1063,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], @@ -1096,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], @@ -1125,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], @@ -1144,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], @@ -1165,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], @@ -1180,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], @@ -1200,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], @@ -1215,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], @@ -1233,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], @@ -1251,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], @@ -1274,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], @@ -1292,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], @@ -1315,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], @@ -1333,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], @@ -1348,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], @@ -1363,13 +2541,23 @@ DEVICES = [ } }, "entities": ["sensor.osram_lightify_dimming_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.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], @@ -1380,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], @@ -1395,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], @@ -1416,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], @@ -1466,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], @@ -1507,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], @@ -1523,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], @@ -1541,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], @@ -1565,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], @@ -1581,16 +2893,29 @@ DEVICES = [ }, "entities": [ "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], @@ -1603,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], @@ -1625,19 +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", "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], @@ -1647,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], @@ -1669,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], @@ -1691,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], @@ -1713,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], @@ -1728,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], @@ -1743,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], @@ -1761,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], @@ -1777,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], @@ -1798,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], @@ -1816,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], @@ -1831,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], @@ -1849,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", }, ] From ec2d378a199e8f6ef4e0549f43d37290508fde05 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 28 Jan 2020 08:44:30 +0100 Subject: [PATCH 299/393] Add test for adding a device to HomematicIP Cloud (#31224) * Add test for adding a hmip device * refactor get_mock_hap to use config_entry setup * remove unused parameters --- .../components/homematicip_cloud/conftest.py | 20 ++++--- .../homematicip_cloud/test_device.py | 53 ++++++++++++++++++- .../components/homematicip_cloud/test_hap.py | 15 ++---- 3 files changed, 66 insertions(+), 22 deletions(-) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 9d70464f842..b0b06447f8a 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -17,7 +17,9 @@ from homeassistant.components.homematicip_cloud.const import ( 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 @@ -56,7 +58,7 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: 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}, ) @@ -84,23 +86,25 @@ async def get_mock_hap( hmip_config_entry: config_entries.ConfigEntry, ) -> HomematicipHAP: """Create a mocked homematic access point.""" - hass.config.components.add(HMIPC_DOMAIN) - 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_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 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 765bee4a75d..e42dfe8fb4e 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -17,13 +17,11 @@ from homeassistant.components.homematicip_cloud.hap import ( HomematicipAuth, HomematicipHAP, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN -from tests.common import MockConfigEntry - async def test_auth_setup(hass): """Test auth setup for client registration.""" @@ -72,7 +70,7 @@ async def test_auth_auth_check_and_register_with_exception(hass): 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() @@ -109,15 +107,8 @@ async def test_hap_setup_connection_error(): assert not hass.config_entries.flow.async_init.mock_calls -async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap, hmip_config): +async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap): """Test calling reset while the entry has been setup.""" - MockConfigEntry( - domain=HMIPC_DOMAIN, - unique_id=HAPID, - data=hmip_config[HMIPC_DOMAIN][0], - state=ENTRY_STATE_LOADED, - ).add_to_hass(hass) - assert hass.data[HMIPC_DOMAIN][HAPID] == default_mock_hap config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 From d813618d0d331b042e2fb88cfab5dc2a4d9928d8 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Tue, 28 Jan 2020 00:35:41 -0800 Subject: [PATCH 300/393] Add Gammu based local SMS notifications (#31233) * Add sms integration Committer: ocalvo * Fix PyLint * Update requirements --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/sms/__init__.py | 33 +++++++++++++++ homeassistant/components/sms/const.py | 3 ++ homeassistant/components/sms/manifest.json | 8 ++++ homeassistant/components/sms/notify.py | 47 ++++++++++++++++++++++ requirements_all.txt | 3 ++ script/gen_requirements_all.py | 1 + 8 files changed, 97 insertions(+) create mode 100644 homeassistant/components/sms/__init__.py create mode 100644 homeassistant/components/sms/const.py create mode 100644 homeassistant/components/sms/manifest.json create mode 100644 homeassistant/components/sms/notify.py diff --git a/.coveragerc b/.coveragerc index bfefbdd116e..3a8672bebe8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -638,6 +638,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/* diff --git a/CODEOWNERS b/CODEOWNERS index 69572c8b5c8..65b0e98b3cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -305,6 +305,7 @@ 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 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/requirements_all.txt b/requirements_all.txt index 047585515b7..3933d684778 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1565,6 +1565,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 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fc539a97f9f..3b30bf04363 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = ( "PySwitchbot", "pySwitchmate", "python-eq3bt", + "python-gammu", "python-lirc", "pyuserinput", "raspihats", From 259a7e84905fa012f4e8f1fdc254e3559e7a5f92 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 28 Jan 2020 09:41:50 +0100 Subject: [PATCH 301/393] Add gammu to wheels (#31239) --- azure-pipelines-wheels.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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} From a9c43c6c625ec5fb73f08751e4935c18a6b85cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 28 Jan 2020 11:58:37 +0100 Subject: [PATCH 302/393] Mill, correct hvac_mode. Fixes #31236 (#31242) --- homeassistant/components/mill/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 03954be12df4a1a2357064cbf58978157d10a40e Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 28 Jan 2020 15:25:15 +0100 Subject: [PATCH 303/393] Add brefra to codeowners list Velbus integration (#31245) * Add myself to codeowners list * Add myself to CODEOWNERS file --- CODEOWNERS | 2 +- homeassistant/components/velbus/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 65b0e98b3cf..ff6c2a39f38 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -370,7 +370,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/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 548dd0e6356..258b367fa5b 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -5,5 +5,5 @@ "requirements": ["python-velbus==2.0.36"], "config_flow": true, "dependencies": [], - "codeowners": ["@Cereal2nd"] + "codeowners": ["@Cereal2nd", "@brefra"] } From 8ceef728535005cb424a44b65a41b722d5b1ba8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Jan 2020 10:54:39 -0800 Subject: [PATCH 304/393] Google Assistant: Track if request is local (#31226) * Track if request is local * Cancel early if 2FA disabled * Allow disabling 2FA for ack * Do not mark devices with 2FA as reachable * Add request source to GA events --- homeassistant/components/cloud/client.py | 4 +- .../components/google_assistant/const.py | 3 + .../components/google_assistant/helpers.py | 13 ++++- .../components/google_assistant/http.py | 7 ++- .../components/google_assistant/smart_home.py | 21 +++++-- .../components/google_assistant/trait.py | 4 +- tests/components/google_assistant/__init__.py | 1 + .../google_assistant/test_smart_home.py | 57 ++++++++++++++++--- .../components/google_assistant/test_trait.py | 8 ++- 9 files changed, 99 insertions(+), 19 deletions(-) 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/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..8444ba11c61 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 @@ -232,7 +233,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 +287,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 +362,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() ) 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/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index edb12f06f33..9ef0599d394 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, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 7ffe9cda477..b3467eae326 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 == { @@ -115,7 +116,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 +149,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 +183,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 +222,7 @@ async def test_query_message(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -247,11 +250,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 +315,7 @@ async def test_execute(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -341,6 +357,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 +367,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 +377,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 +387,7 @@ async def test_execute(hass): "command": "action.devices.commands.BrightnessAbsolute", "params": {"brightness": 20}, }, + "source": "cloud", } assert len(service_events) == 2 @@ -424,6 +444,7 @@ async def test_raising_error_trait(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -448,6 +469,7 @@ async def test_raising_error_trait(hass): "command": "action.devices.commands.ThermostatTemperatureSetpoint", "params": {"thermostatTemperatureSetpoint": 10}, }, + "source": "cloud", } @@ -483,6 +505,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 +538,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 +568,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 +613,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 +654,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 +695,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 +729,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 +779,7 @@ async def test_trait_execute_adding_query_data(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -817,6 +846,7 @@ async def test_identify(hass): } ], }, + const.SOURCE_CLOUD, ) assert result == { @@ -851,8 +881,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 +931,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): From c3cef7227cc709a801f30f99e04769c036c0736d Mon Sep 17 00:00:00 2001 From: John Hollowell Date: Tue, 28 Jan 2020 16:52:59 -0500 Subject: [PATCH 305/393] Add proxmoxve SSLError check and remove log spam (#30818) * Add SSLError check and remove log spam * Update homeassistant/components/proxmoxve/manifest.json remove unnecessary requirement * Remove log level change Moved to proxmoxer library * Update homeassistant/components/proxmoxve/__init__.py * Update manifest.json Update to new version of dependency which fixes log issue * Run formatter and requires updates * oops, tabs * Fix quote marks When you can't get black running, just fix whatever CI says is wrong! --- homeassistant/components/proxmoxve/__init__.py | 7 +++++++ homeassistant/components/proxmoxve/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 3933d684778..a0c276faf45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1043,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 From 747f5fd62cc9ddb01fe90b084595cad51ad017a0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jan 2020 23:33:27 +0100 Subject: [PATCH 306/393] Upgrade iaqualink to 0.3.1 (#31257) --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index a0c276faf45..ef38a951db4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b8a64fd24c..a9346f704b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.7 # homeassistant.components.iaqualink -iaqualink==0.3.0 +iaqualink==0.3.1 # homeassistant.components.influxdb influxdb==5.2.3 From fd4f8d92d2d258af8eb9b9073dd81b7359574385 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jan 2020 23:35:28 +0100 Subject: [PATCH 307/393] Upgrade dsmr_parser to 0.18, re-enable tests (#31256) --- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dsmr/test_sensor.py | 12 ++---------- 4 files changed, 5 insertions(+), 13 deletions(-) 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/requirements_all.txt b/requirements_all.txt index ef38a951db4..5099c2c6975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -447,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9346f704b8..18886493f0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 7000e2ab565..81249c04046 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -14,16 +14,10 @@ import asynctest import pytest from homeassistant.bootstrap import async_setup_component +from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from tests.common import assert_setup_component -# Imports disabled due to missing PyCRC on PyPi -# Also disabled pylint/flake8 where this is used below -# from homeassistant.components.dsmr.sensor import DerivativeDSMREntity - - -pytest.skip("Dependency missing on PyPi", allow_module_level=True) - @pytest.fixture def mock_connection_factory(monkeypatch): @@ -103,9 +97,7 @@ async def test_derivative(): config = {"platform": "dsmr"} - # Disabled to satisfy pylint & flake8 caused by disabled import - # pylint: disable=undefined-variable - entity = DerivativeDSMREntity("test", "1.0.0", config) # noqa: F821 + entity = DerivativeDSMREntity("test", "1.0.0", config) await entity.async_update() assert entity.state is None, "initial state not unknown" From 2c02334c1f763b595763c753e14d285656c602b3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 29 Jan 2020 00:31:54 +0000 Subject: [PATCH 308/393] [ci skip] Translation update --- .../components/almond/.translations/pl.json | 4 +-- .../components/almond/.translations/sv.json | 9 ++++++ .../components/brother/.translations/ko.json | 8 +++++ .../components/brother/.translations/sv.json | 11 +++++++ .../components/deconz/.translations/sv.json | 2 +- .../garmin_connect/.translations/da.json | 24 ++++++++++++++ .../garmin_connect/.translations/hu.json | 7 +++++ .../garmin_connect/.translations/ko.json | 24 ++++++++++++++ .../garmin_connect/.translations/no.json | 24 ++++++++++++++ .../garmin_connect/.translations/pl.json | 24 ++++++++++++++ .../garmin_connect/.translations/ru.json | 24 ++++++++++++++ .../garmin_connect/.translations/sv.json | 24 ++++++++++++++ .../huawei_lte/.translations/sv.json | 7 +++++ .../components/linky/.translations/da.json | 1 + .../components/linky/.translations/hu.json | 7 +++++ .../components/linky/.translations/ko.json | 1 + .../components/linky/.translations/no.json | 1 + .../components/linky/.translations/pl.json | 1 + .../components/linky/.translations/ru.json | 1 + .../components/linky/.translations/sv.json | 7 +++++ .../components/mqtt/.translations/sv.json | 2 +- .../components/netatmo/.translations/sv.json | 13 ++++++++ .../components/ring/.translations/sv.json | 26 ++++++++++++++++ .../samsungtv/.translations/sv.json | 18 +++++++++++ .../components/spotify/.translations/ko.json | 18 +++++++++++ .../components/spotify/.translations/pl.json | 18 +++++++++++ .../components/spotify/.translations/sv.json | 18 +++++++++++ .../components/unifi/.translations/sv.json | 10 ++++++ .../components/vizio/.translations/pl.json | 26 ++++++++-------- .../components/vizio/.translations/sv.json | 31 +++++++++++++++++++ .../components/withings/.translations/pl.json | 2 +- .../components/withings/.translations/sv.json | 13 ++++++++ .../components/zha/.translations/cs.json | 14 +++++++++ 33 files changed, 402 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/almond/.translations/sv.json create mode 100644 homeassistant/components/brother/.translations/sv.json create mode 100644 homeassistant/components/garmin_connect/.translations/da.json create mode 100644 homeassistant/components/garmin_connect/.translations/hu.json create mode 100644 homeassistant/components/garmin_connect/.translations/ko.json create mode 100644 homeassistant/components/garmin_connect/.translations/no.json create mode 100644 homeassistant/components/garmin_connect/.translations/pl.json create mode 100644 homeassistant/components/garmin_connect/.translations/ru.json create mode 100644 homeassistant/components/garmin_connect/.translations/sv.json create mode 100644 homeassistant/components/huawei_lte/.translations/sv.json create mode 100644 homeassistant/components/linky/.translations/hu.json create mode 100644 homeassistant/components/linky/.translations/sv.json create mode 100644 homeassistant/components/netatmo/.translations/sv.json create mode 100644 homeassistant/components/ring/.translations/sv.json create mode 100644 homeassistant/components/samsungtv/.translations/sv.json create mode 100644 homeassistant/components/spotify/.translations/ko.json create mode 100644 homeassistant/components/spotify/.translations/pl.json create mode 100644 homeassistant/components/spotify/.translations/sv.json create mode 100644 homeassistant/components/vizio/.translations/sv.json create mode 100644 homeassistant/components/withings/.translations/sv.json create mode 100644 homeassistant/components/zha/.translations/cs.json diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json index 201905255a7..dc5717539a6 100644 --- a/homeassistant/components/almond/.translations/pl.json +++ b/homeassistant/components/almond/.translations/pl.json @@ -7,8 +7,8 @@ }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek Hass.io: {addon} ?", - "title": "Almond przez dodatek Hass.io" + "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/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/brother/.translations/ko.json b/homeassistant/components/brother/.translations/ko.json index 8ec7497296c..ec0f0d2453f 100644 --- a/homeassistant/components/brother/.translations/ko.json +++ b/homeassistant/components/brother/.translations/ko.json @@ -9,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": { @@ -17,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index ad79dc827c4..f70e6d728df 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_in_progress": "Trwa konfiguracja przep\u0142ywu dla komponentu Vizio.", - "already_setup": "Ten wpis zosta\u0142 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 iz menu Integracje przed ponown\u0105 pr\u00f3b\u0105 dodania tego urz\u0105dzenia.", - "host_exists": "Komponent Vizio z ju\u017c skonfigurowanym hostem.", - "name_exists": "Komponent Vizio z ju\u017c skonfigurowan\u0105 nazw\u0105.", - "updated_options": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci opcji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", - "updated_volume_step": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale rozmiar kroku g\u0142o\u015bno\u015bci w konfiguracji nie pasuje do wpisu konfiguracji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." + "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 - Podane warto\u015bci s\u0105 dok\u0142adne \n przed pr\u00f3b\u0105 ponownego przes\u0142ania.", + "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` to potrzebny jest wa\u017cny token dost\u0119pu." + "tv_needs_token": "Gdy typem urz\u0105dzenia jest `tv` potrzebny jest prawid\u0142owy token dost\u0119pu." }, "step": { "user": { @@ -23,7 +23,7 @@ "host": ":", "name": "Nazwa" }, - "title": "Skonfiguruj klienta Vizio SmartCast" + "title": "Konfiguracja klienta Vizio SmartCast" } }, "title": "Vizio SmartCast" @@ -33,11 +33,11 @@ "init": { "data": { "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", - "volume_step": "Wielko\u015b\u0107 kroku g\u0142o\u015bno\u015bci" + "volume_step": "Skok g\u0142o\u015bno\u015bci" }, - "title": "Zaktualizuj opcje Vizo SmartCast" + "title": "Aktualizacja opcji Vizo SmartCast" } }, - "title": "Zaktualizuj opcje Vizo SmartCast" + "title": "Aktualizuj opcje Vizo 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/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 97aa393fde4..afe35bd06cf 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", + "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." }, 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/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 From 37af2170ece90a21f8f49963bfa104631c6a5ac3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 28 Jan 2020 21:21:33 -0500 Subject: [PATCH 309/393] Mark device unavailable when it leaves Zigbee network. (#31264) --- homeassistant/components/zha/core/gateway.py | 10 ++++--- tests/components/zha/test_gateway.py | 29 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 tests/components/zha/test_gateway.py diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 72b5aa87329..106b77d6602 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 ( @@ -176,9 +178,9 @@ 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) async def _async_remove_device(self, device, entity_refs): if entity_refs is not None: @@ -315,13 +317,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.""" 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 From 36675fe4fa2a322947236ffea2c10d26350b2d54 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 Jan 2020 07:40:42 +0100 Subject: [PATCH 310/393] deCONZ - New light level sensor attribute (#31255) --- homeassistant/components/deconz/binary_sensor.py | 6 ++++-- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/sensor.py | 9 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) 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/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/requirements_all.txt b/requirements_all.txt index 5099c2c6975..0676aab88b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1191,7 +1191,7 @@ pydaikin==1.6.2 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==68 +pydeconz==69 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18886493f0d..7d951641c8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.2 # homeassistant.components.deconz -pydeconz==68 +pydeconz==69 # homeassistant.components.zwave pydispatcher==2.0.5 From 3ae5735e126c62d59c0d194d61e8560a28cce326 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Tue, 28 Jan 2020 23:53:30 -0800 Subject: [PATCH 311/393] Bump abodepy version to 0.17.0 (#31250) --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 0676aab88b6..c2c9a403daf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d951641c8e..52a4c46530c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 From 89ae255de7cda231dd9b0e449ebf1de2ae7e1e31 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 29 Jan 2020 08:54:21 +0100 Subject: [PATCH 312/393] Bump pyhaversion to 3.2.0 (#31241) * upgrade to pyhaversion=3.2.0 * bump to pyhaversion==3.2.0 in manifest.json --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index c2c9a403daf..123063c8c11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1273,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52a4c46530c..b18f1904b4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,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 From 61a86180103983e18b44167964c0aabfb3209863 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 29 Jan 2020 08:57:25 +0100 Subject: [PATCH 313/393] Fix Plugwise climate issues (#31209) * Fix Plugwise climate issues * Remove showing None-state for available_schemas, as requested by reviewer. --- homeassistant/components/plugwise/climate.py | 43 ++++++++++++------- .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 30 insertions(+), 17 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 123063c8c11..68a6de6ad78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 From bcdef4e500298c9d85de3e4f30e577c18679d841 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 28 Jan 2020 23:58:43 -0800 Subject: [PATCH 314/393] Fix reporting of battery sensor for Tesla (#31232) * Fix reporting of battery sensor for Tesla * Remove try --- homeassistant/components/tesla/__init__.py | 2 ++ homeassistant/components/tesla/binary_sensor.py | 1 + 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 729a449c6ff..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, @@ -215,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 3664cf6252d..8b60cd00163 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -55,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 From 9902b648fbded25ff4a6758b13bfbfef30fae7bc Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Wed, 29 Jan 2020 12:02:36 +0100 Subject: [PATCH 315/393] Add channel-mapping for HomeMatic (#31178) * Update entity.py * Fix Black * Fix Black * Update entity.py * Use new style python * Simplify * Allow multible attribute with different channels * Black Co-authored-by: Pascal Vizeli --- homeassistant/components/homematic/entity.py | 27 +++++--------------- 1 file changed, 7 insertions(+), 20 deletions(-) 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.""" From 28bfc6ae76724a716c4f0deacce8a355d58b2616 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 29 Jan 2020 12:04:05 +0100 Subject: [PATCH 316/393] Update homeassistant-pyozw to 0.1.8 (#31270) --- homeassistant/components/zwave/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index 68a6de6ad78..b1a7eb258a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -682,7 +682,7 @@ holidays==0.9.12 home-assistant-frontend==20200108.2 # homeassistant.components.zwave -homeassistant-pyozw==0.1.7 +homeassistant-pyozw==0.1.8 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b18f1904b4c..0a5bec51d02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ holidays==0.9.12 home-assistant-frontend==20200108.2 # homeassistant.components.zwave -homeassistant-pyozw==0.1.7 +homeassistant-pyozw==0.1.8 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From c2f1d6aa195c6f33914401d290ae602d12e56244 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jan 2020 12:04:25 +0100 Subject: [PATCH 317/393] Upgrade pre-commit to 2.0.0 (#31267) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 34582e4f773..b8ab2c23040 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ 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 From 9ff9614d0be629011ca36cf6039d86d503970c6b Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 29 Jan 2020 12:07:25 +0100 Subject: [PATCH 318/393] fix knx light turn_on with ct (#31184) process both brightness and color_temperature in a turn_on call. --- homeassistant/components/knx/light.py | 49 +++++++++++++-------------- 1 file changed, 24 insertions(+), 25 deletions(-) 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.""" From 56c235d4f28e542516473ebc8cce619dcdfc29b4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 29 Jan 2020 12:09:19 +0100 Subject: [PATCH 319/393] Fix example for set_datetime service (#31159) --- homeassistant/components/input_datetime/services.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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. From 080827fb6234098bcdf03a147605ca53f38a6392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20S=C5=82ota?= Date: Wed, 29 Jan 2020 12:11:22 +0100 Subject: [PATCH 320/393] Fix light.turn_on for emulated_hue (#31195) * Fix light.turn_on for emulated_hue HarmonyHub sends `{'xy': [0, 0], 'on': True, 'bri': 0}` when turning on lights that do not support brightness control. Unfortunately current logic always uses brightness value to control on/off state which makes no sense for lights that don't support brightness at all. This change fixes that behavior, making light without brightness control usable with HarmonyHub and probably some other remotes. * Test 'no_brightness' lights --- .../components/emulated_hue/hue_api.py | 5 +- tests/components/emulated_hue/test_hue_api.py | 72 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b054d69e7a4..459a13c066c 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: diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 2fb5c48e768..349d53aaee5 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() @@ -231,6 +241,64 @@ async def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["type"] == "On/off 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( "state,is_reachable", [ From 64edf2fe33d31320a4e22c389e800174aea03437 Mon Sep 17 00:00:00 2001 From: Thibault Maekelbergh <6213695+thibmaek@users.noreply.github.com> Date: Wed, 29 Jan 2020 12:12:40 +0100 Subject: [PATCH 321/393] Create truly live unique id (#31078) * Create truly live unique id * Do not generate unique id on basis of name * Add the station to the default station live name --- homeassistant/components/nmbs/sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index a91ff511b07..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): From a1e1610a697efc6b4a25692358742a92f031ff4a Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 29 Jan 2020 04:22:04 -0800 Subject: [PATCH 322/393] Add device_class to Tesla sensors (#31231) --- homeassistant/components/tesla/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 363cdc742d3..9b06828693f 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -37,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: @@ -59,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) From fa15bead940dc73e16b4bd5eba5d3769499cc496 Mon Sep 17 00:00:00 2001 From: tetienne Date: Wed, 29 Jan 2020 14:08:53 +0100 Subject: [PATCH 323/393] Remove useless assignment (#31272) --- homeassistant/components/template/cover.py | 13 ------------- homeassistant/components/template/fan.py | 6 ------ homeassistant/components/template/light.py | 13 ------------- 3 files changed, 32 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 13828b960fd..870e4035c2f 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -229,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 0f70f8a358b..c5512461f34 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -174,19 +174,6 @@ class LightTemplate(Light): 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 - if self._temperature_template is not None: - self._temperature_template.hass = self.hass - @property def brightness(self): """Return the brightness of the light.""" From ec4ccb10ec1a8abf05d1f3f9dd78809f43a52c77 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 29 Jan 2020 15:16:54 +0100 Subject: [PATCH 324/393] Bump iCloud to 0.9.2 + fix setup log (#31273) - pyicloud to 0.9.2 - fix log `ERROR (MainThread) [homeassistant.config_entries] icloud.async_setup_entry did not return boolean` --- homeassistant/components/icloud/__init__.py | 5 +++++ homeassistant/components/icloud/config_flow.py | 2 +- homeassistant/components/icloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/icloud/test_config_flow.py | 10 +++++----- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 2e1bdf9e82b..62eb2fb91ac 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -122,6 +122,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool 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: @@ -207,3 +210,5 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.services.async_register( DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA ) + + return True diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 9b00ccb2a8d..b3cb9c28181 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -98,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( 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/requirements_all.txt b/requirements_all.txt index b1a7eb258a8..c625e3be996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1294,7 +1294,7 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.1 +pyicloud==0.9.2 # homeassistant.components.intesishome pyintesishome==1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a5bec51d02..dc6409897a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -451,7 +451,7 @@ pyheos==0.6.0 pyhomematic==0.1.63 # homeassistant.components.icloud -pyicloud==0.9.1 +pyicloud==0.9.2 # homeassistant.components.ipma pyipma==2.0.2 diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 747af7c940a..6091d1cf1da 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -39,7 +39,7 @@ 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) @@ -52,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) @@ -65,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 = 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=False) yield service_mock @@ -77,7 +77,7 @@ 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 = 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=False) @@ -324,7 +324,7 @@ async def test_verification_code_success(hass: HomeAssistantType, service: Magic result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - service.return_value.requires_2fa = False + service.return_value.requires_2sa = False result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} From 9312d06fe4ded57915bac036f243693f09f676e4 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 29 Jan 2020 06:18:57 -0800 Subject: [PATCH 325/393] =?UTF-8?q?Catch=20'ConnectionResetError'=20except?= =?UTF-8?q?ions=20for=20Android=20TV=20integra=E2=80=A6=20(#31274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/androidtv/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index ff6359f54b3..3bb65bd1a0a 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -392,6 +392,7 @@ class ADBDevice(MediaPlayerDevice): self.exceptions = ( AttributeError, BrokenPipeError, + ConnectionResetError, TypeError, ValueError, InvalidChecksumError, From 85dbf1ffadcec66fa79680f22d78b22a306f0ca2 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 29 Jan 2020 10:20:43 -0500 Subject: [PATCH 326/393] Add OPNSense device tracker (#26834) * Add OPNSense device_tracker This commit adds a new component for using an OPNSense router as a device tracker. It uses pyopnsense to query the api to look at the arptable for a list of devices on the network. * Run black formatting locally to appease azure * Apply suggestions from code review Co-Authored-By: Fabian Affolter * Fix issues identified during code review This commit updates several issues found in the module during code review. * Update homeassistant/components/opnsense/__init__.py Co-Authored-By: Fabian Affolter * Update CODEOWNERS for recent changes * Fix lint * Apply suggestions from code review Co-Authored-By: Martin Hjelmare * More fixes from review comments This commit fixes several issues from review comments, including abandoning all the use of async code. This also completely reworks the tests to be a bit clearer. * Revert tests to previous format * Add device detection to opnsense device_tracker test This commit adds actual device detection to the unit test for the setup test. A fake api response is added to mocks for both api clients so that they will register devices as expected and asserts are added for that. The pyopnsense import is moved from the module level to be runtime in the class. This was done because it was the only way to make the MockDependency() call work as expected. * Rerun black * Fix lint * Move import back to module level * Return false on configuration errors in setup This commit updates the connection logic to return false if we're unable to connect to the configured OPNsense API endpoint for any reason. Previously we would not catch if an endpoint was incorrectly configured until we first tried to use it. In this case it would raise an unhandled exception. To handle this more gracefully this adds an api call early in the setup and catches any exception raised by that so we can return False to indicate the setup failed. * Update tests * Add pyopnsense to test requirements * Rerun gen_requirements script * Fix failing isort lint job step Since opening the PR originally yet another lint/style checker was added which failed the PR in CI. This commit makes the adjustments to have this pass the additional tool's checks. * Fix comment * Update manifest.json Co-authored-by: Fabian Affolter Co-authored-by: Martin Hjelmare Co-authored-by: Pascal Vizeli --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/opnsense/__init__.py | 77 +++++++++++++++++++ .../components/opnsense/device_tracker.py | 66 ++++++++++++++++ .../components/opnsense/manifest.json | 10 +++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/opnsense/__init__.py | 1 + .../opnsense/test_device_tracker.py | 64 +++++++++++++++ 9 files changed, 226 insertions(+) create mode 100644 homeassistant/components/opnsense/__init__.py create mode 100644 homeassistant/components/opnsense/device_tracker.py create mode 100644 homeassistant/components/opnsense/manifest.json create mode 100644 tests/components/opnsense/__init__.py create mode 100644 tests/components/opnsense/test_device_tracker.py diff --git a/.coveragerc b/.coveragerc index 3a8672bebe8..b936c9c514c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -508,6 +508,7 @@ 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/* diff --git a/CODEOWNERS b/CODEOWNERS index ff6c2a39f38..cd4d1897b6d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -247,6 +247,7 @@ 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/panel_custom/* @home-assistant/frontend 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/requirements_all.txt b/requirements_all.txt index c625e3be996..6a8594e4ceb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1416,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc6409897a4..b1c6a0c2f70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,6 +492,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 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..122a9bf294c --- /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): + """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" From 7116c7404a1bb3114a4bd1a002dea0445de5ae52 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 29 Jan 2020 12:03:20 -0500 Subject: [PATCH 327/393] Add PowerController to covers in Alexa (#31265) * Add PowerController to covers. * Comment Fix. * Update test device_class. * Update Comment. --- homeassistant/components/alexa/entities.py | 1 + homeassistant/components/alexa/handlers.py | 8 +++- tests/components/alexa/test_smart_home.py | 56 ++++++++++++++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6b831986192..254cec44553 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -400,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( diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 77e3c7f7d38..a834a18fbf3 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -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: diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3d1e2b58d89..8ba3b3ab7ff 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -131,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 @@ -1452,7 +1452,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") @@ -2515,6 +2519,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 = ( @@ -2533,7 +2567,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") @@ -2752,7 +2790,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") @@ -2868,7 +2910,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 From 83dff16e1ee3260467bb21d42a37a6dd1170bb1d Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 29 Jan 2020 12:04:57 -0500 Subject: [PATCH 328/393] Add support for rangeValueDeltaDefault in Alexa AdjustRangeValue directive (#31258) * Update tests with rangeValue and rangeValueDelta to use int. * Add support for rangeValueDeltaDefault for covers. * Update tests for range changes. * Test for AdjustRangeValue with rangeValueDeltaDefault True. * Update tilt error. --- homeassistant/components/alexa/handlers.py | 13 +++- tests/components/alexa/test_smart_home.py | 79 +++++++++++----------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index a834a18fbf3..03c5acd42fa 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1195,6 +1195,7 @@ 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 @@ -1220,9 +1221,12 @@ 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) + 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 @@ -1233,9 +1237,14 @@ async def async_api_adjust_range(hass, config, directive, context): # 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) + 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 diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8ba3b3ab7ff..588192e6c3a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -753,7 +753,7 @@ async def test_fan_range(hass): "fan#test_5", "fan.set_speed", hass, - payload={"rangeValue": "1"}, + payload={"rangeValue": 1}, instance="fan.speed", ) assert call.data["speed"] == "low" @@ -764,18 +764,22 @@ async def test_fan_range(hass): "fan#test_5", "fan.set_speed", hass, - payload={"rangeValue": "5"}, + payload={"rangeValue": 5}, instance="fan.speed", ) assert call.data["speed"] == "warp_speed" await assert_range_changes( hass, - [("low", "-1"), ("high", "1"), ("medium", "0"), ("warp_speed", "99")], + [ + ("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", @@ -802,18 +806,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", "-99")], + [("off", -3, False), ("off", -99, False)], "Alexa.RangeController", "AdjustRangeValue", "fan#test_6", - False, "fan.turn_off", "speed", instance="fan.speed", @@ -1524,7 +1527,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 @@ -1535,7 +1538,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] @@ -1549,7 +1552,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] @@ -1563,7 +1566,7 @@ async def test_cover_position_range(hass): "cover#test_range", "cover.open_cover", hass, - payload={"rangeValueDelta": "99"}, + payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, instance="cover.position", ) properties = msg["context"]["properties"][0] @@ -1577,7 +1580,7 @@ async def test_cover_position_range(hass): "cover#test_range", "cover.close_cover", hass, - payload={"rangeValueDelta": "-99"}, + payload={"rangeValueDelta": -99, "rangeValueDeltaDefault": False}, instance="cover.position", ) properties = msg["context"]["properties"][0] @@ -1587,11 +1590,10 @@ async def test_cover_position_range(hass): await assert_range_changes( hass, - [(25, "-5"), (35, "5")], + [(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", @@ -1618,21 +1620,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, @@ -2488,7 +2482,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) @@ -2855,7 +2849,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.open_cover_tilt", hass, - payload={"rangeValueDelta": 99}, + payload={"rangeValueDelta": 99, "rangeValueDeltaDefault": False}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] @@ -2869,7 +2863,7 @@ async def test_cover_tilt_position_range(hass): "cover#test_tilt_range", "cover.close_cover_tilt", hass, - payload={"rangeValueDelta": -99}, + payload={"rangeValueDelta": -99, "rangeValueDeltaDefault": False}, instance="cover.tilt", ) properties = msg["context"]["properties"][0] @@ -2879,11 +2873,10 @@ async def test_cover_tilt_position_range(hass): await assert_range_changes( hass, - [(25, "-5"), (35, "5")], + [(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", @@ -3038,18 +3031,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", @@ -3124,18 +3116,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", @@ -3432,7 +3429,7 @@ async def test_vacuum_fan_speed(hass): "vacuum#test_2", "vacuum.set_fan_speed", hass, - payload={"rangeValue": "1"}, + payload={"rangeValue": 1}, instance="vacuum.fan_speed", ) assert call.data["fan_speed"] == "low" @@ -3443,18 +3440,22 @@ async def test_vacuum_fan_speed(hass): "vacuum#test_2", "vacuum.set_fan_speed", hass, - payload={"rangeValue": "5"}, + payload={"rangeValue": 5}, instance="vacuum.fan_speed", ) assert call.data["fan_speed"] == "super_sucker" await assert_range_changes( hass, - [("low", "-1"), ("high", "1"), ("medium", "0"), ("super_sucker", "99")], + [ + ("low", -1, False), + ("high", 1, False), + ("medium", 0, False), + ("super_sucker", 99, False), + ], "Alexa.RangeController", "AdjustRangeValue", "vacuum#test_2", - False, "vacuum.set_fan_speed", "fan_speed", instance="vacuum.fan_speed", From 6bbb7130130458e788f90a4fb1cb2d5f5d90dbee Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 29 Jan 2020 18:18:24 +0100 Subject: [PATCH 329/393] Fix tests for opnsense (#31277) --- tests/components/opnsense/test_device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/opnsense/test_device_tracker.py b/tests/components/opnsense/test_device_tracker.py index 122a9bf294c..738847e1898 100644 --- a/tests/components/opnsense/test_device_tracker.py +++ b/tests/components/opnsense/test_device_tracker.py @@ -17,7 +17,7 @@ def mocked_opnsense(): yield mocked_opn -async def test_get_scanner(hass, mocked_opnsense): +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 From f4a4c6bea5d3d6908939dd2a6480e8a7163bceb1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 29 Jan 2020 12:24:43 -0500 Subject: [PATCH 330/393] ZHA group and device cleanup (#31260) * add dispatching of groups to light * added ha device registry device id * added zha group object * add group event listener * add and remove group members * get group by name * api cleanup * clean up get device info * create and remove zigpy groups * clean up create and remove group api * use device id * use device id * cleanup * update test * update tests to allow group events to flow --- homeassistant/components/zha/api.py | 173 ++++--------------- homeassistant/components/zha/core/const.py | 13 +- homeassistant/components/zha/core/device.py | 29 ++++ homeassistant/components/zha/core/gateway.py | 129 ++++++++++++-- homeassistant/components/zha/core/group.py | 95 ++++++++++ homeassistant/components/zha/core/helpers.py | 34 +--- tests/components/zha/conftest.py | 5 +- 7 files changed, 289 insertions(+), 189 deletions(-) create mode 100644 homeassistant/components/zha/core/group.py diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ac88b7c1179..fe628d90e90 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,28 +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_name = msg[GROUP_NAME] - zigpy_group = async_get_group_by_name(zha_gateway, group_name) - ret_group = None members = msg.get(ATTR_MEMBERS) - # we start with one to fill any gaps from a user removing existing groups - group_id = 1 - while group_id in zha_gateway.application_controller.groups: - group_id += 1 - - # 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 @@ -351,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) @@ -377,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) @@ -411,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( @@ -712,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) ] diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 3fbb62f8433..b8782101cd4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -225,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 3ed44a8f2aa..8810fd77fe7 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -104,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.""" @@ -406,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 106b77d6602..9456b8e9088 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -21,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, @@ -58,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, @@ -66,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 @@ -87,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 @@ -105,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) @@ -123,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 @@ -142,6 +152,8 @@ class ZHAGateway: ) await asyncio.gather(*init_tasks) + self._initialize_groups() + def device_joined(self, device): """Handle device joined. @@ -182,15 +194,53 @@ class ZHAGateway: """Handle device leaving the network.""" 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: remove_tasks = [] 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) @@ -199,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)) @@ -221,7 +271,14 @@ 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) + + 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.""" @@ -244,6 +301,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.""" @@ -290,6 +352,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.""" @@ -297,7 +365,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))}, @@ -305,10 +373,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 @@ -356,9 +434,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, @@ -432,6 +509,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/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 32e602c1431..18344172d29 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -50,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 From 61e41f0ddc79973b439f4e9c017f62af0f8f2162 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 29 Jan 2020 11:50:18 -0600 Subject: [PATCH 331/393] Add code owner for amcrest integration (#31276) --- CODEOWNERS | 1 + homeassistant/components/amcrest/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index cd4d1897b6d..cbf4f3ad1e9 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 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"] } From 31dc2ad28426bbe0024bc5a8d0a8e8a59e8462f6 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 29 Jan 2020 12:13:09 -0800 Subject: [PATCH 332/393] Allow filtering of sources for Android TV (#30994) --- .../components/androidtv/media_player.py | 53 +++++-- .../components/androidtv/test_media_player.py | 141 +++++++++++++++++- 2 files changed, 177 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 3bb65bd1a0a..93666958919 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -82,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" @@ -134,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, } ) @@ -232,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: @@ -367,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 @@ -375,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 @@ -386,6 +398,8 @@ 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) @@ -561,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 @@ -603,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 @@ -678,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/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f076b461119..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, @@ -300,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)[ @@ -316,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: @@ -332,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: @@ -354,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 @@ -369,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)[ @@ -429,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( @@ -462,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( @@ -495,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( @@ -528,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) From 67b73bd74ce1820a2eddb579da81caa3506e316f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 29 Jan 2020 21:40:41 +0100 Subject: [PATCH 333/393] Updated frontend to 20200129.0 (#31279) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 159ee68a53e..7dfcca4f019 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200108.2" + "home-assistant-frontend==20200129.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dc67061954..22b6328a0db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200108.2 +home-assistant-frontend==20200129.0 importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6a8594e4ceb..c05676a652a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.2 +home-assistant-frontend==20200129.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1c6a0c2f70..7206e60c6e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200108.2 +home-assistant-frontend==20200129.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From ee602e40a69745abac3f8fefcf055fa664e99f8a Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Wed, 29 Jan 2020 14:57:40 -0700 Subject: [PATCH 334/393] Add command 'ps_hold' to PS4 (#31283) --- homeassistant/components/ps4/const.py | 2 +- homeassistant/components/ps4/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/requirements_all.txt b/requirements_all.txt index c05676a652a..576305196d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1452,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7206e60c6e4..21df9279c89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -507,7 +507,7 @@ 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 From e9e44dbd974220067fa667ca01896b7975a44a6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 13:59:45 -0800 Subject: [PATCH 335/393] Fix callback and async (#31281) * Fix callback and async * Fix a return * Fix test * Fix mqtt tests * Fix some more callbacks --- .../alarm_control_panel/__init__.py | 54 ++---- .../components/anthemav/media_player.py | 4 +- homeassistant/components/api/__init__.py | 1 + .../components/apple_tv/media_player.py | 74 +++----- homeassistant/components/apple_tv/remote.py | 19 +- .../components/asterisk_mbox/mailbox.py | 9 +- .../components/automation/litejet.py | 1 + .../binary_sensor/device_condition.py | 3 +- .../components/broadlink/__init__.py | 105 +++++------ homeassistant/components/camera/__init__.py | 16 +- .../components/climate/device_condition.py | 3 +- homeassistant/components/cover/__init__.py | 96 ++++------ .../components/cover/device_condition.py | 4 +- homeassistant/components/demo/mailbox.py | 2 +- .../device_automation/toggle_entity.py | 3 +- .../device_sun_light_trigger/__init__.py | 26 ++- .../device_tracker/device_condition.py | 4 +- .../components/device_tracker/legacy.py | 27 +-- homeassistant/components/emby/media_player.py | 54 ++---- .../components/esphome/entry_data.py | 5 + homeassistant/components/fan/__init__.py | 42 ++--- .../components/fan/device_condition.py | 4 +- .../components/generic_thermostat/climate.py | 5 +- .../components/google_assistant/helpers.py | 2 + .../components/hangouts/hangouts_bot.py | 3 + .../homekit_controller/air_quality.py | 2 + .../homekit_controller/alarm_control_panel.py | 2 + .../homekit_controller/binary_sensor.py | 2 + .../components/homekit_controller/climate.py | 2 + .../homekit_controller/connection.py | 2 + .../components/homekit_controller/cover.py | 2 + .../components/homekit_controller/fan.py | 2 + .../components/homekit_controller/light.py | 2 + .../components/homekit_controller/lock.py | 2 + .../components/homekit_controller/sensor.py | 2 + .../components/homekit_controller/storage.py | 2 + .../components/homekit_controller/switch.py | 2 + .../components/huawei_lte/device_tracker.py | 2 + homeassistant/components/ihc/util.py | 4 + .../components/image_processing/__init__.py | 9 +- .../components/input_datetime/__init__.py | 1 + homeassistant/components/kira/remote.py | 11 +- homeassistant/components/knx/climate.py | 5 +- homeassistant/components/kodi/media_player.py | 88 ++++------ .../components/light/device_condition.py | 3 +- homeassistant/components/lock/__init__.py | 27 +-- .../components/lock/device_condition.py | 3 +- homeassistant/components/mailbox/__init__.py | 5 +- .../manual_mqtt/alarm_control_panel.py | 12 +- .../components/media_player/__init__.py | 165 +++++++----------- .../media_player/device_condition.py | 3 +- homeassistant/components/mqtt/__init__.py | 42 ++--- homeassistant/components/mqtt/camera.py | 5 +- homeassistant/components/mqtt/server.py | 11 +- .../components/mystrom/binary_sensor.py | 2 + homeassistant/components/notify/__init__.py | 5 +- homeassistant/components/plex/media_player.py | 1 + homeassistant/components/remote/__init__.py | 20 +-- .../components/rest_command/__init__.py | 2 + homeassistant/components/rflink/__init__.py | 8 +- homeassistant/components/rflink/cover.py | 12 +- .../components/russound_rio/media_player.py | 17 +- homeassistant/components/saj/sensor.py | 1 + homeassistant/components/scene/__init__.py | 9 +- .../components/sensor/device_condition.py | 3 +- .../components/shopping_list/__init__.py | 5 +- homeassistant/components/sma/sensor.py | 2 + .../components/squeezebox/media_player.py | 145 ++++++--------- .../components/switch/device_condition.py | 3 +- .../components/switcher_kis/switch.py | 10 +- homeassistant/components/tts/__init__.py | 6 +- .../components/universal/media_player.py | 160 ++++++----------- .../components/vacuum/device_condition.py | 3 +- .../components/volumio/media_player.py | 50 +++--- homeassistant/components/zha/api.py | 2 + homeassistant/components/zha/binary_sensor.py | 1 + homeassistant/components/zha/core/gateway.py | 1 + homeassistant/components/zha/cover.py | 2 + homeassistant/components/zha/entity.py | 3 + homeassistant/components/zha/fan.py | 1 + homeassistant/components/zha/light.py | 1 + homeassistant/components/zha/lock.py | 1 + homeassistant/components/zha/sensor.py | 2 + homeassistant/components/zha/switch.py | 1 + homeassistant/helpers/device_registry.py | 1 + homeassistant/helpers/entity.py | 30 ++-- homeassistant/helpers/restore_state.py | 1 + homeassistant/helpers/script.py | 1 + .../integration/device_condition.py | 4 +- tests/components/mqtt/test_server.py | 8 +- 90 files changed, 627 insertions(+), 883 deletions(-) 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/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/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/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/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/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4fe52a7d164..53c5cf16a98 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) 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/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/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/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/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 04ecad3b13d..6d343de8cb2 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/emby/media_player.py b/homeassistant/components/emby/media_player.py index 2fd1014dcdf..b4b05fd6c12 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -309,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/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/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/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index e179b576f70..23ee049052c 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -453,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/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 8444ba11c61..983f638656d 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -122,6 +122,7 @@ class AbstractConfig(ABC): ] await gather(*jobs) + @callback def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep @@ -131,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: 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/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py index 0419c0354e6..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 @@ -83,6 +84,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 800c988279a..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 @@ -45,6 +46,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 9fd93cf732a..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 @@ -98,6 +99,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 ff234f566c7..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 @@ -50,6 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 dec94771b03..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 @@ -41,6 +42,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 a6c4ae769e2..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 @@ -235,6 +236,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 9ce262291b3..e7d1e4d3273 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR_TEMP, Light, ) +from homeassistant.core import callback from . import KNOWN_DEVICES, HomeKitEntity @@ -23,6 +24,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 5183a636f0f..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 @@ -22,6 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 0e3680db346..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 @@ -246,6 +247,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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 6b71b15daff..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 @@ -17,6 +18,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): 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/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/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_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 03b468313f8..371e0dea185 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -326,6 +326,7 @@ class InputDatetime(RestoreEntity): """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: 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/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 78355937d15..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,21 +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)}}) - if media_type == "DIRECTORY": - return self.server.Player.Open({"item": {"directory": str(media_id)}}) - if media_type == "PLUGIN": - return self.server.Player.Open({"item": {"file": str(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/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/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/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/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/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b73208402f8..28951df545a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -485,122 +485,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 +575,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 +669,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 +707,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/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a9d5ac93ebc..a6db90382bf 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -774,27 +774,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 +802,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 +855,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 +866,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 +1004,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 +1071,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/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/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/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/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/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/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/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/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/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8b530e1e728..46b06b93698 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -94,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/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/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/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/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/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/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/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/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/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/zha/api.py b/homeassistant/components/zha/api.py index fe628d90e90..ea5586ef96f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -780,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] @@ -1058,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 d25410a0667..58b671a340f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -125,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/gateway.py b/homeassistant/components/zha/core/gateway.py index 9456b8e9088..e5a199c5bbd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -273,6 +273,7 @@ class ZHAGateway: """Return Group for given 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(): diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index d4fff97c021..3eeb73a23fd 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -113,6 +113,7 @@ class ZhaCover(ZhaEntity, CoverDevice): """ return self._current_position + @callback def async_set_position(self, pos): """Handle position update from channel.""" _LOGGER.debug("setting position: %s", pos) @@ -123,6 +124,7 @@ class ZhaCover(ZhaEntity, CoverDevice): 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) 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 50e9f63a067..6ad13d1c802 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -136,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 11fa87d4618..409cd339122 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -170,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 584df99fe08..b173c166a77 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -125,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/sensor.py b/homeassistant/components/zha/sensor.py index 52d4660a467..8b7dd894973 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -149,6 +149,7 @@ class Sensor(ZhaEntity): return None return self._state + @callback def async_set_state(self, state): """Handle state update from channel.""" if state is not None: @@ -202,6 +203,7 @@ 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": diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index a68fca76af4..1280ace34dc 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -93,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/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 41c78a2f070..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] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b9d1a73351c..a2a0ae840e0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -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/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..0d973afcfe9 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -214,6 +214,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/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/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): From 881437c085b532a10f16842823f882db5fe9f179 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 14:46:48 -0800 Subject: [PATCH 336/393] Catch error when searching for scenes or automations (#31288) --- homeassistant/components/search/__init__.py | 17 ++++++++++++----- tests/components/search/test_init.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 51de916f456..47e7f6ef28d 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -59,6 +59,8 @@ class Searcher: # 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, @@ -85,13 +87,18 @@ class Searcher: # Clean up entity_id items, from the general "entity" type result, # that are also found in the specific entity domain type. - self.results["entity"] -= self.results["script"] - self.results["entity"] -= self.results["scene"] - self.results["entity"] -= self.results["automation"] - self.results["entity"] -= self.results["group"] + for result_type in self.EXIST_AS_ENTITY: + self.results["entity"] -= self.results[result_type] # Remove entry into graph from search results. - self.results[item_type].remove(item_id) + 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} diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index cce98faa290..5762468ff1d 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -189,6 +189,23 @@ async def test_search(hass): 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.""" From 424e15c7a75d1457a30ae7d3c79d3d92c3ed16c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 16:19:13 -0800 Subject: [PATCH 337/393] Find related items scripts/automations (#31293) * Find related items scripts/automations * Update manifest --- .../components/automation/__init__.py | 109 +++++++++++++----- homeassistant/components/script/__init__.py | 70 ++++++++++- homeassistant/components/search/__init__.py | 43 +++++-- homeassistant/components/search/manifest.json | 2 +- homeassistant/helpers/condition.py | 53 ++++++++- homeassistant/helpers/script.py | 54 +++++++++ tests/components/automation/test_init.py | 78 +++++++++++++ tests/components/script/test_init.py | 74 ++++++++++-- tests/components/search/test_init.py | 63 ++++++++++ tests/helpers/test_condition.py | 34 ++++++ tests/helpers/test_script.py | 55 +++++++++ 11 files changed, 586 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 52769063b7e..45f892d783e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -2,7 +2,7 @@ from functools import partial import importlib import logging -from typing import Any, Awaitable, Callable +from typing import Any, Awaitable, Callable, List import voluptuous as vol @@ -19,7 +19,7 @@ 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 @@ -119,9 +119,75 @@ 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.action_script.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.action_script.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.action_script.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.action_script.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) @@ -168,7 +234,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): name, async_attach_triggers, cond_func, - async_action, + action_script, hidden, initial_state, ): @@ -178,7 +244,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._async_attach_triggers = async_attach_triggers 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 @@ -277,7 +343,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() @@ -358,7 +433,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) @@ -383,7 +458,7 @@ async def _async_process_config(hass, config, component): name, async_attach_triggers, cond_func, - action, + action_script, hidden, initial_state, ) @@ -394,24 +469,6 @@ 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) 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 index 47e7f6ef28d..a3bbd3844aa 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -1,14 +1,16 @@ """The Search integration.""" -from collections import defaultdict +from collections import defaultdict, deque +import logging import voluptuous as vol -from homeassistant.components import group, websocket_api +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): @@ -73,16 +75,17 @@ class Searcher: self._device_reg = device_reg self._entity_reg = entity_reg self.results = defaultdict(set) - self._to_resolve = 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.add((item_type, item_id)) + self._to_resolve.append((item_type, item_id)) while self._to_resolve: - search_type, search_id = self._to_resolve.pop() + 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, @@ -112,7 +115,7 @@ class Searcher: self.results[item_type].add(item_id) if item_type not in self.DONT_RESOLVE: - self._to_resolve.add((item_type, item_id)) + self._to_resolve.append((item_type, item_id)) @callback def _resolve_area(self, area_id) -> None: @@ -140,7 +143,11 @@ class Searcher: ): self._add_or_resolve("entity", entity_entry.entity_id) - # Extra: Find automations that reference this device + 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: @@ -153,6 +160,12 @@ class Searcher: 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: @@ -164,7 +177,7 @@ class Searcher: domain = split_entity_id(entity_id)[0] - if domain in ("scene", "automation", "script", "group"): + if domain in self.EXIST_AS_ENTITY: self._add_or_resolve(domain, entity_id) @callback @@ -173,7 +186,13 @@ class Searcher: Will only be called if automation is an entry point. """ - # Extra: Check with automation integration what entities/devices they reference + 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: @@ -181,7 +200,11 @@ class Searcher: Will only be called if script is an entry point. """ - # Extra: Check with script integration what entities/devices they reference + 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: diff --git a/homeassistant/components/search/manifest.json b/homeassistant/components/search/manifest.json index 337ce45f9bf..581a702f514 100644 --- a/homeassistant/components/search/manifest.json +++ b/homeassistant/components/search/manifest.json @@ -7,6 +7,6 @@ "zeroconf": [], "homekit": {}, "dependencies": ["websocket_api"], - "after_dependencies": ["scene", "group"], + "after_dependencies": ["scene", "group", "automation", "script"], "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index c3d09853960..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 @@ -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/script.py b/homeassistant/helpers/script.py index 0d973afcfe9..378a6016c20 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -156,12 +156,66 @@ 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 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( diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 83db0cdf7dd..391c9646dd4 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, @@ -922,3 +923,80 @@ 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"}, + "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": "state", "entity_id": "sensor.trigger_2"}, + "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")) == { + "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")) == { + "device-in-both", + "device-in-last", + } diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index cb66c26b6a3..9d64f5298f4 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -358,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", @@ -375,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/test_init.py b/tests/components/search/test_init.py index 5762468ff1d..54a32bed229 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -131,6 +131,62 @@ async def test_search(hass): }, ) + 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": "state", "entity_id": "sensor.trigger_1"}, + "action": [ + { + "service": "test.script", + "data": {"entity_id": wled_segment_1_entity.entity_id}, + }, + ], + }, + { + "alias": "wled_device", + "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "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}, @@ -139,6 +195,8 @@ async def test_search(hass): "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 ( @@ -149,6 +207,9 @@ async def test_search(hass): ("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) @@ -176,6 +237,8 @@ async def test_search(hass): "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"), 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_script.py b/tests/helpers/test_script.py index a7fe2c25236..b226ed15720 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1022,3 +1022,58 @@ 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", + }, + {"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 From 111fc1fa8e6e7d0e974ddaa2631ee2ddc1d0bcbb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 30 Jan 2020 01:21:23 +0100 Subject: [PATCH 338/393] Use all new helper functions (#31278) --- .../components/deconz/config_flow.py | 68 +++++++------------ tests/components/deconz/test_config_flow.py | 6 +- 2 files changed, 27 insertions(+), 47 deletions(-) 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/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" From 01dad31adc6b12f0fd010527095973c200b85ee3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 16:27:25 -0800 Subject: [PATCH 339/393] Fix service helper not handling sync methods (#31254) * Fix service helper not handling sync methods * Add legacy support for returning coroutine objects * Fix tests * Fix tests * Convert demo cover to async --- homeassistant/components/demo/cover.py | 46 ++++++++++++++------------ homeassistant/helpers/service.py | 18 +++++++--- tests/components/rflink/test_light.py | 19 ++++------- tests/helpers/test_service.py | 36 ++++++++++++++++---- 4 files changed, 73 insertions(+), 46 deletions(-) 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/helpers/service.py b/homeassistant/helpers/service.py index d621d4e6242..89c2715a760 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 @@ -339,7 +339,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non 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) ] @@ -352,7 +352,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non async def _handle_service_platform_call( - func, data, entities, context, required_features + hass, func, data, entities, context, required_features ): """Handle a function call.""" tasks = [] @@ -370,9 +370,17 @@ async def _handle_service_platform_call( entity.async_set_context(context) if isinstance(func, str): - await getattr(entity, func)(**data) + result = await hass.async_add_job(partial(getattr(entity, func), **data)) else: - await func(entity, data) + result = await hass.async_add_job(func, entity, data) + + if asyncio.iscoroutine(result): + _LOGGER.error( + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to component author.", + func, + entity.entity_id, + ) + await result if entity.should_poll: tasks.append(entity.async_update_ha_state(True)) 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/helpers/test_service.py b/tests/helpers/test_service.py index c80b6eac193..8d28bc73b88 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -306,6 +306,30 @@ 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_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"}), + ) + assert mock_entities["light.kitchen"].sync_method.call_count == 1 + + 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: @@ -348,7 +372,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"]] @@ -379,7 +403,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"]] @@ -422,7 +446,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()) @@ -442,7 +466,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"]] @@ -458,7 +482,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"], @@ -480,7 +504,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 == [] From 6499feffa38e2c8be079050fd3444549ebd999a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 29 Jan 2020 16:30:03 -0800 Subject: [PATCH 340/393] Bumped version to 0.105.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index facb365f75c..4a78a181ef7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 268430a61d32312141bf3f751c204183cb54aa77 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 30 Jan 2020 10:04:06 -0500 Subject: [PATCH 341/393] ZHA dependencies bump (#31295) * ZHA dependencies bump. * Bump bellows-homeassistant. --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index b436f677f6b..759cb4489fe 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.12.0", + "bellows-homeassistant==0.13.1", "zha-quirks==0.0.31", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.12.0", - "zigpy-xbee-homeassistant==0.8.0", + "zigpy-homeassistant==0.13.0", + "zigpy-xbee-homeassistant==0.9.0", "zigpy-zigate==0.5.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 576305196d3..49f6b6d4c71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 @@ -2130,10 +2130,10 @@ 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.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21df9279c89..9e1c13297a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 @@ -699,10 +699,10 @@ zha-quirks==0.0.31 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.1 From 9db2ad1fd7d4f8f5776f81b63802a5771b0584d4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 01:09:06 -0800 Subject: [PATCH 342/393] Add zones services.yaml (#31298) --- homeassistant/components/zone/services.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 homeassistant/components/zone/services.yaml 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. From f55193c2dac8be478caf4a9cf0b18b8c136061a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 01:06:17 -0800 Subject: [PATCH 343/393] Add zone to defaul config (#31303) --- homeassistant/components/default_config/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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": [] } From afe869bee97cd6dd62bd6e07887ddbe52ba4f3d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 09:28:06 -0800 Subject: [PATCH 344/393] Handle service calls that do not refer entity IDs (#31317) --- homeassistant/helpers/script.py | 4 ++++ tests/helpers/test_script.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 378a6016c20..1cac4679d82 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -201,6 +201,10 @@ class Script: continue entity_ids = data.get(ATTR_ENTITY_ID) + + if entity_ids is None: + continue + if isinstance(entity_ids, str): entity_ids = [entity_ids] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b226ed15720..5e748e3adfe 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1043,6 +1043,7 @@ async def test_referenced_entities(): "entity_id": "sensor.condition", "state": "100", }, + {"service": "test.script", "data": {"without": "entity_id"}}, {"scene": "scene.hello"}, {"event": "test_event"}, {"delay": "{{ delay_period }}"}, From af8b63fe31970cb84953926496f92cb1410c91f2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 30 Jan 2020 18:30:59 +0100 Subject: [PATCH 345/393] Updated frontend to 20200130.0 (#31318) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7dfcca4f019..6b16970c675 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200129.0" + "home-assistant-frontend==20200130.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 22b6328a0db..7ce2d357f82 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 49f6b6d4c71..d8bf0d47386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e1c13297a1..137ca3ae9ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200129.0 +home-assistant-frontend==20200130.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From e91c32cb007c045aefe98110bc12e59c4d6f22d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 09:47:16 -0800 Subject: [PATCH 346/393] Fix HTTP config serialization (#31319) --- homeassistant/components/http/__init__.py | 11 ++++++++++- tests/components/http/test_init.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 58cfb4b9cc1..565f84fdb8a 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -166,7 +166,16 @@ async def async_setup(hass, config): # If we are set up successful, we store the HTTP settings for safe mode. store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - await store.async_save(conf) + + 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) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 43a39302f4f..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 @@ -244,12 +245,16 @@ async def test_cors_defaults(hass): 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()} + config = { + http.CONF_SERVER_PORT: aiohttp_unused_port(), + "use_x_forwarded_for": True, + "trusted_proxies": ["192.168.1.100"], + } - await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) + 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 await hass.components.http.async_get_last_config() == http.HTTP_SCHEMA( - config - ) + assert restored == http.HTTP_SCHEMA(config) From 202fd4197bbeda566e223ee20ef00f629b3ae049 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 09:48:00 -0800 Subject: [PATCH 347/393] Bumped version to 0.105.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4a78a181ef7..689a82dcf92 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 6b95e98eebedfd80cd2458758c638b93d79ad7a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 11:13:54 -0800 Subject: [PATCH 348/393] Guard Z-Wave light HS conversion on None (#31320) --- homeassistant/components/zwave/light.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index 9c582eba89a..b32daf71f54 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -380,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") From 73f27c728c1480665b8d173952422a2fddb6f2c5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jan 2020 11:40:16 -0800 Subject: [PATCH 349/393] Fix wemo lights (#31323) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 8e43f47ef00..a615b3f5dfd 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -113,7 +113,7 @@ class WemoLight(Light): """Return the device info.""" return { "name": self.wemo.name, - "identifiers": {(WEMO_DOMAIN, self.wemo.serialnumber)}, + "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, "model": self.wemo.model_name, "manufacturer": "Belkin", } From 3635c4df5018ae7423c4f8b2d4df5f1982baa320 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 31 Jan 2020 08:30:13 +0100 Subject: [PATCH 350/393] Emulated Hue: changed fallback device-type to fix Alexa compatibility issues (#30013) (#31330) * Emulated Hue: changed the reported fallback device-type to fix Alexa compatibility issues (#30013) * Emulated Hue: updated tests (#30013) --- homeassistant/components/emulated_hue/hue_api.py | 7 ++++--- tests/components/emulated_hue/test_hue_api.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 459a13c066c..118bf7e3eaa 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -704,9 +704,10 @@ def entity_to_json(config, entity): 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" + # On/off plug-in unit (Zigbee Device ID: 0x0000) + # Supports groups and on/off control + # Used for compatibility purposes with Alexa instead of "On/off light" + retval["type"] = "On/off plug-in unit" retval["modelid"] = "HASS321" return retval diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 349d53aaee5..0ddc429b2d9 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -238,7 +238,7 @@ 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"] == "On/off plug-in unit" async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): From a53c3d10fefdc5cd38576e54742d4761612fdc3d Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Fri, 31 Jan 2020 01:28:54 -0600 Subject: [PATCH 351/393] Fix async bug in amcrest when registering services (#31334) --- homeassistant/components/amcrest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c6baf026a724f7cae0bc0aefe1f963b5d81bab83 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 00:32:43 -0800 Subject: [PATCH 352/393] Guard for callbacks in service helper (#31339) --- homeassistant/components/camera/__init__.py | 10 ++++------ homeassistant/helpers/service.py | 8 ++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 53c5cf16a98..b02874780e5 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -400,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/helpers/service.py b/homeassistant/helpers/service.py index 89c2715a760..36bfd9c8cb0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -370,9 +370,13 @@ async def _handle_service_platform_call( entity.async_set_context(context) if isinstance(func, str): - result = await hass.async_add_job(partial(getattr(entity, func), **data)) + result = hass.async_add_job(partial(getattr(entity, func), **data)) else: - result = await hass.async_add_job(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( From 7b3dc426737722d4bf916b1a819b2e4e29766f68 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 01:58:27 -0800 Subject: [PATCH 353/393] Fix incorrect annotation async flock notify (#31342) * Fix incorrect annotation async flock notify * Update notify.py * Update notify.py Co-authored-by: Pascal Vizeli --- homeassistant/components/flock/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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}" From 1aa322f2f04322af86d246f3a0704ef512ee53ea Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 31 Jan 2020 10:26:09 +0000 Subject: [PATCH 354/393] Bump version to 0.105.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 689a82dcf92..8f7d7bbaeb0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 283cc5c8c38a16784708875a82ae3ce3bc70385b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 14:47:40 -0800 Subject: [PATCH 355/393] Update Hue data fetching (#31338) * Refactor Hue Lights to use DataCoordinator * Redo how Hue updates data * Address comments * Inherit from Entity and remove pylint disable * Add tests for debounce --- homeassistant/components/hue/__init__.py | 4 +- homeassistant/components/hue/binary_sensor.py | 35 ++- homeassistant/components/hue/bridge.py | 9 + homeassistant/components/hue/const.py | 4 + homeassistant/components/hue/helpers.py | 8 +- homeassistant/components/hue/light.py | 277 +++++++----------- homeassistant/components/hue/sensor.py | 42 +-- homeassistant/components/hue/sensor_base.py | 178 +++++------ homeassistant/helpers/debounce.py | 77 +++++ homeassistant/helpers/event.py | 2 +- homeassistant/helpers/update_coordinator.py | 135 +++++++++ tests/components/hue/conftest.py | 11 + tests/components/hue/test_light.py | 30 +- tests/components/hue/test_sensor_base.py | 30 +- tests/helpers/test_debounce.py | 62 ++++ 15 files changed, 549 insertions(+), 355 deletions(-) create mode 100644 homeassistant/helpers/debounce.py create mode 100644 homeassistant/helpers/update_coordinator.py create mode 100644 tests/components/hue/conftest.py create mode 100644 tests/helpers/test_debounce.py 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/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/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..3db07ba2e5b 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.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/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/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/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/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_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/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 From ec3dc3dd16d8a56203989e9514f74332f697bfb9 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 31 Jan 2020 18:25:54 +0200 Subject: [PATCH 356/393] Upgrade pysma, fix #27154 (#31346) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index d8bf0d47386..1e5ce4a80ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1497,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 137ca3ae9ba..7f52743fe59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -513,7 +513,7 @@ pyps4-2ndscreen==1.0.6 pyqwikswitch==0.93 # homeassistant.components.sma -pysma==0.3.4 +pysma==0.3.5 # homeassistant.components.smartthings pysmartapp==0.3.2 From 7ee741d424a84d6489dbe2307d8606692f98f95a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 31 Jan 2020 17:27:16 +0100 Subject: [PATCH 357/393] Partially Revert "Deprecate hide_if_away from device_tracker (#30833) (#31348) --- .../components/device_tracker/legacy.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 6d343de8cb2..da3c945bc86 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -519,24 +519,21 @@ async def async_load_config( This method is a coroutine. """ - dev_schema = vol.All( - cv.deprecated(CONF_AWAY_HIDE, invalidation_version="0.107.0"), - vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), - vol.Optional("track", default=False): cv.boolean, - vol.Optional(CONF_MAC, default=None): vol.Any( - None, vol.All(cv.string, vol.Upper) - ), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - vol.Optional("gravatar", default=None): vol.Any(None, cv.string), - vol.Optional("picture", default=None): vol.Any(None, cv.string), - vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( - cv.time_period, cv.positive_timedelta - ), - } - ), + dev_schema = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon), + vol.Optional("track", default=False): cv.boolean, + vol.Optional(CONF_MAC, default=None): vol.Any( + None, vol.All(cv.string, vol.Upper) + ), + vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, + vol.Optional("gravatar", default=None): vol.Any(None, cv.string), + vol.Optional("picture", default=None): vol.Any(None, cv.string), + vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( + cv.time_period, cv.positive_timedelta + ), + } ) result = [] try: From f26cb83fd58fde9618dadb0c41898a8c4b1c7998 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 31 Jan 2020 17:14:43 -0500 Subject: [PATCH 358/393] Protect for unknown state attributes. (#31354) --- .../components/alexa/capabilities.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 8b93b911fc4..02ebdf785cd 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1326,14 +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[fan.ATTR_SPEED_LIST] - speed = self.entity.attributes[fan.ATTR_SPEED] - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + speed_list = self.entity.attributes.get(fan.ATTR_SPEED_LIST) + speed = self.entity.attributes.get(fan.ATTR_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 # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1349,12 +1355,13 @@ class AlexaRangeController(AlexaCapability): # Vacuum Fan Speed if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": - speed_list = self.entity.attributes[vacuum.ATTR_FAN_SPEED_LIST] - speed = self.entity.attributes[vacuum.ATTR_FAN_SPEED] - speed_index = next( - (i for i, v in enumerate(speed_list) if v == speed), None - ) - return speed_index + 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 From 8f8468f0160c10440f98ac3ec75cfa6792760299 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 31 Jan 2020 13:55:06 -0500 Subject: [PATCH 359/393] bump quirks (#31355) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 759cb4489fe..f7f70db590a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.13.1", - "zha-quirks==0.0.31", + "zha-quirks==0.0.32", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.13.0", "zigpy-xbee-homeassistant==0.9.0", diff --git a/requirements_all.txt b/requirements_all.txt index 1e5ce4a80ef..a4cb2e684b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2118,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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f52743fe59..f5f30a518d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ 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 From 25d6bc348c964999a20242e07034c4f0124e6fe2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 14:01:25 -0800 Subject: [PATCH 360/393] Fix wemo device types for lights (#31360) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index a615b3f5dfd..7d2cf9afc43 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -114,7 +114,7 @@ class WemoLight(Light): return { "name": self.wemo.name, "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, - "model": self.wemo.model_name, + "model": self.wemo.device_type, "manufacturer": "Belkin", } From 7ef352701c91e2620033eb74a797e4cf78613cad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 31 Jan 2020 14:48:26 -0800 Subject: [PATCH 361/393] Bumped version to 0.105.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8f7d7bbaeb0..b9d2f833b41 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From e1fd46d6db5c3a940e2ebbc6e1b72a0f4197376a Mon Sep 17 00:00:00 2001 From: Dan Lehman <53992354+DanTLehman@users.noreply.github.com> Date: Sat, 1 Feb 2020 16:42:37 +1100 Subject: [PATCH 362/393] Updated wemo lights fix for #31360 (#31369) --- homeassistant/components/wemo/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 7d2cf9afc43..5988019e66f 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -114,7 +114,7 @@ class WemoLight(Light): return { "name": self.wemo.name, "identifiers": {(WEMO_DOMAIN, self.wemo.uniqueID)}, - "model": self.wemo.device_type, + "model": type(self.wemo).__name__, "manufacturer": "Belkin", } From d382b0ba4236565ad9bc5bfae64f7d2131cad3c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 1 Feb 2020 00:29:29 -0800 Subject: [PATCH 363/393] Bumped version to 0.105.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b9d2f833b41..b340146bae3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 11fcb2cc7fc742f8da22f7072f18ec3d5a764cf8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 1 Feb 2020 10:01:42 +0100 Subject: [PATCH 364/393] Fix auto_bypass in alarmdecoder (#30961) * Fix auto_bypass in alarmdecoder * Address review comments: used dict[key] and renamed variable * Use dict[key] for required or optional config keys with default values --- homeassistant/components/alarmdecoder/__init__.py | 11 +++++++---- .../components/alarmdecoder/alarm_control_panel.py | 14 +++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) 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, From 59a9ca71ceb99f4b4ab02051d7415335ac244cf7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 17:08:49 +0100 Subject: [PATCH 365/393] deCONZ - Add support for new switch type (#31362) --- homeassistant/components/deconz/const.py | 2 +- homeassistant/components/deconz/light.py | 9 ++++++--- tests/components/deconz/test_light.py | 19 ++++++++++++++++--- tests/components/deconz/test_switch.py | 13 ++++++++++++- 4 files changed, 35 insertions(+), 8 deletions(-) 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/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/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_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", From 3b9306556819d6fc6f82c0f15f042124d31ea1ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 15:01:52 -0800 Subject: [PATCH 366/393] Add dump service to MQTT integration (#31370) * Add dump service to MQTT integration * Lint --- homeassistant/components/mqtt/__init__.py | 39 +++++++++++++++++++-- homeassistant/components/mqtt/services.yaml | 11 ++++++ tests/components/mqtt/test_init.py | 25 +++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a6db90382bf..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 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/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" From d91f9fc2f5c4312c540e62373597ed164f1d5a53 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Sat, 1 Feb 2020 19:44:40 -0500 Subject: [PATCH 367/393] Filter int in fan speed_list when yielding RangeController in Alexa (#31375) * Allow for int in fan speed_list. * Test for int in fan speed_list. * prevent yielding preset for int labels. --- .../components/alexa/capabilities.py | 8 ++++++-- tests/components/alexa/test_smart_home.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 02ebdf785cd..eb1474aed7e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1386,12 +1386,16 @@ class AlexaRangeController(AlexaCapability): precision=1, ) for index, speed in enumerate(speed_list): - labels = [speed.replace("_", " ")] + 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) - self._resource.add_preset(value=index, labels=labels) + + if len(labels) > 0: + self._resource.add_preset(value=index, labels=labels) return self._resource.serialize_capability_resources() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 588192e6c3a..ca6b1e1ccb6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -669,7 +669,7 @@ async def test_fan_range(hass): { "friendly_name": "Test fan 5", "supported_features": 1, - "speed_list": ["off", "low", "medium", "high", "turbo", "warp_speed"], + "speed_list": ["off", "low", "medium", "high", "turbo", 5, "warp_speed"], "speed": "medium", }, ) @@ -705,7 +705,7 @@ async def test_fan_range(hass): supported_range = configuration["supportedRange"] assert supported_range["minimumValue"] == 0 - assert supported_range["maximumValue"] == 5 + assert supported_range["maximumValue"] == 6 assert supported_range["precision"] == 1 presets = configuration["presets"] @@ -737,8 +737,10 @@ async def test_fan_range(hass): }, } in presets + assert {"rangeValue": 5} not in presets + assert { - "rangeValue": 5, + "rangeValue": 6, "presetResources": { "friendlyNames": [ {"@type": "text", "value": {"text": "warp speed", "locale": "en-US"}}, @@ -767,6 +769,17 @@ async def test_fan_range(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( From aaea55efede50c187abbf6ecd212435b349b0448 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 1 Feb 2020 18:11:05 +0100 Subject: [PATCH 368/393] deCONZ - Services normalize bridge id (#31378) * Services should also make sure to normalize bridge id since users do not know to manage themselves --- homeassistant/components/deconz/services.py | 12 ++++++------ requirements_test_pre_commit.txt | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) 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/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 8af2cbb6123..87ff3604dd6 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,6 +2,7 @@ bandit==1.6.2 black==19.10b0 +codespell==v1.16.0 flake8-docstrings==1.5.0 flake8==3.7.9 isort==v4.3.21 From fb26dd3028264da19d7abdad09116295743039b1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 1 Feb 2020 22:02:39 +0100 Subject: [PATCH 369/393] Revert "Bump alarmdecoder to 1.13.9 (#30303)" (#31385) This reverts commit f11d39f8ebf281a2069dc9f01777869c59405be4. --- homeassistant/components/alarmdecoder/manifest.json | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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/requirements_all.txt b/requirements_all.txt index a4cb2e684b1..a3f6606eb9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 From 5b7a65c5eaff17a6a4eab1a4a822f995157bdb0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 15:36:39 -0800 Subject: [PATCH 370/393] Fix service annotations (#31402) * Fix service annotations * Filter area_id from service data * Fix services not accepting entities * Typo --- .../components/input_select/__init__.py | 17 +++-- .../components/media_player/__init__.py | 65 ++++++++++++++----- homeassistant/helpers/config_validation.py | 4 +- homeassistant/helpers/service.py | 9 ++- tests/helpers/test_service.py | 12 +++- 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 26a07e600f3..6044375d8a8 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -143,11 +143,15 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) 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( @@ -248,7 +252,8 @@ class InputSelect(RestoreEntity): """Return unique id for the entity.""" return self._config[CONF_ID] - async def async_select_option(self, option): + @callback + def async_select_option(self, option): """Select new option.""" if option not in self._options: _LOGGER.warning( @@ -260,14 +265,16 @@ class InputSelect(RestoreEntity): self._current_option = option 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] 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._config[CONF_OPTIONS] = options diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 28951df545a..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( 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/service.py b/homeassistant/helpers/service.py index 36bfd9c8cb0..b30cab3fbd4 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -283,7 +283,11 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non # 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 @@ -323,6 +327,7 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non for platform in platforms: platform_entities = [] for entity in platform.entities.values(): + if entity.entity_id not in entity_ids: continue @@ -380,7 +385,7 @@ async def _handle_service_platform_call( if asyncio.iscoroutine(result): _LOGGER.error( - "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to component author.", + "Service %s for %s incorrectly returns a coroutine object. Await result instead in service handler. Report bug to integration author.", func, entity.entity_id, ) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 8d28bc73b88..d90842d1b71 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -320,14 +320,20 @@ async def test_call_with_sync_func(hass, mock_entities): async def test_call_with_sync_attr(hass, mock_entities): """Test invoking sync service calls.""" - mock_entities["light.kitchen"].sync_method = Mock() + 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"}), + ha.ServiceCall( + "test_domain", + "test_service", + {"entity_id": "light.kitchen", "area_id": "abcd"}, + ), ) - assert mock_entities["light.kitchen"].sync_method.call_count == 1 + 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): From a54d5f0bc42b26b5d5b8ec5963809269f58ae1e6 Mon Sep 17 00:00:00 2001 From: FrengerH Date: Sun, 2 Feb 2020 12:54:59 +0100 Subject: [PATCH 371/393] deCONZ - Fix magic cube awake gesture (#31403) --- homeassistant/components/deconz/deconz_event.py | 2 +- tests/components/deconz/test_deconz_event.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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() From 3cbd426c522f630bad93e0e4f005bb138323f4c5 Mon Sep 17 00:00:00 2001 From: akasma74 Date: Sun, 2 Feb 2020 16:23:13 +0000 Subject: [PATCH 372/393] Fix rflink commands containing equals sign (#31412) * new lib verson available * new rflink lib version * new rflink lib version --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/requirements_all.txt b/requirements_all.txt index a3f6606eb9a..fec294f1fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5f30a518d8..0c0222fc62b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -576,7 +576,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 From 2c1b4652150a78f31da33b3f4482f10c55d88dfc Mon Sep 17 00:00:00 2001 From: Martin Date: Sun, 2 Feb 2020 23:52:00 +0100 Subject: [PATCH 373/393] Emulated Hue + Alexa: Fix devices not discovered and error response (#30013 & #29899) (#31413) * Revert "Emulated Hue: changed the reported fallback device-type to fix Alexa compatibility issues (#30013)" This reverts commit ddc8d9e25c0c8fd4073c0c516de9fa096cceb9bc. * Revert "Emulated Hue: updated tests (#30013)" This reverts commit 90df461e752fd6ecc1dc65bae0eba17f26a82f5f. * Emulated Hue + Alexa: changed the fallback device-type again to "Dimmable Light" (#30013) after collective debugging; fixed brightness for on/off-devices and scripts to prevent "device malfunction" response from Alexa (#29899) * Emulated Hue + Alexa: lint (#30013, #29899) --- .../components/emulated_hue/hue_api.py | 28 +++++++++---------- tests/components/emulated_hue/test_hue_api.py | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 118bf7e3eaa..56e76b1d499 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -688,27 +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 plug-in unit (Zigbee Device ID: 0x0000) - # Supports groups and on/off control - # Used for compatibility purposes with Alexa instead of "On/off light" - retval["type"] = "On/off plug-in unit" - 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/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 0ddc429b2d9..51c3da7f08d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -238,7 +238,7 @@ 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 plug-in unit" + assert light_without_brightness_json["type"] == "Dimmable light" async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): From 67e7541016f4c22c30bbc851c7fc2fcd095e9319 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 2 Feb 2020 23:48:13 +0100 Subject: [PATCH 374/393] Fix device name Google Assistant when using aliases (#31416) * Fix device name Google Assistant when using aliases * Adjust cloud tests --- homeassistant/components/google_assistant/helpers.py | 2 +- tests/components/cloud/test_client.py | 2 +- tests/components/google_assistant/__init__.py | 5 ++++- tests/components/google_assistant/test_smart_home.py | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 983f638656d..f1b7a89bffe 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -399,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/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/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 9ef0599d394..c0b5aa7b193 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -104,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_smart_home.py b/tests/components/google_assistant/test_smart_home.py index b3467eae326..aa073c699f8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -92,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, From 9dfc00898be42cede883996551d54e595232a55a Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Sun, 2 Feb 2020 23:50:30 +0100 Subject: [PATCH 375/393] always call set_volume with integer values (#31418) --- homeassistant/components/webostv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 0e98bd8e703..99df9fd17ce 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -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 From 4f79ec0c78c6671a40960c5b6e0f33d4d1147233 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 15:39:51 -0800 Subject: [PATCH 376/393] Bumped version to 0.105.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b340146bae3..c0b1c7424cc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 04cb2e9fd5d96da7ea805ea8d07eca3c17b60559 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 30 Jan 2020 20:21:51 +0200 Subject: [PATCH 377/393] Rework Mikrotik device scanning following Unifi (#27484) * rework device scanning, add tests * update requirements and coverage * fix description comments * update tests, fix disabled entity updates * rework device scanning, add tests * update requirements and coverage * fix description comments * update tests, fix disabled entity updates * update librouteros to 3.0.0 * fix sorting * fix sorting 2 * revert to 2.3.0 as 3.0.0 requires code update * fix requirements * apply fixes * fix tests * update hub.py and fix tests * fix test_hub_setup_failed * rebased on dev and update librouteros to 3.0.0 * fixed test_config_flow * fixed tests * fix test_config_flow --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/mikrotik/.translations/en.json | 37 ++ homeassistant/components/mikrotik/__init__.py | 187 ++------ .../components/mikrotik/config_flow.py | 120 +++++ homeassistant/components/mikrotik/const.py | 40 +- .../components/mikrotik/device_tracker.py | 301 ++++++------- homeassistant/components/mikrotik/errors.py | 10 + homeassistant/components/mikrotik/hub.py | 413 ++++++++++++++++++ .../components/mikrotik/manifest.json | 13 +- .../components/mikrotik/strings.json | 37 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/mikrotik/__init__.py | 133 ++++++ tests/components/mikrotik/test_config_flow.py | 208 +++++++++ .../mikrotik/test_device_tracker.py | 118 +++++ tests/components/mikrotik/test_hub.py | 179 ++++++++ tests/components/mikrotik/test_init.py | 83 ++++ 18 files changed, 1546 insertions(+), 341 deletions(-) create mode 100644 homeassistant/components/mikrotik/.translations/en.json create mode 100644 homeassistant/components/mikrotik/config_flow.py create mode 100644 homeassistant/components/mikrotik/errors.py create mode 100644 homeassistant/components/mikrotik/hub.py create mode 100644 homeassistant/components/mikrotik/strings.json create mode 100644 tests/components/mikrotik/__init__.py create mode 100644 tests/components/mikrotik/test_config_flow.py create mode 100644 tests/components/mikrotik/test_device_tracker.py create mode 100644 tests/components/mikrotik/test_hub.py create mode 100644 tests/components/mikrotik/test_init.py diff --git a/.coveragerc b/.coveragerc index b936c9c514c..693959684f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,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/* diff --git a/CODEOWNERS b/CODEOWNERS index cbf4f3ad1e9..6983d13fc8b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,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 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 8c21b2e1c35..9a8ee7bdb45 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,43 +1,28 @@ -"""The mikrotik component.""" -import logging -import ssl - -from librouteros import connect -from librouteros.exceptions import LibRouterosError -from librouteros.login import plain as login_plain, token as 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( @@ -45,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, } ) ) @@ -61,124 +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 - else: - login_method = 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 LibRouterosError 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 = connect(self._host, self._user, self._password, **kwargs) - self._connected = True - except LibRouterosError 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 LibRouterosError 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 932df2edd29..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==3.0.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/generated/config_flows.py b/homeassistant/generated/config_flows.py index 70fc4355061..cf77dae7fb2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "luftdaten", "mailgun", "met", + "mikrotik", "mobile_app", "mqtt", "neato", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c0222fc62b..93d29daba4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -283,6 +283,9 @@ keyrings.alt==3.4.0 # homeassistant.components.dyson libpurecool==0.6.0 +# homeassistant.components.mikrotik +librouteros==3.0.0 + # homeassistant.components.soundtouch libsoundtouch==0.7.2 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] From 13aae8b5ec0ce39583032eeb51ce710315827c63 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 2 Feb 2020 20:31:39 -0800 Subject: [PATCH 378/393] Bumped version to 0.105.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c0b1c7424cc..5df3dbc2fa5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 9bb8b2bc0023d59178c0627f6af09bfc3d7024f1 Mon Sep 17 00:00:00 2001 From: Yarmo Mackenbach Date: Mon, 3 Feb 2020 13:39:45 +0000 Subject: [PATCH 379/393] Update NSAPI to 3.0.2 (#30971) * Bump NSAPI version to 3.0.1 * Compatibility with NSAPI 3.0.1 response * Removed commented code * Obsolete setups receive an upgrade notification * Bump NS-API to 3.0.2 * Assign platform values directly * Removed obsolete config warning * Improved reference to obsolete password --- .../nederlandse_spoorwegen/manifest.json | 2 +- .../nederlandse_spoorwegen/sensor.py | 55 ++++++++++++------- requirements_all.txt | 2 +- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index 92231bd460c..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==3.0.0"], + "requirements": ["nsapi==3.0.2"], "dependencies": [], "codeowners": ["@YarmoM"] } diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 5477aaf0e2b..df37fad2aa3 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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 = [] @@ -127,20 +127,16 @@ class NSDepartureSensor(Entity): # Static attributes attributes = { "going": self._trips[0].going, - "departure_time_planned": self._trips[0].departure_time_planned.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": None, - "arrival_time_planned": self._trips[0].arrival_time_planned.strftime( - "%H:%M" - ), + "departure_platform_actual": self._trips[0].departure_platform_actual, + "arrival_time_planned": None, "arrival_time_actual": None, "arrival_delay": False, - "arrival_platform_platform": self._trips[0].arrival_platform_planned, - "arrival_platform_actual": None, + "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, @@ -149,25 +145,46 @@ class NSDepartureSensor(Entity): ATTR_ATTRIBUTION: ATTRIBUTION, } - # Departure attributes + # 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") - attributes["departure_delay"] = True - attributes["departure_platform_actual"] = self._trips[ - 0 - ].departure_platform_actual - # Arrival attributes + # 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 - attributes["arrival_platform_actual"] = self._trips[ - 0 - ].arrival_platform_actual # Next attributes if self._trips[1].departure_time_actual is not None: diff --git a/requirements_all.txt b/requirements_all.txt index fec294f1fc5..a494fc645f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -905,7 +905,7 @@ niko-home-control==0.2.1 niluclient==0.1.2 # homeassistant.components.nederlandse_spoorwegen -nsapi==3.0.0 +nsapi==3.0.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 2f2146c989c280d59e461aa5d115979f34d16002 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 3 Feb 2020 20:34:02 +0100 Subject: [PATCH 380/393] Samsung TV refinements (#31248) * use st not deviceType * show model in flow title * Update strings.json * add re-auth to entity * add re-auth to config_flow * handle auth popup better * use media player domain const * fix tests * rename not_found to not_successful * authz not authn * Update media_player.py * Update config_flow.py * Update media_player.py * Update test_media_player.py * finalize re-auth * fix ssd tests * better naming * fix ip-address-mock serialization * fix turn_on_action serialization * add type of hass object * fix acces denied test * remove half-added typing * async get ip address * fix pylint --- .../components/samsungtv/__init__.py | 10 ++- .../components/samsungtv/config_flow.py | 70 +++++++++++-------- homeassistant/components/samsungtv/const.py | 2 +- .../components/samsungtv/manifest.json | 2 +- .../components/samsungtv/media_player.py | 35 ++++++++-- .../components/samsungtv/strings.json | 11 +-- homeassistant/generated/ssdp.py | 2 +- .../components/samsungtv/test_config_flow.py | 28 ++++---- tests/components/samsungtv/test_init.py | 8 ++- .../components/samsungtv/test_media_player.py | 41 ++++++++--- 10 files changed, 138 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 5647b407bfb..bc49dc3156d 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -3,6 +3,7 @@ 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 @@ -41,7 +42,14 @@ CONFIG_SCHEMA = vol.Schema( 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 @@ -54,7 +62,7 @@ async def async_setup(hass, config): 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, "media_player") + 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 index 0bf39cc248b..debe7349b6c 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, - ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_MANUFACTURER, ATTR_UPNP_MODEL_NAME, ATTR_UPNP_UDN, @@ -24,20 +23,13 @@ from homeassistant.const import ( ) # pylint:disable=unused-import -from .const import ( - CONF_MANUFACTURER, - CONF_MODEL, - CONF_ON_ACTION, - DOMAIN, - LOGGER, - METHODS, -) +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_FOUND = "not_found" +RESULT_NOT_SUCCESSFUL = "not_successful" RESULT_NOT_SUPPORTED = "not_supported" @@ -63,23 +55,21 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._method = None self._model = None self._name = None - self._on_script = None self._port = None self._title = None - self._uuid = None + self._id = None def _get_entry(self): return self.async_create_entry( title=self._title, data={ CONF_HOST: self._host, - CONF_ID: self._uuid, + 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_ON_ACTION: self._on_script, CONF_PORT: self._port, }, ) @@ -94,7 +84,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "host": self._host, "method": method, "port": self._port, - "timeout": 1, + # We need this high timeout because waiting for auth popup is just an open socket + "timeout": 31, } try: LOGGER.debug("Try config: %s", config) @@ -108,15 +99,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except UnhandledResponse: LOGGER.debug("Working but unsupported config: %s", config) return RESULT_NOT_SUPPORTED - except (OSError): - LOGGER.debug("Failing config: %s", config) + except OSError as err: + LOGGER.debug("Failing config: %s, error: %s", config, err) LOGGER.debug("No working config found") - return RESULT_NOT_FOUND + return RESULT_NOT_SUCCESSFUL async def async_step_import(self, user_input=None): """Handle configuration by yaml file.""" - self._on_script = user_input.get(CONF_ON_ACTION) self._port = user_input.get(CONF_PORT) return await self.async_step_user(user_input) @@ -133,7 +123,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = user_input.get(CONF_HOST) self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._title = user_input.get(CONF_NAME) + self._name = user_input.get(CONF_NAME) + self._title = self._name result = await self.hass.async_add_executor_job(self._try_connect) @@ -150,24 +141,27 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._host = host self._ip = self.context[CONF_IP_ADDRESS] = ip_address - self._manufacturer = user_input[ATTR_UPNP_MANUFACTURER] - self._model = user_input[ATTR_UPNP_MODEL_NAME] - self._name = user_input[ATTR_UPNP_FRIENDLY_NAME] - if self._name.startswith("[TV]"): - self._name = self._name[4:] - self._title = f"{self._name} ({self._model})" - self._uuid = user_input[ATTR_UPNP_UDN] - if self._uuid.startswith("uuid:"): - self._uuid = self._uuid[5:] + 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._uuid + 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): @@ -182,3 +176,19 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 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 7cf71e406cb..ea893390a5b 100644 --- a/homeassistant/components/samsungtv/const.py +++ b/homeassistant/components/samsungtv/const.py @@ -4,7 +4,7 @@ import logging LOGGER = logging.getLogger(__package__) DOMAIN = "samsungtv" -DEFAULT_NAME = "Samsung TV Remote" +DEFAULT_NAME = "Samsung TV" CONF_MANUFACTURER = "manufacturer" CONF_MODEL = "model" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 0d0a360fc20..3adc3b52eb3 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ ], "ssdp": [ { - "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1" + "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], "dependencies": [], diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index aca54838a99..8de42d157b7 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -23,7 +23,9 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( CONF_HOST, CONF_ID, + CONF_IP_ADDRESS, CONF_METHOD, + CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON, @@ -59,8 +61,16 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Samsung TV from a config entry.""" - turn_on_action = config_entry.data.get(CONF_ON_ACTION) - on_script = Script(hass, turn_on_action) if turn_on_action else None + 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)]) @@ -70,12 +80,11 @@ class SamsungTVDevice(MediaPlayerDevice): def __init__(self, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry - self._name = config_entry.title - self._uuid = config_entry.data.get(CONF_ID) 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._update_listener = None + 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 @@ -88,7 +97,7 @@ class SamsungTVDevice(MediaPlayerDevice): # Generate a configuration for the Samsung library self._config = { "name": "HomeAssistant", - "description": self._name, + "description": "HomeAssistant", "id": "ha.component.samsung", "method": config_entry.data[CONF_METHOD], "port": config_entry.data.get(CONF_PORT), @@ -124,7 +133,19 @@ class SamsungTVDevice(MediaPlayerDevice): """Create or return a remote control instance.""" 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 diff --git a/homeassistant/components/samsungtv/strings.json b/homeassistant/components/samsungtv/strings.json index ee762503e5c..2e36062669f 100644 --- a/homeassistant/components/samsungtv/strings.json +++ b/homeassistant/components/samsungtv/strings.json @@ -1,10 +1,11 @@ { "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 authentication.", + "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" @@ -12,15 +13,15 @@ }, "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 authentication. Manual configurations for this TV will be overwritten." + "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 authenticated to connect to this Samsung TV.", - "not_found": "No supported Samsung TV devices found on the network.", - "not_supported": "This Samsung TV devices is currently not supported." + "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/generated/ssdp.py b/homeassistant/generated/ssdp.py index 83f375f031b..bea04484b11 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -38,7 +38,7 @@ SSDP = { ], "samsungtv": [ { - "deviceType": "urn:samsung.com:device:RemoteControlReceiver:1" + "st": "urn:samsung.com:device:RemoteControlReceiver:1" } ], "sonos": [ diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index ce6741f0703..9c8ec3a9a09 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -42,7 +42,7 @@ AUTODETECT_WEBSOCKET = { "method": "websocket", "port": None, "host": "fake_host", - "timeout": 1, + "timeout": 31, } AUTODETECT_LEGACY = { "name": "HomeAssistant", @@ -51,7 +51,7 @@ AUTODETECT_LEGACY = { "method": "legacy", "port": None, "host": "fake_host", - "timeout": 1, + "timeout": 31, } @@ -87,7 +87,7 @@ async def test_user(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake_name" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] is None + 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 @@ -123,19 +123,19 @@ async def test_user_not_supported(hass): assert result["reason"] == "not_supported" -async def test_user_not_found(hass): - """Test starting a flow by user but no device found.""" +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 found + # 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_found" + assert result["reason"] == "not_successful" async def test_user_already_configured(hass, remote): @@ -170,9 +170,9 @@ async def test_ssdp(hass, remote): result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "fake_name (fake_model)" + assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" + 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" @@ -193,9 +193,9 @@ async def test_ssdp_noprefix(hass, remote): result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "fake2_name (fake2_model)" + assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "fake2_name" + 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" @@ -245,7 +245,7 @@ async def test_ssdp_not_supported(hass): assert result["reason"] == "not_supported" -async def test_ssdp_not_found(hass): +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", @@ -264,7 +264,7 @@ async def test_ssdp_not_found(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_found" + assert result["reason"] == "not_successful" async def test_ssdp_already_in_progress(hass, remote): @@ -380,7 +380,7 @@ async def test_autodetect_none(hass, remote): DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_found" + assert result["reason"] == "not_successful" assert remote.call_count == 2 assert remote.call_args_list == [ call(AUTODETECT_WEBSOCKET), diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 55ec52b56ae..cd31434e6b0 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -32,7 +32,7 @@ MOCK_CONFIG = { } REMOTE_CALL = { "name": "HomeAssistant", - "description": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_NAME], + "description": "HomeAssistant", "id": "ha.component.samsung", "method": "websocket", "port": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_PORT], @@ -44,11 +44,13 @@ REMOTE_CALL = { @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("homeassistant.components.samsungtv.socket"), patch( + with patch("homeassistant.components.samsungtv.socket") as socket1, patch( "homeassistant.components.samsungtv.config_flow.socket" - ), patch("homeassistant.components.samsungtv.config_flow.Remote"), patch( + ) 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 diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2b9f379515d..ba245ce7d6f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -75,15 +75,17 @@ MOCK_CONFIG_NOTURNON = { @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("homeassistant.components.samsungtv.config_flow.socket"), patch( - "homeassistant.components.samsungtv.config_flow.Remote" - ), 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.socket" - ): + ) as socket2: remote = mock.Mock() remote_class.return_value = remote + socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" + socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" yield remote @@ -135,11 +137,12 @@ 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) + with patch( "homeassistant.components.samsungtv.media_player.SamsungRemote", side_effect=[OSError("Boom"), mock.DEFAULT], - ), patch("homeassistant.components.samsungtv.config_flow.socket"): - await setup_samsungtv(hass, MOCK_CONFIG) + ): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): @@ -150,13 +153,35 @@ async def test_update_off(hass, remote, mock_now): assert state.state == STATE_OFF +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], - ), patch("homeassistant.components.samsungtv.config_flow.socket"): - await setup_samsungtv(hass, MOCK_CONFIG) + ): next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): From 1008ab20ba1f40735674e5723a4a25c2e56b5015 Mon Sep 17 00:00:00 2001 From: quthla Date: Mon, 3 Feb 2020 23:09:25 +0100 Subject: [PATCH 381/393] Fix theme color (#31366) --- homeassistant/components/frontend/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8039b9947e7..fdea21fe91e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -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} ) From 10d5ce24f6f0cee415b43e7e588b0077c20afdee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 3 Feb 2020 23:22:47 +0100 Subject: [PATCH 382/393] Keep track of the derivative for unit_time (#31397) * keep track of the derivative for unit_time In this way, you will get a better estimate of the derivate during the timescale that is relavant to the sensor. This solved a problem where sensors have a low output resolution. For example a temperature sensor that can only be integer numbers. It might report many values that are the same and then suddenly go up one value. Only in that moment (with the current implementation) the derivative will be finite. With my proposed implementation, this problem will not occur, because it takes the average derivative of the last `unit_time`. * only loop as much as needed * treat the special case of 1 entry * add option time_window * use cv.time_period * fix comment * set time_window=0 by default * rephrase comment * use timedelta for time_window * fix the "G" unit_prefix and add more prefixes https://en.wikipedia.org/wiki/Unit_prefix * add debugging lines * simplify logic * fix bug where the there was a division of unit_time instead of multiplication * simplify tests * add test_data_moving_average_for_discrete_sensor * fix test_dataSet6 * improve readability of the tests * better explain the test * remove debugging log lines --- homeassistant/components/derivative/sensor.py | 60 +++-- tests/components/derivative/test_sensor.py | 205 ++++++------------ 2 files changed, 113 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 177d1258f3c..5e68b268685 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -27,9 +27,19 @@ 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, "k": 10 ** 3, "G": 10 ** 6, "T": 10 ** 9} +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} @@ -37,6 +47,7 @@ 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( { @@ -46,6 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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, } ) @@ -53,12 +65,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the derivative sensor.""" derivative = DerivativeSensor( - config[CONF_SOURCE], - config.get(CONF_NAME), - config[CONF_ROUND_DIGITS], - config[CONF_UNIT_PREFIX], - config[CONF_UNIT_TIME], - config.get(CONF_UNIT), + 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]) @@ -75,11 +88,13 @@ class DerivativeSensor(RestoreEntity): 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" @@ -93,6 +108,7 @@ class DerivativeSensor(RestoreEntity): 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.""" @@ -114,6 +130,19 @@ class DerivativeSensor(RestoreEntity): ): 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( @@ -122,13 +151,16 @@ class DerivativeSensor(RestoreEntity): try: # derivative of previous measures. - gradient = 0 - elapsed_time = ( - new_state.last_updated - old_state.last_updated - ).total_seconds() - gradient = Decimal(new_state.state) - Decimal(old_state.state) - derivative = gradient / ( - Decimal(elapsed_time) * (self._unit_prefix * self._unit_time) + 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: diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 8893319ab36..05ce55223d0 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -38,26 +38,30 @@ async def test_state(hass): assert state.attributes.get("unit_of_measurement") == "kW" -async def test_dataSet1(hass): - """Test derivative sensor state.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } +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 [(20, 10), (30, 30), (40, 5), (50, 0)]: + 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) @@ -66,163 +70,88 @@ async def test_dataSet1(hass): state = hass.states.get("sensor.power") assert state is not None - assert round(float(state.state), config["sensor"]["round"]) == -0.5 + 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.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - 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() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 0)]: - 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"]) == -0.5 + 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.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - 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() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 10)]: - 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"]) == 0.5 + 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.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - 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() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 5), (30, 5)]: - 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"]) == 0 + 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.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "unit_time": "s", - "round": 2, - } - } - - 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() - - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 10), (30, -10)]: - 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"]) == -2 + 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.""" - config = { - "sensor": { - "platform": "derivative", - "name": "power", - "source": "sensor.energy", - "round": 2, - } - } + await setup_tests(hass, {}, times=[0, 60], values=[0, 1 / 60], expected_state=1) - 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() +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%. - # Testing a energy sensor with non-monotonic intervals and values - for time, value in [(20, 0), (30, 36000)]: + 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() - state = hass.states.get("sensor.power") - assert state is not None - - assert round(float(state.state), config["sensor"]["round"]) == 1 + 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): From af75a4bc85aad2da0a3c8acc7ea2568697688fcb Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Tue, 4 Feb 2020 17:23:08 +0100 Subject: [PATCH 383/393] Update libpurecool upstream library to latest version (#31457) * Update upstream library to latest version * update version in requirements_all.txt * update version in requirements_all.txt --- homeassistant/components/dyson/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 4fc49b4ca60..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": ["@etheralm"] } diff --git a/requirements_all.txt b/requirements_all.txt index a494fc645f4..29ad55ff7d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93d29daba4a..791e47ea80a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ 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 From e5b6fbf37423c1c3be5236a3937624b169402ff4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 4 Feb 2020 17:07:09 +0100 Subject: [PATCH 384/393] Updated frontend to 20200130.1 (#31460) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- requirements_test_pre_commit.txt | 1 - 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6b16970c675..09bd35ba89b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20200130.0" + "home-assistant-frontend==20200130.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ce2d357f82..41e00c5d8de 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.31 -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 importlib-metadata==1.4.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 29ad55ff7d8..0f44cb7705c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 791e47ea80a..33d81e4b24d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -247,7 +247,7 @@ hole==0.5.0 holidays==0.9.12 # homeassistant.components.frontend -home-assistant-frontend==20200130.0 +home-assistant-frontend==20200130.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 87ff3604dd6..8af2cbb6123 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,7 +2,6 @@ bandit==1.6.2 black==19.10b0 -codespell==v1.16.0 flake8-docstrings==1.5.0 flake8==3.7.9 isort==v4.3.21 From d411ae250317069f726ea80a83ed3a81b4250a97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 09:29:39 -0800 Subject: [PATCH 385/393] Bumped version to 0.105.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5df3dbc2fa5..e321d3f8ba3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 105 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0b7" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 250951895038de614922a5f3a624e026beabf330 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 4 Feb 2020 14:31:03 -0500 Subject: [PATCH 386/393] Update vizio host check to handle entries that don't have port (#31463) * Update vizio host check to handle entries that don't have port * add comment explaining no_port test for future * remove _name_is_same function and support user updating name in config * Update strings.json Co-authored-by: Paulus Schoutsen --- homeassistant/components/vizio/config_flow.py | 30 +++++++--- homeassistant/components/vizio/strings.json | 4 +- tests/components/vizio/test_config_flow.py | 56 ++++++++++++++++++- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 5500ec3db94..04f70da4a8c 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -53,6 +53,11 @@ def _get_config_flow_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: ) +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.""" @@ -108,7 +113,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if new config entry matches any existing config entries for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: + if _host_is_same(entry.data[CONF_HOST], user_input[CONF_HOST]): errors[CONF_HOST] = "host_exists" break @@ -165,24 +170,31 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """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 entry.data[CONF_HOST] == import_config[CONF_HOST] and entry.data[ - CONF_NAME - ] == import_config.get(CONF_NAME): + 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: + if updated_options or updated_name: new_data = entry.data.copy() - new_data.update(updated_options) new_options = entry.options.copy() - new_options.update(updated_options) + + 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_options") + return self.async_abort(reason="updated_entry") return self.async_abort(reason="already_setup") @@ -199,7 +211,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # 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 entry.data[CONF_HOST] == discovery_info[CONF_HOST]: + 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 diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 305e49d56f8..64b2fb5f936 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -21,7 +21,7 @@ "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_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_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": { @@ -35,4 +35,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index c82c7a8de0f..cf6cdb6afdb 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -231,6 +231,30 @@ async def test_user_host_already_configured( 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, @@ -394,13 +418,43 @@ async def test_import_flow_update_options( ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "updated_options" + 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, From 6d7989892689e594d25d22fb1bdb77b4ad37263e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 14:57:15 -0800 Subject: [PATCH 387/393] Fix coordinator reference (#31467) --- homeassistant/components/hue/sensor_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 3db07ba2e5b..f57b0f98d30 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -209,7 +209,7 @@ class GenericHueSensor(entity.Entity): Only used by the generic entity update service. """ - await self.bridge.sensor_manager.coordinator.coordinator.async_request_refresh() + await self.bridge.sensor_manager.coordinator.async_request_refresh() @property def device_info(self): From 2d393b8f8b06052c1f768abf66cf1fb6cb6d0233 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 5 Feb 2020 00:26:47 +0100 Subject: [PATCH 388/393] Fix iCloud device battery level can be None (#31468) --- homeassistant/components/icloud/account.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index afa1ad092a2..af7963d8dc1 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -331,14 +331,13 @@ class IcloudDevice: 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._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_BATTERY_STATUS] = self._battery_status - self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode + self._attrs[ATTR_LOW_POWER_MODE] = self._status[DEVICE_LOW_POWER_MODE] if ( self._status[DEVICE_LOCATION] From 8b6b8f1994d3ba7fac69352d0c507d358dd20961 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 07:52:21 -0800 Subject: [PATCH 389/393] Automation device/entity extraction to include triggers + conditions (#31474) * Add support for extracting triggers * Add support for extracting triggers * Fix test --- .../components/automation/__init__.py | 172 ++++++++++++------ tests/components/automation/test_init.py | 24 ++- tests/components/search/test_init.py | 4 +- 3 files changed, 143 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 45f892d783e..528a314dd7b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,16 +1,19 @@ """Allow to set up simple automation rules via the config file.""" -from functools import partial import importlib import logging -from typing import Any, Awaitable, Callable, List +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, @@ -130,7 +133,7 @@ def automations_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: results = [] for automation_entity in component.entities: - if entity_id in automation_entity.action_script.referenced_entities: + if entity_id in automation_entity.referenced_entities: results.append(automation_entity.entity_id) return results @@ -149,7 +152,7 @@ def entities_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: if automation_entity is None: return [] - return list(automation_entity.action_script.referenced_entities) + return list(automation_entity.referenced_entities) @callback @@ -163,7 +166,7 @@ def automations_with_device(hass: HomeAssistant, device_id: str) -> List[str]: results = [] for automation_entity in component.entities: - if device_id in automation_entity.action_script.referenced_devices: + if device_id in automation_entity.referenced_devices: results.append(automation_entity.entity_id) return results @@ -182,7 +185,7 @@ def devices_in_automation(hass: HomeAssistant, entity_id: str) -> List[str]: if automation_entity is None: return [] - return list(automation_entity.action_script.referenced_devices) + return list(automation_entity.referenced_devices) async def async_setup(hass, config): @@ -232,7 +235,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self, automation_id, name, - async_attach_triggers, + trigger_config, cond_func, action_script, hidden, @@ -241,7 +244,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): """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.action_script = action_script @@ -249,6 +252,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): 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): @@ -280,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() @@ -330,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. @@ -373,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 @@ -385,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 @@ -407,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.""" @@ -441,22 +517,12 @@ 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_script, hidden, @@ -471,7 +537,7 @@ async def _async_process_config(hass, config, component): 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: @@ -485,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/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 391c9646dd4..c27a0262a4e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -935,6 +935,11 @@ async def test_extraction_functions(hass): { "alias": "test1", "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "condition": { + "condition": "state", + "entity_id": "light.condition_state", + "state": "on", + }, "action": [ { "service": "test.script", @@ -954,7 +959,20 @@ async def test_extraction_functions(hass): }, { "alias": "test2", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_2"}, + "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", @@ -989,6 +1007,8 @@ async def test_extraction_functions(hass): "automation.test2", } assert set(automation.entities_in_automation(hass, "automation.test1")) == { + "sensor.trigger_1", + "light.condition_state", "light.in_both", "light.in_first", } @@ -997,6 +1017,8 @@ async def test_extraction_functions(hass): "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/search/test_init.py b/tests/components/search/test_init.py index 54a32bed229..a379b91f82a 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -163,7 +163,7 @@ async def test_search(hass): "automation": [ { "alias": "wled_entity", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": {"platform": "template", "value_template": "true"}, "action": [ { "service": "test.script", @@ -173,7 +173,7 @@ async def test_search(hass): }, { "alias": "wled_device", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": {"platform": "template", "value_template": "true"}, "action": [ { "domain": "light", From 97250d8225cde6d2ff3332380be4b3e96d95dfa7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Feb 2020 11:04:17 +0100 Subject: [PATCH 390/393] Re-branding of Hass.io panel to Supervisor (#31480) --- homeassistant/components/hassio/__init__.py | 2 +- tests/components/hassio/test_init.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index f70e44cfa55..cc03f26085c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -194,7 +194,7 @@ 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, 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": { From f1d5fcac75e74148f23cce3a0cccbb8612fcabb4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 5 Feb 2020 17:01:57 +0100 Subject: [PATCH 391/393] Bumped version to 0.105.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e321d3f8ba3..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 = 105 -PATCH_VERSION = "0b7" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 1ee1a43fb9e036512f4343d6e08338c443193eb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 09:00:20 -0800 Subject: [PATCH 392/393] Remove tests for deprecated key (#31491) --- .../components/google_assistant/test_http.py | 32 ------------------- 1 file changed, 32 deletions(-) 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" From 6a4d9d3a730d4fbb79e350145f5ca4140af3d4a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Feb 2020 09:50:00 -0800 Subject: [PATCH 393/393] Fix Google API key test (#31492) --- tests/components/google_assistant/test_init.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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