diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 2b6847b65b8..5e56d125f44 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.4", + "bleak-retry-connector==2.8.5", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.61.1" diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 4c9c83a8787..ce70ed14d19 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -612,7 +612,7 @@ class TimeSMAFilter(Filter, SensorEntity): moving_sum = 0 start = new_state.timestamp - self._time_window - prev_state = self.last_leak or self.queue[0] + prev_state = self.last_leak if self.last_leak is not None else self.queue[0] for state in self.queue: moving_sum += (state.timestamp - start).total_seconds() * prev_state.state start = state.timestamp diff --git a/homeassistant/components/flo/device.py b/homeassistant/components/flo/device.py index ca4b6aa6234..d6e05c17136 100644 --- a/homeassistant/components/flo/device.py +++ b/homeassistant/components/flo/device.py @@ -1,7 +1,6 @@ """Flo device object.""" from __future__ import annotations -import asyncio from datetime import datetime, timedelta from typing import Any @@ -40,14 +39,10 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self): """Update data via library.""" try: - async with timeout(10): - await asyncio.gather( - *[ - self.send_presence_ping(), - self._update_device(), - self._update_consumption_data(), - ] - ) + async with timeout(20): + await self.send_presence_ping() + await self._update_device() + await self._update_consumption_data() except (RequestError) as error: raise UpdateFailed(error) from error diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 632ef04e456..66aa9fe0b92 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.32"], + "requirements": ["flux_led==0.28.34"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b29e44490e9..a7436ef1ae3 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.2.0"], + "requirements": ["xknx==1.2.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8aa2903506f..ec04d5f147d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -375,4 +375,3 @@ async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> No hubs = hass.data[DOMAIN] for name in hubs: await hubs[name].async_close() - del hass.data[DOMAIN] diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 5e15f65035a..e2240f530c6 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -132,6 +132,12 @@ async def async_modbus_setup( await async_setup_reload_service(hass, DOMAIN, [DOMAIN]) + if DOMAIN in hass.data and config[DOMAIN] == []: + hubs = hass.data[DOMAIN] + for name in hubs: + if not await hubs[name].async_setup(): + return False + hass.data[DOMAIN] = hub_collect = {} for conf_hub in config[DOMAIN]: my_hub = ModbusHub(hass, conf_hub) diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index c434d370e27..78352e486e0 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -29,7 +29,7 @@ class NetatmoBase(Entity): self._device_name: str = "" self._id: str = "" self._model: str = "" - self._config_url: str = "" + self._config_url: str | None = None self._attr_name = None self._attr_unique_id = None self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py index 66c66aaabe1..0b12afd9e05 100644 --- a/homeassistant/components/nibe_heatpump/sensor.py +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, + POWER_KILO_WATT, POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -80,6 +81,13 @@ UNIT_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), + "kW": SensorEntityDescription( + key="kW", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_KILO_WATT, + ), "Wh": SensorEntityDescription( key="Wh", entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 2fdf3d61d20..d8550e6f46b 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -18,7 +18,7 @@ from tesla_powerwall import ( from homeassistant.components import persistent_notification from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -221,6 +221,17 @@ def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData: ) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Return True if the last update was successful.""" + return bool( + (domain_data := hass.data.get(DOMAIN)) + and (entry_data := domain_data.get(entry.entry_id)) + and (coordinator := entry_data.get(POWERWALL_COORDINATOR)) + and coordinator.last_update_success + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index b9f6f3969fd..6e4f40bf01b 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -20,11 +20,18 @@ from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.util.network import is_ip_address +from . import async_last_update_was_successful from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = { + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +} + + def _login_and_fetch_site_info( power_wall: Powerwall, password: str ) -> tuple[SiteInfo, str]: @@ -34,6 +41,17 @@ def _login_and_fetch_site_info( return power_wall.get_site_info(), power_wall.get_gateway_din() +def _powerwall_is_reachable(ip_address: str, password: str) -> bool: + """Check if the powerwall is reachable.""" + try: + Powerwall(ip_address).login(password) + except AccessDeniedError: + return True + except PowerwallUnreachableError: + return False + return True + + async def validate_input( hass: core.HomeAssistant, data: dict[str, str] ) -> dict[str, str]: @@ -69,13 +87,31 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.title: str | None = None self.reauth_entry: config_entries.ConfigEntry | None = None + async def _async_powerwall_is_offline( + self, entry: config_entries.ConfigEntry + ) -> bool: + """Check if the power wall is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the powerwall is unreachable + since device may be updating. + """ + ip_address = entry.data[CONF_IP_ADDRESS] + password = entry.data[CONF_PASSWORD] + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(self.hass, entry) + ) and not await self.hass.async_add_executor_job( + _powerwall_is_reachable, ip_address, password + ) + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" self.ip_address = discovery_info.ip gateway_din = discovery_info.hostname.upper() # The hostname is the gateway_din (unique_id) await self.async_set_unique_id(gateway_din) - self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: self.ip_address}) for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_IP_ADDRESS] == discovery_info.ip: if entry.unique_id is not None and is_ip_address(entry.unique_id): @@ -86,6 +122,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_reload(entry.entry_id) ) return self.async_abort(reason="already_configured") + if entry.unique_id == gateway_din: + if await self._async_powerwall_is_offline(entry): + if self.hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_IP_ADDRESS: self.ip_address} + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + # Still need to abort for ignored entries + self._abort_if_unique_id_configured() self.context["title_placeholders"] = { "name": gateway_din, "ip_address": self.ip_address, diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a41db1d18f9..a8f6f316d3a 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.10.0"], + "requirements": ["regenmaschine==2022.11.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 83bf4258a95..f4e82a550e3 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.7.2"], + "requirements": ["pysma==0.7.3"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index c770f1b2382..0294022137c 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.17.0"], + "requirements": ["PyViCare==2.19.0"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/homeassistant/const.py b/homeassistant/const.py index 7b4230c79f1..0fe06355342 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4641c32d562..b83c9c8db01 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/pyproject.toml b/pyproject.toml index d60943ab098..0562e18b5a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.3" +version = "2022.11.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index bbe5c347870..4b9242fe2a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.17.0 +PyViCare==2.19.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 # homeassistant.components.bluetooth bleak==0.19.2 @@ -693,7 +693,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.32 +flux_led==0.28.34 # homeassistant.components.homekit # homeassistant.components.recorder @@ -1893,7 +1893,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.7.2 +pysma==0.7.3 # homeassistant.components.smappee pysmappee==0.2.29 @@ -2153,7 +2153,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.10.0 +regenmaschine==2022.11.0 # homeassistant.components.renault renault-api==0.1.11 @@ -2563,7 +2563,7 @@ xboxapi==2.0.1 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.2.0 +xknx==1.2.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1415f9b4cb6..506a740b2aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.7 # homeassistant.components.vicare -PyViCare==2.17.0 +PyViCare==2.19.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.14.3 @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.4 +bleak-retry-connector==2.8.5 # homeassistant.components.bluetooth bleak==0.19.2 @@ -515,7 +515,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.32 +flux_led==0.28.34 # homeassistant.components.homekit # homeassistant.components.recorder @@ -1334,7 +1334,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.7.2 +pysma==0.7.3 # homeassistant.components.smappee pysmappee==0.2.29 @@ -1489,7 +1489,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.10.0 +regenmaschine==2022.11.0 # homeassistant.components.renault renault-api==0.1.11 @@ -1776,7 +1776,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.10.0 # homeassistant.components.knx -xknx==1.2.0 +xknx==1.2.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index f4dcfd87b8b..11861a8238c 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Powerwall config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from tesla_powerwall import ( AccessDeniedError, @@ -18,6 +18,7 @@ from .mocks import ( MOCK_GATEWAY_DIN, _mock_powerwall_side_effect, _mock_powerwall_site_name, + _mock_powerwall_with_fixtures, ) from tests.common import MockConfigEntry @@ -351,7 +352,7 @@ async def test_dhcp_discovery_update_ip_address(hass): unique_id=MOCK_GATEWAY_DIN, ) entry.add_to_hass(hass) - mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + mock_powerwall = MagicMock(login=MagicMock(side_effect=PowerwallUnreachableError)) with patch( "homeassistant.components.powerwall.config_flow.Powerwall", @@ -375,6 +376,70 @@ async def test_dhcp_discovery_update_ip_address(hass): assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" +async def test_dhcp_discovery_does_not_update_ip_when_auth_fails(hass): + """Test we do not switch to another interface when auth is failing.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = MagicMock(login=MagicMock(side_effect=AccessDeniedError("any"))) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + + +async def test_dhcp_discovery_does_not_update_ip_when_auth_successful(hass): + """Test we do not switch to another interface when auth is successful.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = MagicMock(login=MagicMock(return_value=True)) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.1.1.1", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + + async def test_dhcp_discovery_updates_unique_id(hass): """Test we can update the unique id from dhcp.""" entry = MockConfigEntry( @@ -406,3 +471,70 @@ async def test_dhcp_discovery_updates_unique_id(hass): assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN + + +async def test_dhcp_discovery_updates_unique_id_when_entry_is_failed(hass): + """Test we can update the unique id from dhcp in a failed state.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id="1.2.3.4", + ) + entry.add_to_hass(hass) + entry.state = config_entries.ConfigEntryState.SETUP_ERROR + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" + assert entry.unique_id == MOCK_GATEWAY_DIN + + +async def test_discovered_wifi_does_not_update_ip_if_is_still_online(hass) -> None: + """Test a discovery does not update the ip unless the powerwall at the old ip is offline.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + unique_id=MOCK_GATEWAY_DIN, + ) + entry.add_to_hass(hass) + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", + macaddress="AA:BB:CC:DD:EE:FF", + hostname=MOCK_GATEWAY_DIN.lower(), + ), + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4"