Skip to content
4 changes: 2 additions & 2 deletions homeassistant/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def cmdline() -> List[str]:


def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> Optional[int]:
args: argparse.Namespace) -> int:
"""Set up HASS and run."""
from homeassistant import bootstrap

Expand Down Expand Up @@ -274,7 +274,7 @@ def setup_and_run_hass(config_dir: str,
log_no_color=args.log_no_color)

if hass is None:
return None
return -1

if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
Expand Down
20 changes: 11 additions & 9 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@
# hass.data key for logging information.
DATA_LOGGING = 'logging'

FIRST_INIT_COMPONENT = set((
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
'introduction', 'frontend', 'history'))
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
'logger', 'introduction', 'frontend', 'history'}


def from_config_dict(config: Dict[str, Any],
Expand Down Expand Up @@ -95,7 +94,8 @@ async def async_from_config_dict(config: Dict[str, Any],
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
return None

await hass.async_add_job(conf_util.process_ha_config_upgrade, hass)
await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)

hass.config.skip_pip = skip_pip
if skip_pip:
Expand Down Expand Up @@ -137,15 +137,15 @@ async def async_from_config_dict(config: Dict[str, Any],
for component in components:
if component not in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
hass.async_create_task(async_setup_component(hass, component, config))

await hass.async_block_till_done()

# stage 2
for component in components:
if component in FIRST_INIT_COMPONENT:
continue
hass.async_add_job(async_setup_component(hass, component, config))
hass.async_create_task(async_setup_component(hass, component, config))

await hass.async_block_till_done()

Expand All @@ -162,7 +162,8 @@ def from_config_file(config_path: str,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.

Will add functionality to 'hass' parameter if given,
Expand All @@ -187,7 +188,8 @@ async def async_from_config_file(config_path: str,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False):
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.

Will add functionality to 'hass' parameter.
Expand All @@ -204,7 +206,7 @@ async def async_from_config_file(config_path: str,
log_no_color)

try:
config_dict = await hass.async_add_job(
config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None):
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data)


async def setup(hass, config):
async def async_setup(hass, config):
"""Listen for download events to download files."""
@callback
def log_message(service):
Expand Down
7 changes: 2 additions & 5 deletions homeassistant/components/panel_iframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/panel_iframe/
"""
import asyncio

import voluptuous as vol

from homeassistant.const import (CONF_ICON, CONF_URL)
Expand Down Expand Up @@ -34,11 +32,10 @@
}})}, extra=vol.ALLOW_EXTRA)


@asyncio.coroutine
def setup(hass, config):
async def async_setup(hass, config):
"""Set up the iFrame frontend panels."""
for url_path, info in config[DOMAIN].items():
yield from hass.components.frontend.async_register_built_in_panel(
await hass.components.frontend.async_register_built_in_panel(
'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_path, {'url': info[CONF_URL]})

Expand Down
24 changes: 20 additions & 4 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from time import monotonic

from types import MappingProxyType
from typing import Optional, Any, Callable, List, TypeVar, Dict # NOQA
from typing import ( # NOQA
Optional, Any, Callable, List, TypeVar, Dict, Coroutine)

from async_timeout import timeout
import voluptuous as vol
Expand Down Expand Up @@ -205,8 +206,8 @@ def add_job(self, target: Callable[..., None], *args: Any) -> None:
def async_add_job(
self,
target: Callable[..., Any],
*args: Any) -> Optional[asyncio.tasks.Task]:
"""Add a job from within the eventloop.
*args: Any) -> Optional[asyncio.Future]:
"""Add a job from within the event loop.

This method must be run in the event loop.

Expand All @@ -230,11 +231,26 @@ def async_add_job(

return task

@callback
def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task:
"""Create a task from within the eventloop.

This method must be run in the event loop.

target: target to call.
"""
task = self.loop.create_task(target)

if self._track_task:
self._pending_tasks.append(task)

return task

@callback
def async_add_executor_job(
self,
target: Callable[..., Any],
*args: Any) -> asyncio.tasks.Task:
*args: Any) -> asyncio.Future:
"""Add an executor job from within the event loop."""
task = self.loop.run_in_executor(None, target, *args)

Expand Down
5 changes: 2 additions & 3 deletions homeassistant/helpers/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,10 @@ async def _async_load(self):
data = self._data
else:
data = await self.hass.async_add_executor_job(
json.load_json, self.path, None)
json.load_json, self.path)

if data is None:
if data == {}:
return None

if data['version'] == self.version:
stored = data['data']
else:
Expand Down
13 changes: 10 additions & 3 deletions homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
import sys
from types import ModuleType

from typing import Optional, Set
# pylint: disable=unused-import
from typing import Dict, List, Optional, Sequence, Set, TYPE_CHECKING # NOQA

from homeassistant.const import PLATFORM_FORMAT
from homeassistant.util import OrderedSet

# Typing imports that create a circular dependency
# pylint: disable=using-constant-test,unused-import
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant # NOQA

PREPARED = False

DEPENDENCY_BLACKLIST = set(('config',))
DEPENDENCY_BLACKLIST = {'config'}

_LOGGER = logging.getLogger(__name__)

Expand All @@ -33,7 +39,8 @@
PACKAGE_COMPONENTS = 'homeassistant.components'


def set_component(hass, comp_name: str, component: ModuleType) -> None:
def set_component(hass, # type: HomeAssistant
comp_name: str, component: Optional[ModuleType]) -> None:
"""Set a component in the cache.

Async friendly.
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def async_setup_component(hass: core.HomeAssistant, domain: str,
if setup_tasks is None:
setup_tasks = hass.data[DATA_SETUP] = {}

task = setup_tasks[domain] = hass.async_add_job(
task = setup_tasks[domain] = hass.async_create_task(
_async_setup_component(hass, domain, config))

return await task
Expand Down Expand Up @@ -142,7 +142,7 @@ def log_error(msg, link=True):
result = await component.async_setup( # type: ignore
hass, processed_config)
else:
result = await hass.async_add_job(
result = await hass.async_add_executor_job(
component.setup, hass, processed_config) # type: ignore
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error during setup of component %s", domain)
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/util/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,8 @@ def color_xy_brightness_to_RGB(vX: float, vY: float,
def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> Tuple[int, int, int]:
"""Convert a hsb into its rgb representation."""
if fS == 0:
fV = fB * 255
return (fV, fV, fV)
fV = int(fB * 255)
return fV, fV, fV

r = g = b = 0
h = fH / 60
Expand Down
26 changes: 13 additions & 13 deletions homeassistant/util/dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
from typing import Any, Dict, Union, Optional, Tuple # NOQA

import pytz
import pytz.exceptions as pytzexceptions

DATE_STR_FORMAT = "%Y-%m-%d"
UTC = DEFAULT_TIME_ZONE = pytz.utc # type: dt.tzinfo
UTC = pytz.utc
DEFAULT_TIME_ZONE = pytz.utc # type: dt.tzinfo


# Copyright (c) Django Software Foundation and individual contributors.
Expand Down Expand Up @@ -42,7 +44,7 @@ def get_time_zone(time_zone_str: str) -> Optional[dt.tzinfo]:
"""
try:
return pytz.timezone(time_zone_str)
except pytz.exceptions.UnknownTimeZoneError:
except pytzexceptions.UnknownTimeZoneError:
return None


Expand All @@ -64,7 +66,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime:
if dattim.tzinfo == UTC:
return dattim
elif dattim.tzinfo is None:
dattim = DEFAULT_TIME_ZONE.localize(dattim)
dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore

return dattim.astimezone(UTC)

Expand Down Expand Up @@ -92,7 +94,7 @@ def as_local(dattim: dt.datetime) -> dt.datetime:

def utc_from_timestamp(timestamp: float) -> dt.datetime:
"""Return a UTC time from a timestamp."""
return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC)
return UTC.localize(dt.datetime.utcfromtimestamp(timestamp))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same? Is a non timezone aware date that is being localized treated as having been UTC time? Or would it treat it as local time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the implementation of UTC.localize

    def localize(self, dt, is_dst=False):
        '''Convert naive time to local time'''
        if dt.tzinfo is not None:
            raise ValueError('Not naive datetime (tzinfo is already set)')
        return dt.replace(tzinfo=self)



def start_of_local_day(dt_or_d:
Expand All @@ -102,13 +104,14 @@ def start_of_local_day(dt_or_d:
date = now().date() # type: dt.date
elif isinstance(dt_or_d, dt.datetime):
date = dt_or_d.date()
return DEFAULT_TIME_ZONE.localize(dt.datetime.combine(date, dt.time()))
return DEFAULT_TIME_ZONE.localize(dt.datetime.combine( # type: ignore
date, dt.time()))


# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
# https://github.com/django/django/blob/master/LICENSE
def parse_datetime(dt_str: str) -> dt.datetime:
def parse_datetime(dt_str: str) -> Optional[dt.datetime]:
"""Parse a string and return a datetime.datetime.

This function supports time zone offsets. When the input contains one,
Expand All @@ -134,14 +137,12 @@ def parse_datetime(dt_str: str) -> dt.datetime:
if tzinfo_str[0] == '-':
offset = -offset
tzinfo = dt.timezone(offset)
else:
tzinfo = None
kws = {k: int(v) for k, v in kws.items() if v is not None}
kws['tzinfo'] = tzinfo
return dt.datetime(**kws)


def parse_date(dt_str: str) -> dt.date:
def parse_date(dt_str: str) -> Optional[dt.date]:
"""Convert a date string to a date object."""
try:
return dt.datetime.strptime(dt_str, DATE_STR_FORMAT).date()
Expand Down Expand Up @@ -180,9 +181,8 @@ def get_age(date: dt.datetime) -> str:
def formatn(number: int, unit: str) -> str:
"""Add "unit" if it's plural."""
if number == 1:
return "1 %s" % unit
elif number > 1:
return "%d %ss" % (number, unit)
return '1 {}'.format(unit)
return '{:d} {}s'.format(number, unit)

def q_n_r(first: int, second: int) -> Tuple[int, int]:
"""Return quotient and remaining."""
Expand Down Expand Up @@ -210,4 +210,4 @@ def q_n_r(first: int, second: int) -> Tuple[int, int]:
if minute > 0:
return formatn(minute, 'minute')

return formatn(second, 'second') if second > 0 else "0 seconds"
return formatn(second, 'second')
10 changes: 4 additions & 6 deletions homeassistant/util/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@

_LOGGER = logging.getLogger(__name__)

_UNDEFINED = object()


class SerializationError(HomeAssistantError):
"""Error serializing the data to JSON."""
Expand All @@ -19,7 +17,7 @@ class WriteError(HomeAssistantError):
"""Error writing the data."""


def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \
def load_json(filename: str, default: Union[List, Dict, None] = None) \
-> Union[List, Dict]:
"""Load JSON data from a file and return as dict or list.

Expand All @@ -37,7 +35,7 @@ def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \
except OSError as error:
_LOGGER.exception('JSON file reading failed: %s', filename)
raise HomeAssistantError(error)
return {} if default is _UNDEFINED else default
return {} if default is None else default

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This not the same. By removing the sentinel value, it is no longer possible to have None be the default value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were no uses of None as default value except the place I changed in storage.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was no use yet, as the storage helper is very new. I don't think that we should change this.

@andrey-git andrey-git Jul 12, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The storage helper was the lone use. I meant that no one else used None for load_json

By using sentinel value the function becomes not type-safe, as it accepts object and thus could return object if one were to pass it as default value.

Another option is to have {} as the default value and ignore that warning that generates with a comment that we are not changing it, nor do we return it as-is for the caller to modify.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel too strongly I guess.

Any place that was using util.json or util.yaml needs to be rewritten to use the storage helper anyway.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you could also make the sentinal value an empty dict, that way you can keep it all as dict. Comparison is still done with is so will work the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will generate a warning that the default value is mutable.

Lets keep it None



def save_json(filename: str, data: Union[List, Dict]):
Expand All @@ -46,9 +44,9 @@ def save_json(filename: str, data: Union[List, Dict]):
Returns True on success.
"""
try:
data = json.dumps(data, sort_keys=True, indent=4)
json_data = json.dumps(data, sort_keys=True, indent=4)
with open(filename, 'w', encoding='utf-8') as fdesc:
fdesc.write(data)
fdesc.write(json_data)
except TypeError as error:
_LOGGER.exception('Failed to serialize to JSON: %s',
filename)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/util/unit_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ def __init__(self: object, name: str, temperature: str, length: str,
self.volume_unit = volume

@property
def is_metric(self: object) -> bool:
def is_metric(self) -> bool:
"""Determine if this is the metric unit system."""
return self.name == CONF_UNIT_SYSTEM_METRIC

def temperature(self: object, temperature: float, from_unit: str) -> float:
def temperature(self, temperature: float, from_unit: str) -> float:
"""Convert the given temperature to this unit system."""
if not isinstance(temperature, Number):
raise TypeError(
Expand All @@ -99,7 +99,7 @@ def temperature(self: object, temperature: float, from_unit: str) -> float:
return temperature_util.convert(temperature,
from_unit, self.temperature_unit)

def length(self: object, length: float, from_unit: str) -> float:
def length(self, length: float, from_unit: str) -> float:
"""Convert the given length to this unit system."""
if not isinstance(length, Number):
raise TypeError('{} is not a numeric value.'.format(str(length)))
Expand Down
Loading