diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 9fe501078c2a45..f7e24d69884975 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -216,7 +216,7 @@ def check_pid(pid_file: str) -> None: try: with open(pid_file, "r") as file: pid = int(file.readline()) - except IOError: + except OSError: # PID File does not exist return @@ -239,7 +239,7 @@ def write_pid(pid_file: str) -> None: try: with open(pid_file, "w") as file: file.write(str(pid)) - except IOError: + except OSError: print(f"Fatal Error: Unable to write pid file {pid_file}") sys.exit(1) @@ -258,7 +258,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: val = fcntl(_fd, F_GETFD) if not val & FD_CLOEXEC: fcntl(_fd, F_SETFD, val | FD_CLOEXEC) - except IOError: + except OSError: pass diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 20fdfc9ee79f6f..a36b35ea9d4be2 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -171,7 +171,7 @@ def _setup_bme680(config): sensor.select_gas_heater_profile(0) else: sensor.set_gas_status(bme680.DISABLE_GAS_MEAS) - except (RuntimeError, IOError): + except (RuntimeError, OSError): _LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address) return None diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c257470bb2d0e8..160c8a5e4551c9 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -142,7 +142,7 @@ def update(self, *_): self.account.update_vehicle_states() for listener in self._update_listeners: listener() - except IOError as exception: + except OSError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " "The vehicle state could not be updated." diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 272a6f5d1be2ed..570de0a239cf8c 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "async_step_deconz_devices": { + "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index e17ea8f065d10a..9e11f522dd53d0 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -110,7 +110,7 @@ def setup(hass, config): server = egardiaserver.EgardiaServer("", rs_port) bound = server.bind() if not bound: - raise IOError( + raise OSError( "Binding error occurred while " + "starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server @@ -123,7 +123,7 @@ def handle_stop_event(event): # listen to home assistant stop event hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) - except IOError: + except OSError: _LOGGER.error("Binding error occurred while starting EgardiaServer") return False diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2b17091ba5cb94..3659b40b7b01b7 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190901.0" + "home-assistant-frontend==20190904.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 35f866b3d813aa..9fc3e2fa58e8fb 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -293,7 +293,7 @@ async def _async_send_message(self, message, targets, data): if self.hass.config.is_allowed_path(uri): try: image_file = open(uri, "rb") - except IOError as error: + except OSError as error: _LOGGER.error( "Image file I/O error(%s): %s", error.errno, error.strerror ) diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 1a3b41f74bd993..8b901dcc61e930 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -157,7 +157,7 @@ def run(self): try: event = self.dev.read_one() - except IOError: # Keyboard Disconnected + except OSError: # Keyboard Disconnected self.dev = None self.hass.bus.fire( KEYBOARD_REMOTE_DISCONNECTED, diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 98418d6be81895..c466d71c4c5fd3 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device = LiveboxPlayTvDevice(host, port, name) livebox_devices.append(device) - except IOError: + except OSError: _LOGGER.error( "Failed to connect to Livebox Play TV at %s:%s. " "Please check your configuration", diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 86f1462e2cca55..28020a801750f3 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -157,7 +157,7 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.info("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 9cd1f1cebc29be..adeba48dbc8517 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -157,7 +157,7 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.warning("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 1df635bbde4ab3..f3ae36c5746855 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,13 +7,19 @@ from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_DEVICE from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription +from . import ( + ATTR_DISCOVERY_HASH, + CONF_UNIQUE_ID, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -26,6 +32,7 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, } ) @@ -45,7 +52,9 @@ async def async_discover(discovery_payload): try: discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, discovery_hash) + await _async_setup_entity( + config, async_add_entities, config_entry, discovery_hash + ) except Exception: if discovery_hash: clear_discovery_hash(hass, discovery_hash) @@ -56,15 +65,17 @@ async def async_discover(discovery_payload): ) -async def _async_setup_entity(config, async_add_entities, discovery_hash=None): +async def _async_setup_entity( + config, async_add_entities, config_entry=None, discovery_hash=None +): """Set up the MQTT Camera.""" - async_add_entities([MqttCamera(config, discovery_hash)]) + async_add_entities([MqttCamera(config, config_entry, discovery_hash)]) -class MqttCamera(MqttDiscoveryUpdate, Camera): +class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): """representation of a MQTT camera.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT Camera.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -73,8 +84,11 @@ def __init__(self, config, discovery_hash): self._qos = 0 self._last_image = None + device_config = config.get(CONF_DEVICE) + Camera.__init__(self) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe MQTT events.""" @@ -85,6 +99,7 @@ async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 5d4f5dd25b52f8..2688b15e837c1d 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -92,7 +92,7 @@ def send_code(call): try: pilight_client.send_code(message_data) - except IOError: + except OSError: _LOGGER.error("Pilight send failed for %s", str(message_data)) hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 7d145315748417..53a4f620dcc3d4 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -66,7 +66,7 @@ def _precheck_image(image, opts): raise ValueError() try: img = Image.open(io.BytesIO(image)) - except IOError: + except OSError: _LOGGER.warning("Failed to open image") raise ValueError() imgfmt = str(img.format) diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index ccefd00c723ac3..33356d0e3b82cc 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -47,7 +47,7 @@ def setup_input(address, port, pull_mode, bouncetime): bounce_time=bouncetime, pin_factory=PiGPIOFactory(address), ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return None diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 8c7d7b7d023b1d..e12d83324fd756 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): button = remote_rpi_gpio.setup_input( address, port_num, pull_mode, bouncetime ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic) devices.append(new_sensor) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index aa20a2909d2ffd..8240de7951d710 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for port, name in ports.items(): try: led = remote_rpi_gpio.setup_output(address, port, invert_logic) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_switch = RemoteRPiGPIOSwitch(name, led, invert_logic) devices.append(new_switch) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index c8969add244a19..109c410c16d0b8 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -94,7 +94,7 @@ def _parse_skyhub_response(data_str): """Parse the Sky Hub data format.""" pattmatch = re.search("attach_dev = '(.*)'", data_str) if pattmatch is None: - raise IOError( + raise OSError( "Error: Impossible to fetch data from" + " Sky Hub. Try to reboot the router." ) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 9eef9d989cb093..86e763142e6191 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -65,7 +65,7 @@ def setup(hass, base_config): srv_info, ) return False - except IOError: + except OSError: _LOGGER.exception( "Server: %s not configured. Error on Supla API access: ", server_address ) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index a4777af54578bb..87fb70bb8886a6 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -117,7 +117,7 @@ def _telnet_command(self, command): response = telnet.read_until(b"\r", timeout=self._timeout) _LOGGER.debug("telnet response: %s", response.decode("ASCII").strip()) return response.decode("ASCII").strip() - except IOError as error: + except OSError as error: _LOGGER.error( 'Command "%s" failed with exception: %s', command, repr(error) ) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index c5e5c4af978773..a32de3da10fb62 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -96,7 +96,7 @@ def update(self): ) sensor_value = self.temper_device.get_temperature(format_str) self.current_value = round(sensor_value, 1) - except IOError: + except OSError: _LOGGER.error( "Failed to get temperature. The device address may" "have changed. Attempting to reset device" diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json new file mode 100644 index 00000000000000..d1b3b7e65fdf8a --- /dev/null +++ b/homeassistant/components/transmission/.translations/en.json @@ -0,0 +1,57 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Set up Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "active_torrents": "Active Torrents", + "current_status": "Current Status", + "download_speed": "Download Speed [MB/s]", + "paused_torrents": "Pause Torrents", + "total_torrents": "Total Torrents", + "upload_speed": "Upload Speed [MB/s]", + "completed_torrents": "Completed torrents (seeding)", + "started_torrents": "Started torrents (downloading)", + "turtle_mode": "‘Turtle mode’ switch", + "scan_interval": "Update frequency" + } + } + }, + "error": { + "cannot_connect": "Unable to Connect to Client" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "active_torrents": "Active Torrents", + "current_status": "Current Status", + "download_speed": "Download Speed [MB/s]", + "paused_torrents": "Pause Torrents", + "total_torrents": "Total Torrents", + "upload_speed": "Upload Speed [MB/s]", + "completed_torrents": "Completed torrents (seeding)", + "started_torrents": "Started torrents (downloading)", + "turtle_mode": "‘Turtle mode’ switch", + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e7f9b94046d367..df0126870e03c9 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -2,8 +2,11 @@ from datetime import timedelta import logging +import transmissionrpc +from transmissionrpc.error import TransmissionError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -13,36 +16,26 @@ CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + ATTR_TORRENT, + CONF_SENSOR_TYPES, + CONF_TURTLE_MODE, + DATA_TRANSMISSION, + DATA_UPDATED, + DOMAIN, + SERVICE_ADD_TORRENT, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "transmission" -DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" DEFAULT_NAME = "Transmission" DEFAULT_PORT = 9091 -TURTLE_MODE = "turtle_mode" - -SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], - "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], - "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], -} - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=120) - -ATTR_TORRENT = "torrent" - -SERVICE_ADD_TORRENT = "add_torrent" +DEFAULT_SCAN_INTERVAL = 120 SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) @@ -55,13 +48,13 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(TURTLE_MODE, default=False): cv.boolean, + vol.Optional(CONF_TURTLE_MODE, default=False): cv.boolean, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, vol.Optional( CONF_MONITORED_CONDITIONS, default=["current_status"] - ): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + ): vol.All(cv.ensure_list, [vol.In(CONF_SENSOR_TYPES)]), } ) }, @@ -69,63 +62,121 @@ ) -def setup(hass, config): +async def async_setup(hass, config): + """Set up the Transmission Component from config.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - host = config[DOMAIN][CONF_HOST] - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - port = config[DOMAIN][CONF_PORT] - scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - - import transmissionrpc - from transmissionrpc.error import TransmissionError - - try: - api = transmissionrpc.Client(host, port=port, user=username, password=password) - api.session_stats() - except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for" " Transmission client are not valid") + if not config_entry.options: + await async_populate_options(hass, config_entry) + + client = TransmissionClient(hass, config_entry) + if not await client.async_setup(): return False - tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api) + return True - tm_data.update() - tm_data.init_torrent_list() - def refresh(event_time): - """Get the latest data from Transmission.""" - tm_data.update() +async def async_populate_options(hass, config_entry): + """Populate default options for Transmission Client.""" + options = {} + options[CONF_MONITORED_CONDITIONS] = {} + for sensor in CONF_SENSOR_TYPES: + options[CONF_MONITORED_CONDITIONS][sensor] = config_entry.data["options"].get( + sensor, False + ) + options[CONF_TURTLE_MODE] = config_entry.data["options"].get( + CONF_TURTLE_MODE, False + ) + options[CONF_SCAN_INTERVAL] = config_entry.data["options"].get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) - track_time_interval(hass, refresh, scan_interval) - - def add_torrent(service): - """Add new torrent to download.""" - torrent = service.data[ATTR_TORRENT] - if torrent.startswith( - ("http", "ftp:", "magnet:") - ) or hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) - else: - _LOGGER.warning( - "Could not add torrent: " "unsupported type or no permission" - ) + hass.config_entries.async_update_entry(config_entry, options=options) - hass.services.register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA - ) - sensorconfig = { - "sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS], - "client_name": config[DOMAIN][CONF_NAME], - } +async def async_unload_entry(hass, entry): + """Unload Transmission Entry from config_entry.""" + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) + return True - discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config) - if config[DOMAIN][TURTLE_MODE]: - discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config) +class TransmissionClient: + """Transmission Client Object.""" - return True + def __init__(self, hass, config_entry): + """Initialize the Transmission RPC API.""" + self.hass = hass + self.config_entry = config_entry + self.host = self.config_entry.data[CONF_HOST] + self.username = self.config_entry.data.get(CONF_USERNAME) + self.password = self.config_entry.data.get(CONF_PASSWORD) + self.port = self.config_entry.data[CONF_PORT] + self.scan_interval = self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + + async def async_setup(self): + """Set up the Transmission client.""" + hass = self.hass + try: + api = transmissionrpc.Client( + self.host, port=self.port, user=self.username, password=self.password + ) + api.session_stats() + except TransmissionError as error: + if str(error).find("401: Unauthorized"): + _LOGGER.error("Credentials for" " Transmission client are not valid") + return False + tm_data = self.hass.data[DATA_TRANSMISSION] = TransmissionData( + self.hass, self.config_entry, api + ) + + tm_data.update() + await tm_data.async_init_torrent_list() + + def refresh(event_time): + """Get the latest data from Transmission.""" + tm_data.update() + + async_track_time_interval( + self.hass, refresh, timedelta(seconds=self.scan_interval) + ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(self.config_entry, "sensor") + ) + if self.config_entry.options.get(CONF_TURTLE_MODE): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + self.config_entry, "switch" + ) + ) + + def add_torrent(self, service): + """Add new torrent to download.""" + torrent = service.data[ATTR_TORRENT] + if torrent.startswith( + ("http", "ftp:", "magnet:") + ) or self.hass.config.is_allowed_path(torrent): + self.api.add_torrent(torrent) + else: + _LOGGER.warning( + "Could not add torrent: " "unsupported type or no permission" + ) + + hass.services.async_register( + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + ) + return True class TransmissionData: @@ -141,6 +192,9 @@ def __init__(self, hass, config, api): self.completed_torrents = [] self.started_torrents = [] self.hass = hass + self.scan_interval = timedelta( + seconds=config.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ) def update(self): """Get the latest data from Transmission instance.""" @@ -162,7 +216,7 @@ def update(self): self.available = False _LOGGER.error("Unable to connect to Transmission client") - def init_torrent_list(self): + async def async_init_torrent_list(self): """Initialize torrent lists.""" self.torrents = self._api.get_torrents() self.completed_torrents = [ diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py new file mode 100644 index 00000000000000..384af7715bd503 --- /dev/null +++ b/homeassistant/components/transmission/config_flow.py @@ -0,0 +1,165 @@ +"""Config flow for Transmission Bittorent Client.""" +import transmissionrpc +from transmissionrpc.error import TransmissionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from .const import ( + CONF_SENSOR_TYPES, + CONF_TURTLE_MODE, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) + + +class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UniFi 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 TransmissionOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Transmission flow.""" + self.config = None + self.errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="one_instance_allowed") + + if user_input is not None: + valid = await self.is_valid(user_input) + if valid: + self.config = user_input + return await self.async_step_options() + + 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.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + errors=self.errors, + ) + + async def is_valid(self, user_input): + """Validate connection to the Transmission Client.""" + try: + transmissionrpc.Client( + user_input[CONF_HOST], + port=user_input[CONF_PORT], + user=user_input.get(CONF_USERNAME), + password=user_input.get(CONF_PASSWORD), + ) + return True + + except TransmissionError as error: + if str(error).find("401: Unauthorized"): + self.errors["base"] = "cannot_connect" + + return False + + async def async_step_options(self, user_input=None): + """Set options for the Transmission Client.""" + if user_input is not None: + self.config["options"] = user_input + return self.async_create_entry( + title=self.config[CONF_NAME], data=self.config + ) + + options = { + vol.Optional(CONF_TURTLE_MODE, default=False): bool, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, + } + for sensor in CONF_SENSOR_TYPES: + options.update( + {vol.Optional(sensor, default=CONF_SENSOR_TYPES[sensor][2]): bool} + ) + + return self.async_show_form(step_id="options", data_schema=vol.Schema(options)) + + async def async_step_import(self, import_config): + """Import from Transmission client config.""" + config = { + CONF_NAME: import_config.get(CONF_NAME, DEFAULT_NAME), + CONF_HOST: import_config[CONF_HOST], + CONF_USERNAME: import_config.get(CONF_USERNAME), + CONF_PASSWORD: import_config.get(CONF_PASSWORD), + CONF_PORT: import_config.get(CONF_PORT, DEFAULT_PORT), + } + + return await self.async_step_user(user_input=config) + + +class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Transmission client options.""" + + def __init__(self, config_entry): + """Initialize Transmission options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Transmission options.""" + if user_input is not None: + options = {} + options[CONF_MONITORED_CONDITIONS] = {} + for sensor in CONF_SENSOR_TYPES: + options[CONF_MONITORED_CONDITIONS][sensor] = user_input[sensor] + options[CONF_TURTLE_MODE] = user_input[CONF_TURTLE_MODE] + options[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL] + + return self.async_create_entry(title="", data=options) + + options = { + vol.Optional( + CONF_TURTLE_MODE, + default=self.config_entry.options.get( + CONF_TURTLE_MODE, + self.config_entry.data["options"][CONF_TURTLE_MODE], + ), + ): bool, + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, + self.config_entry.data["options"][CONF_SCAN_INTERVAL], + ), + ): int, + } + for sensor in CONF_SENSOR_TYPES: + options.update( + { + vol.Optional( + sensor, + default=self.config_entry.options[ + CONF_MONITORED_CONDITIONS + ].get(sensor, self.config_entry.data["options"][sensor]), + ): bool + } + ) + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py new file mode 100644 index 00000000000000..4fe5910405098c --- /dev/null +++ b/homeassistant/components/transmission/const.py @@ -0,0 +1,25 @@ +"""Constants for the Transmission Bittorent Client component.""" +DOMAIN = "transmission" + +CONF_TURTLE_MODE = "turtle_mode" +CONF_SENSOR_TYPES = { + "active_torrents": ["Active Torrents", None, False], + "current_status": ["Status", None, True], + "download_speed": ["Down Speed", "MB/s", False], + "paused_torrents": ["Paused Torrents", None, False], + "total_torrents": ["Total Torrents", None, False], + "upload_speed": ["Up Speed", "MB/s", False], + "completed_torrents": ["Completed Torrents", None, False], + "started_torrents": ["Started Torrents", None, False], +} + +DEFAULT_NAME = "Transmission" +DEFAULT_PORT = 9091 +DEFAULT_SCAN_INTERVAL = 120 + +ATTR_TORRENT = "torrent" + +SERVICE_ADD_TORRENT = "add_torrent" + +DATA_UPDATED = "transmission_data_updated" +DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index bc5da64fcacd9b..9451ce90773399 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -1,6 +1,7 @@ { "domain": "transmission", "name": "Transmission", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/transmission", "requirements": [ "transmissionrpc==0.11" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ac2e64ce92f390..a6617ed60f6cbb 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,41 +1,39 @@ """Support for monitoring the Transmission BitTorrent client API.""" -from datetime import timedelta import logging -from homeassistant.const import STATE_IDLE +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TRANSMISSION, DATA_UPDATED, SENSOR_TYPES +from .const import CONF_SENSOR_TYPES, DATA_TRANSMISSION, DATA_UPDATED _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission" -SCAN_INTERVAL = timedelta(seconds=120) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - if discovery_info is None: - return transmission_api = hass.data[DATA_TRANSMISSION] - monitored_variables = discovery_info["sensors"] - name = discovery_info["client_name"] + name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in monitored_variables: - dev.append( - TransmissionSensor( - sensor_type, - transmission_api, - name, - SENSOR_TYPES[sensor_type][0], - SENSOR_TYPES[sensor_type][1], + for sensor_type in CONF_SENSOR_TYPES: + if config_entry.options[CONF_MONITORED_CONDITIONS].get(sensor_type): + dev.append( + TransmissionSensor( + sensor_type, + transmission_api, + name, + CONF_SENSOR_TYPES[sensor_type][0], + CONF_SENSOR_TYPES[sensor_type][1], + ) ) - ) async_add_entities(dev, True) diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e049f89b3c6a6d..ab383584e83fdc 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -3,4 +3,4 @@ add_torrent: fields: torrent: description: URL, magnet link or Base64 encoded file. - example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent} + example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json new file mode 100644 index 00000000000000..f184cf477d2f73 --- /dev/null +++ b/homeassistant/components/transmission/strings.json @@ -0,0 +1,57 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "active_torrents": "Active Torrents", + "current_status": "Current Status", + "download_speed": "Download Speed [MB/s]", + "paused_torrents": "Pause Torrents", + "total_torrents": "Total Torrents", + "upload_speed": "Upload Speed [MB/s]", + "completed_torrents": "Completed torrents (seeding)", + "started_torrents": "Started torrents (downloading)", + "turtle_mode": "‘Turtle mode’ switch", + "scan_interval": "Update frequency" + } + } + }, + "error": { + "cannot_connect": "Unable to Connect to Client" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "active_torrents": "Active Torrents", + "current_status": "Current Status", + "download_speed": "Download Speed [MB/s]", + "paused_torrents": "Pause Torrents", + "total_torrents": "Total Torrents", + "upload_speed": "Upload Speed [MB/s]", + "completed_torrents": "Completed torrents (seeding)", + "started_torrents": "Started torrents (downloading)", + "turtle_mode": "‘Turtle mode’ switch", + "scan_interval": "Update frequency" + } + } + } + } +} diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index df490cdbe47cbe..9a17adc0ff988b 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,7 +1,7 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity @@ -10,17 +10,17 @@ _LOGGING = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission Turtle Mode" - 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.""" - if discovery_info is None: - return - component_name = DATA_TRANSMISSION - transmission_api = hass.data[component_name] - name = discovery_info["client_name"] + transmission_api = hass.data[DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] async_add_entities([TransmissionSwitch(transmission_api, name)], True) @@ -74,7 +74,7 @@ async def async_added_to_hass(self): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) - def update(self): + async def async_update(self): """Get the latest data from Transmission and updates the state.""" active = self.transmission_client.get_alt_speed_enabled() diff --git a/homeassistant/config.py b/homeassistant/config.py index 4b7efed00e4082..d3bd97dad8f777 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -289,7 +289,7 @@ def _write_default_config(config_dir: str) -> Optional[str]: return config_path - except IOError: + except OSError: print("Unable to create default configuration file", config_path) return None @@ -393,7 +393,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: try: with open(config_path, "wt", encoding="utf-8") as config_file: config_file.write(config_raw) - except IOError: + except OSError: _LOGGER.exception("Migrating to google_translate tts failed") pass diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 32690153221b65..70d4f5dd8f0b30 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -56,6 +56,7 @@ "tplink", "traccar", "tradfri", + "transmission", "twentemilieu", "twilio", "unifi", @@ -67,5 +68,5 @@ "wwlln", "zha", "zone", - "zwave" + "zwave", ] diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3be00c859a7a41..4b97aff19a8983 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -154,8 +154,8 @@ def async_get_or_create( if entity_id: return self._async_update_entity( entity_id, - config_entry_id=config_entry_id, - device_id=device_id, + config_entry_id=config_entry_id or _UNDEF, + device_id=device_id 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 diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2fa5a1cd41ae4b..6ec2ed358d5797 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.17 -home-assistant-frontend==20190901.0 +home-assistant-frontend==20190904.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index e8d8306c8ce038..ceb3609dbdb1b7 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -27,7 +27,7 @@ def install_osx(): try: with open(path, "w", encoding="utf-8") as outp: outp.write(plist) - except IOError as err: + except OSError as err: print("Unable to write to " + path, err) return diff --git a/requirements_all.txt b/requirements_all.txt index 852788e1be305f..41d474808bef3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -631,7 +631,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190901.0 +home-assistant-frontend==20190904.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a23bc7ce610ee8..5e0e0d2a3ea9d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190901.0 +home-assistant-frontend==20190904.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index ecc54e0e209b0c..70b5e941fe3868 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,6 @@ """The tests for mqtt camera component.""" from unittest.mock import ANY +import json from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start @@ -167,3 +168,79 @@ async def test_entity_id_update(hass, mqtt_mock): assert state is not None assert mock_mqtt.async_subscribe.call_count == 1 mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, None) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT camera device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps( + { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "device": { + "identifiers": ["helloworld"], + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + "unique_id": "veryunique", + } + ) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.identifiers == {("mqtt", "helloworld")} + assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.manufacturer == "Whatever" + assert device.name == "Beer" + assert device.model == "Glass" + assert device.sw_version == "0.1-beta" + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "device": { + "identifiers": ["helloworld"], + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + "unique_id": "veryunique", + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.name == "Beer" + + config["device"]["name"] = "Milk" + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.name == "Milk"