From a83e741dc7c33430719e3eb25678eef9f0b99864 Mon Sep 17 00:00:00 2001
From: Markus Nigbur <markus@mnigbur.de>
Date: Mon, 20 Nov 2017 04:47:55 +0100
Subject: [PATCH] Refactored to new global json saving and loading (#10677)

* Refactored to new global json saving and loading

* Fixed emulated_hue tests

* Removed unnecassary error handling

* Added missing newline

* Remove unused imports

* Fixed linting error

* Moved _load_json wrapper out of the config class
---
 .gitignore                                    |  2 +-
 homeassistant/components/axis.py              | 27 ++--------
 homeassistant/components/ecobee.py            |  5 +-
 .../components/emulated_hue/__init__.py       | 39 +++++---------
 homeassistant/components/fan/insteon_local.py | 37 ++-----------
 homeassistant/components/ios.py               | 53 ++++---------------
 .../components/light/insteon_local.py         | 38 ++-----------
 .../components/media_player/braviatv.py       | 44 ++-------------
 .../components/media_player/gpmdp.py          | 37 ++-----------
 homeassistant/components/media_player/plex.py | 13 +++--
 homeassistant/components/notify/matrix.py     | 16 ++----
 homeassistant/components/sensor/fitbit.py     | 41 +++-----------
 homeassistant/components/sensor/sabnzbd.py    | 28 ++--------
 .../components/switch/insteon_local.py        | 37 ++-----------
 tests/components/emulated_hue/test_init.py    |  6 +--
 15 files changed, 74 insertions(+), 349 deletions(-)

diff --git a/.gitignore b/.gitignore
index 87bc6990ce4..e01de1b49b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,4 @@ docs/build
 desktop.ini
 /home-assistant.pyproj
 /home-assistant.sln
-/.vs/home-assistant/v14
+/.vs/*
diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py
index 401afe8c62c..a7c820f23c7 100644
--- a/homeassistant/components/axis.py
+++ b/homeassistant/components/axis.py
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/axis/
 """
 
-import json
 import logging
 import os
 
@@ -22,6 +21,7 @@ from homeassistant.helpers import config_validation as cv
 from homeassistant.helpers import discovery
 from homeassistant.helpers.dispatcher import dispatcher_send
 from homeassistant.helpers.entity import Entity
+from homeassistant.util.json import load_json, save_json
 
 
 REQUIREMENTS = ['axis==14']
@@ -103,9 +103,9 @@ def request_configuration(hass, config, name, host, serialnumber):
             return False
 
         if setup_device(hass, config, device_config):
-            config_file = _read_config(hass)
+            config_file = load_json(hass.config.path(CONFIG_FILE))
             config_file[serialnumber] = dict(device_config)
-            _write_config(hass, config_file)
+            save_json(hass.config.path(CONFIG_FILE), config_file)
             configurator.request_done(request_id)
         else:
             configurator.notify_errors(request_id,
@@ -163,7 +163,7 @@ def setup(hass, config):
         serialnumber = discovery_info['properties']['macaddress']
 
         if serialnumber not in AXIS_DEVICES:
-            config_file = _read_config(hass)
+            config_file = load_json(hass.config.path(CONFIG_FILE))
             if serialnumber in config_file:
                 # Device config previously saved to file
                 try:
@@ -274,25 +274,6 @@ def setup_device(hass, config, device_config):
     return True
 
 
-def _read_config(hass):
-    """Read Axis config."""
-    path = hass.config.path(CONFIG_FILE)
-
-    if not os.path.isfile(path):
-        return {}
-
-    with open(path) as f_handle:
-        # Guard against empty file
-        return json.loads(f_handle.read() or '{}')
-
-
-def _write_config(hass, config):
-    """Write Axis config."""
-    data = json.dumps(config)
-    with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
-        outfile.write(data)
-
-
 class AxisDeviceEvent(Entity):
     """Representation of a Axis device event."""
 
diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py
index 0b0c9d1d65a..31cf31dac1e 100644
--- a/homeassistant/components/ecobee.py
+++ b/homeassistant/components/ecobee.py
@@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv
 from homeassistant.helpers import discovery
 from homeassistant.const import CONF_API_KEY
 from homeassistant.util import Throttle
+from homeassistant.util.json import save_json
 
 REQUIREMENTS = ['python-ecobee-api==0.0.10']
 
@@ -110,12 +111,10 @@ def setup(hass, config):
     if 'ecobee' in _CONFIGURING:
         return
 
-    from pyecobee import config_from_file
-
     # Create ecobee.conf if it doesn't exist
     if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)):
         jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)}
-        config_from_file(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
+        save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig)
 
     NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE))
 
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
index d1a58ba941e..1a3b6413d2c 100644
--- a/homeassistant/components/emulated_hue/__init__.py
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/emulated_hue/
 """
 import asyncio
-import json
 import logging
 
 import voluptuous as vol
@@ -16,8 +15,10 @@ from homeassistant.const import (
 )
 from homeassistant.components.http import REQUIREMENTS  # NOQA
 from homeassistant.components.http import HomeAssistantWSGI
+from homeassistant.exceptions import HomeAssistantError
 from homeassistant.helpers.deprecation import get_deprecated
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.json import load_json, save_json
 from .hue_api import (
     HueUsernameView, HueAllLightsStateView, HueOneLightStateView,
     HueOneLightChangeView)
@@ -187,7 +188,7 @@ class Config(object):
             return entity_id
 
         if self.numbers is None:
-            self.numbers = self._load_numbers_json()
+            self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
 
         # Google Home
         for number, ent_id in self.numbers.items():
@@ -198,7 +199,7 @@ class Config(object):
         if self.numbers:
             number = str(max(int(k) for k in self.numbers) + 1)
         self.numbers[number] = entity_id
-        self._save_numbers_json()
+        save_json(self.hass.config.path(NUMBERS_FILE), self.numbers)
         return number
 
     def number_to_entity_id(self, number):
@@ -207,7 +208,7 @@ class Config(object):
             return number
 
         if self.numbers is None:
-            self.numbers = self._load_numbers_json()
+            self.numbers = _load_json(self.hass.config.path(NUMBERS_FILE))
 
         # Google Home
         assert isinstance(number, str)
@@ -244,25 +245,11 @@ class Config(object):
 
         return is_default_exposed or expose
 
-    def _load_numbers_json(self):
-        """Set up helper method to load numbers json."""
-        try:
-            with open(self.hass.config.path(NUMBERS_FILE),
-                      encoding='utf-8') as fil:
-                return json.loads(fil.read())
-        except (OSError, ValueError) as err:
-            # OSError if file not found or unaccessible/no permissions
-            # ValueError if could not parse JSON
-            if not isinstance(err, FileNotFoundError):
-                _LOGGER.warning("Failed to open %s: %s", NUMBERS_FILE, err)
-            return {}
-
-    def _save_numbers_json(self):
-        """Set up helper method to save numbers json."""
-        try:
-            with open(self.hass.config.path(NUMBERS_FILE), 'w',
-                      encoding='utf-8') as fil:
-                fil.write(json.dumps(self.numbers))
-        except OSError as err:
-            # OSError if file write permissions
-            _LOGGER.warning("Failed to write %s: %s", NUMBERS_FILE, err)
+
+def _load_json(filename):
+    """Wrapper, because we actually want to handle invalid json."""
+    try:
+        return load_json(filename)
+    except HomeAssistantError:
+        pass
+    return {}
diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py
index e12e3476c3a..58c8caa331b 100644
--- a/homeassistant/components/fan/insteon_local.py
+++ b/homeassistant/components/fan/insteon_local.py
@@ -4,9 +4,7 @@ Support for Insteon fans via local hub control.
 For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/fan.insteon_local/
 """
-import json
 import logging
-import os
 from datetime import timedelta
 
 from homeassistant.components.fan import (
@@ -14,6 +12,7 @@ from homeassistant.components.fan import (
     SUPPORT_SET_SPEED, FanEntity)
 from homeassistant.helpers.entity import ToggleEntity
 import homeassistant.util as util
+from homeassistant.util.json import load_json, save_json
 
 _CONFIGURING = {}
 _LOGGER = logging.getLogger(__name__)
@@ -33,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the Insteon local fan platform."""
     insteonhub = hass.data['insteon_local']
 
-    conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
+    conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
     if conf_fans:
         for device_id in conf_fans:
             setup_fan(device_id, conf_fans[device_id], insteonhub, hass,
@@ -88,44 +87,16 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
         configurator.request_done(request_id)
         _LOGGER.info("Device configuration done!")
 
-    conf_fans = config_from_file(hass.config.path(INSTEON_LOCAL_FANS_CONF))
+    conf_fans = load_json(hass.config.path(INSTEON_LOCAL_FANS_CONF))
     if device_id not in conf_fans:
         conf_fans[device_id] = name
 
-    if not config_from_file(
-            hass.config.path(INSTEON_LOCAL_FANS_CONF),
-            conf_fans):
-        _LOGGER.error("Failed to save configuration file")
+    save_json(hass.config.path(INSTEON_LOCAL_FANS_CONF), conf_fans)
 
     device = insteonhub.fan(device_id)
     add_devices_callback([InsteonLocalFanDevice(device, name)])
 
 
-def config_from_file(filename, config=None):
-    """Small configuration file management function."""
-    if config:
-        # We're writing configuration
-        try:
-            with open(filename, 'w') as fdesc:
-                fdesc.write(json.dumps(config))
-        except IOError as error:
-            _LOGGER.error('Saving config file failed: %s', error)
-            return False
-        return True
-    else:
-        # We're reading config
-        if os.path.isfile(filename):
-            try:
-                with open(filename, 'r') as fdesc:
-                    return json.loads(fdesc.read())
-            except IOError as error:
-                _LOGGER.error("Reading configuration file failed: %s", error)
-                # This won't work yet
-                return False
-        else:
-            return {}
-
-
 class InsteonLocalFanDevice(FanEntity):
     """An abstract Class for an Insteon node."""
 
diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py
index e3c58425b27..cfa1693f571 100644
--- a/homeassistant/components/ios.py
+++ b/homeassistant/components/ios.py
@@ -5,26 +5,21 @@ For more details about this component, please refer to the documentation at
 https://home-assistant.io/ecosystem/ios/
 """
 import asyncio
-import os
-import json
 import logging
 import datetime
 
 import voluptuous as vol
 # from voluptuous.humanize import humanize_error
 
-from homeassistant.helpers import config_validation as cv
-
-from homeassistant.helpers import discovery
-
-from homeassistant.core import callback
-
 from homeassistant.components.http import HomeAssistantView
-
-from homeassistant.remote import JSONEncoder
-
 from homeassistant.const import (HTTP_INTERNAL_SERVER_ERROR,
                                  HTTP_BAD_REQUEST)
+from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import discovery
+from homeassistant.util.json import load_json, save_json
+
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -174,36 +169,6 @@ CONFIG_FILE = {ATTR_DEVICES: {}}
 CONFIG_FILE_PATH = ""
 
 
-def _load_config(filename):
-    """Load configuration."""
-    if not os.path.isfile(filename):
-        return {}
-
-    try:
-        with open(filename, "r") as fdesc:
-            inp = fdesc.read()
-
-        # In case empty file
-        if not inp:
-            return {}
-
-        return json.loads(inp)
-    except (IOError, ValueError) as error:
-        _LOGGER.error("Reading config file %s failed: %s", filename, error)
-        return None
-
-
-def _save_config(filename, config):
-    """Save configuration."""
-    try:
-        with open(filename, 'w') as fdesc:
-            fdesc.write(json.dumps(config, cls=JSONEncoder))
-    except (IOError, TypeError) as error:
-        _LOGGER.error("Saving config file failed: %s", error)
-        return False
-    return True
-
-
 def devices_with_push():
     """Return a dictionary of push enabled targets."""
     targets = {}
@@ -244,7 +209,7 @@ def setup(hass, config):
 
     CONFIG_FILE_PATH = hass.config.path(CONFIGURATION_FILE)
 
-    CONFIG_FILE = _load_config(CONFIG_FILE_PATH)
+    CONFIG_FILE = load_json(CONFIG_FILE_PATH)
 
     if CONFIG_FILE == {}:
         CONFIG_FILE[ATTR_DEVICES] = {}
@@ -305,7 +270,9 @@ class iOSIdentifyDeviceView(HomeAssistantView):
 
         CONFIG_FILE[ATTR_DEVICES][name] = data
 
-        if not _save_config(CONFIG_FILE_PATH, CONFIG_FILE):
+        try:
+            save_json(CONFIG_FILE_PATH, CONFIG_FILE)
+        except HomeAssistantError:
             return self.json_message("Error saving device.",
                                      HTTP_INTERNAL_SERVER_ERROR)
 
diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py
index 8917a9e9ccf..9d704327a1d 100644
--- a/homeassistant/components/light/insteon_local.py
+++ b/homeassistant/components/light/insteon_local.py
@@ -4,14 +4,14 @@ Support for Insteon dimmers via local hub control.
 For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/light.insteon_local/
 """
-import json
 import logging
-import os
 from datetime import timedelta
 
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
 import homeassistant.util as util
+from homeassistant.util.json import load_json, save_json
+
 
 _CONFIGURING = {}
 _LOGGER = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the Insteon local light platform."""
     insteonhub = hass.data['insteon_local']
 
-    conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
+    conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
     if conf_lights:
         for device_id in conf_lights:
             setup_light(device_id, conf_lights[device_id], insteonhub, hass,
@@ -85,44 +85,16 @@ def setup_light(device_id, name, insteonhub, hass, add_devices_callback):
         configurator.request_done(request_id)
         _LOGGER.debug("Device configuration done")
 
-    conf_lights = config_from_file(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
+    conf_lights = load_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF))
     if device_id not in conf_lights:
         conf_lights[device_id] = name
 
-    if not config_from_file(
-            hass.config.path(INSTEON_LOCAL_LIGHTS_CONF),
-            conf_lights):
-        _LOGGER.error("Failed to save configuration file")
+    save_json(hass.config.path(INSTEON_LOCAL_LIGHTS_CONF), conf_lights)
 
     device = insteonhub.dimmer(device_id)
     add_devices_callback([InsteonLocalDimmerDevice(device, name)])
 
 
-def config_from_file(filename, config=None):
-    """Small configuration file management function."""
-    if config:
-        # We're writing configuration
-        try:
-            with open(filename, 'w') as fdesc:
-                fdesc.write(json.dumps(config))
-        except IOError as error:
-            _LOGGER.error("Saving config file failed: %s", error)
-            return False
-        return True
-    else:
-        # We're reading config
-        if os.path.isfile(filename):
-            try:
-                with open(filename, 'r') as fdesc:
-                    return json.loads(fdesc.read())
-            except IOError as error:
-                _LOGGER.error("Reading configuration file failed: %s", error)
-                # This won't work yet
-                return False
-        else:
-            return {}
-
-
 class InsteonLocalDimmerDevice(Light):
     """An abstract Class for an Insteon node."""
 
diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py
index 399052611c1..f0cc93a8b0f 100644
--- a/homeassistant/components/media_player/braviatv.py
+++ b/homeassistant/components/media_player/braviatv.py
@@ -5,8 +5,6 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/media_player.braviatv/
 """
 import logging
-import os
-import json
 import re
 
 import voluptuous as vol
@@ -18,6 +16,7 @@ from homeassistant.components.media_player import (
     PLATFORM_SCHEMA)
 from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.json import load_json, save_json
 
 REQUIREMENTS = [
     'https://github.com/aparraga/braviarc/archive/0.3.7.zip'
@@ -61,38 +60,6 @@ def _get_mac_address(ip_address):
     return None
 
 
-def _config_from_file(filename, config=None):
-    """Create the configuration from a file."""
-    if config:
-        # We're writing configuration
-        bravia_config = _config_from_file(filename)
-        if bravia_config is None:
-            bravia_config = {}
-        new_config = bravia_config.copy()
-        new_config.update(config)
-        try:
-            with open(filename, 'w') as fdesc:
-                fdesc.write(json.dumps(new_config))
-        except IOError as error:
-            _LOGGER.error("Saving config file failed: %s", error)
-            return False
-        return True
-    else:
-        # We're reading config
-        if os.path.isfile(filename):
-            try:
-                with open(filename, 'r') as fdesc:
-                    return json.loads(fdesc.read())
-            except ValueError as error:
-                return {}
-            except IOError as error:
-                _LOGGER.error("Reading config file failed: %s", error)
-                # This won't work yet
-                return False
-        else:
-            return {}
-
-
 # pylint: disable=unused-argument
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the Sony Bravia TV platform."""
@@ -102,7 +69,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
         return
 
     pin = None
-    bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE))
+    bravia_config = load_json(hass.config.path(BRAVIA_CONFIG_FILE))
     while bravia_config:
         # Set up a configured TV
         host_ip, host_config = bravia_config.popitem()
@@ -136,10 +103,9 @@ def setup_bravia(config, pin, hass, add_devices):
             _LOGGER.info("Discovery configuration done")
 
         # Save config
-        if not _config_from_file(
-                hass.config.path(BRAVIA_CONFIG_FILE),
-                {host: {'pin': pin, 'host': host, 'mac': mac}}):
-            _LOGGER.error("Failed to save configuration file")
+        save_json(
+            hass.config.path(BRAVIA_CONFIG_FILE),
+            {host: {'pin': pin, 'host': host, 'mac': mac}})
 
         add_devices([BraviaTVDevice(host, mac, name, pin)])
 
diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py
index 4090f420855..2f116abebc3 100644
--- a/homeassistant/components/media_player/gpmdp.py
+++ b/homeassistant/components/media_player/gpmdp.py
@@ -6,7 +6,6 @@ https://home-assistant.io/components/media_player.gpmdp/
 """
 import logging
 import json
-import os
 import socket
 import time
 
@@ -19,6 +18,7 @@ from homeassistant.components.media_player import (
 from homeassistant.const import (
     STATE_PLAYING, STATE_PAUSED, STATE_OFF, CONF_HOST, CONF_PORT, CONF_NAME)
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.json import load_json, save_json
 
 REQUIREMENTS = ['websocket-client==0.37.0']
 
@@ -86,8 +86,7 @@ def request_configuration(hass, config, url, add_devices_callback):
                 continue
             setup_gpmdp(hass, config, code,
                         add_devices_callback)
-            _save_config(hass.config.path(GPMDP_CONFIG_FILE),
-                         {"CODE": code})
+            save_json(hass.config.path(GPMDP_CONFIG_FILE), {"CODE": code})
             websocket.send(json.dumps({'namespace': 'connect',
                                        'method': 'connect',
                                        'arguments': ['Home Assistant', code]}))
@@ -122,39 +121,9 @@ def setup_gpmdp(hass, config, code, add_devices):
     add_devices([GPMDP(name, url, code)], True)
 
 
-def _load_config(filename):
-    """Load configuration."""
-    if not os.path.isfile(filename):
-        return {}
-
-    try:
-        with open(filename, 'r') as fdesc:
-            inp = fdesc.read()
-
-        # In case empty file
-        if not inp:
-            return {}
-
-        return json.loads(inp)
-    except (IOError, ValueError) as error:
-        _LOGGER.error("Reading config file %s failed: %s", filename, error)
-        return None
-
-
-def _save_config(filename, config):
-    """Save configuration."""
-    try:
-        with open(filename, 'w') as fdesc:
-            fdesc.write(json.dumps(config, indent=4, sort_keys=True))
-    except (IOError, TypeError) as error:
-        _LOGGER.error("Saving configuration file failed: %s", error)
-        return False
-    return True
-
-
 def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the GPMDP platform."""
-    codeconfig = _load_config(hass.config.path(GPMDP_CONFIG_FILE))
+    codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE))
     if codeconfig:
         code = codeconfig.get('CODE')
     elif discovery_info is not None:
diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py
index 4722a538fa9..9b984813ff6 100644
--- a/homeassistant/components/media_player/plex.py
+++ b/homeassistant/components/media_player/plex.py
@@ -121,13 +121,12 @@ def setup_plexserver(
         _LOGGER.info("Discovery configuration done")
 
     # Save config
-    if not save_json(
-            hass.config.path(PLEX_CONFIG_FILE), {host: {
-                'token': token,
-                'ssl': has_ssl,
-                'verify': verify_ssl,
-            }}):
-        _LOGGER.error("Failed to save configuration file")
+    save_json(
+        hass.config.path(PLEX_CONFIG_FILE), {host: {
+            'token': token,
+            'ssl': has_ssl,
+            'verify': verify_ssl,
+        }})
 
     _LOGGER.info('Connected to: %s://%s', http_prefix, host)
 
diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py
index c3bdeae0280..03bc53e204c 100644
--- a/homeassistant/components/notify/matrix.py
+++ b/homeassistant/components/notify/matrix.py
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/notify.matrix/
 """
 import logging
-import json
 import os
 from urllib.parse import urlparse
 
@@ -15,6 +14,7 @@ import homeassistant.helpers.config_validation as cv
 from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA,
                                              BaseNotificationService)
 from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL
+from homeassistant.util.json import load_json, save_json
 
 REQUIREMENTS = ['matrix-client==0.0.6']
 
@@ -82,8 +82,7 @@ class MatrixNotificationService(BaseNotificationService):
             return {}
 
         try:
-            with open(self.session_filepath) as handle:
-                data = json.load(handle)
+            data = load_json(self.session_filepath)
 
             auth_tokens = {}
             for mx_id, token in data.items():
@@ -101,16 +100,7 @@ class MatrixNotificationService(BaseNotificationService):
         """Store authentication token to session and persistent storage."""
         self.auth_tokens[self.mx_id] = token
 
-        try:
-            with open(self.session_filepath, 'w') as handle:
-                handle.write(json.dumps(self.auth_tokens))
-
-        # Not saving the tokens to disk should not stop the client, we can just
-        # login using the password every time.
-        except (OSError, IOError, PermissionError) as ex:
-            _LOGGER.warning(
-                "Storing authentication tokens to file '%s' failed: %s",
-                self.session_filepath, str(ex))
+        save_json(self.session_filepath, self.auth_tokens)
 
     def login(self):
         """Login to the matrix homeserver and return the client instance."""
diff --git a/homeassistant/components/sensor/fitbit.py b/homeassistant/components/sensor/fitbit.py
index 5f33874c412..35748b30ecf 100644
--- a/homeassistant/components/sensor/fitbit.py
+++ b/homeassistant/components/sensor/fitbit.py
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/sensor.fitbit/
 """
 import os
-import json
 import logging
 import datetime
 import time
@@ -19,6 +18,8 @@ from homeassistant.const import ATTR_ATTRIBUTION
 from homeassistant.helpers.entity import Entity
 from homeassistant.helpers.icon import icon_for_battery_level
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.json import load_json, save_json
+
 
 REQUIREMENTS = ['fitbit==0.3.0']
 
@@ -147,31 +148,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
 })
 
 
-def config_from_file(filename, config=None):
-    """Small configuration file management function."""
-    if config:
-        # We"re writing configuration
-        try:
-            with open(filename, 'w') as fdesc:
-                fdesc.write(json.dumps(config))
-        except IOError as error:
-            _LOGGER.error("Saving config file failed: %s", error)
-            return False
-        return config
-    else:
-        # We"re reading config
-        if os.path.isfile(filename):
-            try:
-                with open(filename, 'r') as fdesc:
-                    return json.loads(fdesc.read())
-            except IOError as error:
-                _LOGGER.error("Reading config file failed: %s", error)
-                # This won"t work yet
-                return False
-        else:
-            return {}
-
-
 def request_app_setup(hass, config, add_devices, config_path,
                       discovery_info=None):
     """Assist user with configuring the Fitbit dev application."""
@@ -182,7 +158,7 @@ def request_app_setup(hass, config, add_devices, config_path,
         """Handle configuration updates."""
         config_path = hass.config.path(FITBIT_CONFIG_FILE)
         if os.path.isfile(config_path):
-            config_file = config_from_file(config_path)
+            config_file = load_json(config_path)
             if config_file == DEFAULT_CONFIG:
                 error_msg = ("You didn't correctly modify fitbit.conf",
                              " please try again")
@@ -242,13 +218,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the Fitbit sensor."""
     config_path = hass.config.path(FITBIT_CONFIG_FILE)
     if os.path.isfile(config_path):
-        config_file = config_from_file(config_path)
+        config_file = load_json(config_path)
         if config_file == DEFAULT_CONFIG:
             request_app_setup(
                 hass, config, add_devices, config_path, discovery_info=None)
             return False
     else:
-        config_file = config_from_file(config_path, DEFAULT_CONFIG)
+        config_file = save_json(config_path, DEFAULT_CONFIG)
         request_app_setup(
             hass, config, add_devices, config_path, discovery_info=None)
         return False
@@ -384,9 +360,7 @@ class FitbitAuthCallbackView(HomeAssistantView):
                 ATTR_CLIENT_SECRET: self.oauth.client_secret,
                 ATTR_LAST_SAVED_AT: int(time.time())
             }
-        if not config_from_file(hass.config.path(FITBIT_CONFIG_FILE),
-                                config_contents):
-            _LOGGER.error("Failed to save config file")
+        save_json(hass.config.path(FITBIT_CONFIG_FILE), config_contents)
 
         hass.async_add_job(setup_platform, hass, self.config, self.add_devices)
 
@@ -513,5 +487,4 @@ class FitbitSensor(Entity):
             ATTR_CLIENT_SECRET: self.client.client.client_secret,
             ATTR_LAST_SAVED_AT: int(time.time())
         }
-        if not config_from_file(self.config_path, config_contents):
-            _LOGGER.error("Failed to save config file")
+        save_json(self.config_path, config_contents)
diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py
index 928e855915a..f034755e780 100644
--- a/homeassistant/components/sensor/sabnzbd.py
+++ b/homeassistant/components/sensor/sabnzbd.py
@@ -4,9 +4,7 @@ Support for monitoring an SABnzbd NZB client.
 For more details about this platform, please refer to the documentation at
 https://home-assistant.io/components/sensor.sabnzbd/
 """
-import os
 import logging
-import json
 from datetime import timedelta
 
 import voluptuous as vol
@@ -17,6 +15,7 @@ from homeassistant.const import (
     CONF_SSL)
 from homeassistant.helpers.entity import Entity
 from homeassistant.util import Throttle
+from homeassistant.util.json import load_json, save_json
 import homeassistant.helpers.config_validation as cv
 
 REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/'
@@ -104,9 +103,9 @@ def request_configuration(host, name, hass, config, add_devices, sab_api):
 
             def success():
                 """Set up was successful."""
-                conf = _read_config(hass)
+                conf = load_json(hass.config.path(CONFIG_FILE))
                 conf[host] = {'api_key': api_key}
-                _write_config(hass, conf)
+                save_json(hass.config.path(CONFIG_FILE), conf)
                 req_config = _CONFIGURING.pop(host)
                 hass.async_add_job(configurator.request_done, req_config)
 
@@ -144,7 +143,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     api_key = config.get(CONF_API_KEY)
 
     if not api_key:
-        conf = _read_config(hass)
+        conf = load_json(hass.config.path(CONFIG_FILE))
         if conf.get(base_url, {}).get('api_key'):
             api_key = conf[base_url]['api_key']
 
@@ -214,22 +213,3 @@ class SabnzbdSensor(Entity):
                 self._state = self.sabnzb_client.queue.get('diskspace1')
             else:
                 self._state = 'Unknown'
-
-
-def _read_config(hass):
-    """Read SABnzbd config."""
-    path = hass.config.path(CONFIG_FILE)
-
-    if not os.path.isfile(path):
-        return {}
-
-    with open(path) as f_handle:
-        # Guard against empty file
-        return json.loads(f_handle.read() or '{}')
-
-
-def _write_config(hass, config):
-    """Write SABnzbd config."""
-    data = json.dumps(config)
-    with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
-        outfile.write(data)
diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py
index 674a20278b3..5fd37c84986 100644
--- a/homeassistant/components/switch/insteon_local.py
+++ b/homeassistant/components/switch/insteon_local.py
@@ -4,13 +4,12 @@ Support for Insteon switch devices via local hub support.
 For more details about this component, please refer to the documentation at
 https://home-assistant.io/components/switch.insteon_local/
 """
-import json
 import logging
-import os
 from datetime import timedelta
 
 from homeassistant.components.switch import SwitchDevice
 import homeassistant.util as util
+from homeassistant.util.json import load_json, save_json
 
 _CONFIGURING = {}
 _LOGGER = logging.getLogger(__name__)
@@ -28,8 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
     """Set up the Insteon local switch platform."""
     insteonhub = hass.data['insteon_local']
 
-    conf_switches = config_from_file(hass.config.path(
-        INSTEON_LOCAL_SWITCH_CONF))
+    conf_switches = load_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF))
     if conf_switches:
         for device_id in conf_switches:
             setup_switch(
@@ -82,43 +80,16 @@ def setup_switch(device_id, name, insteonhub, hass, add_devices_callback):
         configurator.request_done(request_id)
         _LOGGER.info("Device configuration done")
 
-    conf_switch = config_from_file(hass.config.path(INSTEON_LOCAL_SWITCH_CONF))
+    conf_switch = load_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF))
     if device_id not in conf_switch:
         conf_switch[device_id] = name
 
-    if not config_from_file(
-            hass.config.path(INSTEON_LOCAL_SWITCH_CONF), conf_switch):
-        _LOGGER.error("Failed to save configuration file")
+    save_json(hass.config.path(INSTEON_LOCAL_SWITCH_CONF), conf_switch)
 
     device = insteonhub.switch(device_id)
     add_devices_callback([InsteonLocalSwitchDevice(device, name)])
 
 
-def config_from_file(filename, config=None):
-    """Small configuration file management function."""
-    if config:
-        # We're writing configuration
-        try:
-            with open(filename, 'w') as fdesc:
-                fdesc.write(json.dumps(config))
-        except IOError as error:
-            _LOGGER.error("Saving configuration file failed: %s", error)
-            return False
-        return True
-    else:
-        # We're reading config
-        if os.path.isfile(filename):
-            try:
-                with open(filename, 'r') as fdesc:
-                    return json.loads(fdesc.read())
-            except IOError as error:
-                _LOGGER.error("Reading config file failed: %s", error)
-                # This won't work yet
-                return False
-        else:
-            return {}
-
-
 class InsteonLocalSwitchDevice(SwitchDevice):
     """An abstract Class for an Insteon node."""
 
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
index b9ef09fe4a7..25bcbc1dd55 100755
--- a/tests/components/emulated_hue/test_init.py
+++ b/tests/components/emulated_hue/test_init.py
@@ -15,7 +15,7 @@ def test_config_google_home_entity_id_to_number():
     mop = mock_open(read_data=json.dumps({'1': 'light.test2'}))
     handle = mop()
 
-    with patch('homeassistant.components.emulated_hue.open', mop, create=True):
+    with patch('homeassistant.util.json.open', mop, create=True):
         number = conf.entity_id_to_number('light.test')
         assert number == '2'
         assert handle.write.call_count == 1
@@ -45,7 +45,7 @@ def test_config_google_home_entity_id_to_number_altered():
     mop = mock_open(read_data=json.dumps({'21': 'light.test2'}))
     handle = mop()
 
-    with patch('homeassistant.components.emulated_hue.open', mop, create=True):
+    with patch('homeassistant.util.json.open', mop, create=True):
         number = conf.entity_id_to_number('light.test')
         assert number == '22'
         assert handle.write.call_count == 1
@@ -75,7 +75,7 @@ def test_config_google_home_entity_id_to_number_empty():
     mop = mock_open(read_data='')
     handle = mop()
 
-    with patch('homeassistant.components.emulated_hue.open', mop, create=True):
+    with patch('homeassistant.util.json.open', mop, create=True):
         number = conf.entity_id_to_number('light.test')
         assert number == '1'
         assert handle.write.call_count == 1
-- 
GitLab