Exemple #1
0
 def start(self):
     LOGGER.info('Started Sun Position')
     if not 'longitude' in self.polyConfig[
             'customParams'] or not 'latitude' in self.polyConfig[
                 'customParams']:
         LOGGER.error(
             'Please specify latitude and longitude configuration parameters'
         )
     else:
         self.tz = get_localzone()
         self.today = datetime.date.today()
         self.location = Location()
         self.location.timezone = str(self.tz)
         self.location.longitude = float(
             self.polyConfig['customParams']['longitude'])
         self.location.latitude = float(
             self.polyConfig['customParams']['latitude'])
         if 'elevation' in self.polyConfig['customParams']:
             self.location.elevation = int(
                 self.polyConfig['customParams']['elevation'])
         self.sunrise = self.location.sunrise()
         self.sunset = self.location.sunset()
         ts_now = datetime.datetime.now(self.tz)
         if self.sunrise < ts_now < self.sunset:
             self.sun_above_horizon = True
         self.updateInfo()
    def test_BadTzinfo(self):
        loc = Location()
        loc._location_info = dataclasses.replace(loc._location_info,
                                                 timezone="Bad/Timezone")

        with pytest.raises(ValueError):
            loc.tzinfo
def test_Dawn_NeverReachesDepression():
    d = datetime.date(2016, 5, 29)
    with pytest.raises(ValueError):
        loc = Location(
            LocationInfo("Ghent", "Belgium", "Europe/Brussels", "51°3'N",
                         "3°44'W"))
        loc.solar_depression = 18
        loc.dawn(date=d, local=True)
    def test_SolarDepression(self):
        c = Location(
            LocationInfo("Heidelberg", "Germany", "Europe/Berlin", 49.412,
                         -8.71))
        c.solar_depression = "nautical"
        assert c.solar_depression == 12

        c.solar_depression = 18
        assert c.solar_depression == 18
    def __init__(self, config):
        super(BaseAction, self).__init__(config)
        self._latitude = self.config['latitude']
        self._longitude = self.config['longitude']
        self._timezone = self.config['timezone']

        location = Location(('name', 'region', float(self._latitude),
                             float(self._longitude), self._timezone, 0))

        self.sun = location.sun()
Exemple #6
0
    def init_sun(self):
        latitude = self.AD.latitude
        longitude = self.AD.longitude

        if latitude < -90 or latitude > 90:
            raise ValueError("Latitude needs to be -90 .. 90")

        if longitude < -180 or longitude > 180:
            raise ValueError("Longitude needs to be -180 .. 180")

        self.location = Location(
            LocationInfo("", "", self.AD.tz.zone, latitude, longitude))
Exemple #7
0
def _get_astral_location(info):
    try:
        from astral import LocationInfo
        from astral.location import Location

        latitude, longitude, timezone, elevation = info
        info = ("", "", timezone, latitude, longitude)
        return Location(LocationInfo(*info)), elevation
    except ImportError:
        from astral import Location

        info = ("", "", *info)
        return Location(info), None
def main():
    loc = get_loc_from_ip()
    loc = json.loads(loc.text)

    # loc['latitude'], loc['longitude'] = (float(x) for x in loc['loc'].strip().split(','))
    # loc['time_zone'] = tzlocal.get_localzone().zone
    # print(loc['ip'])

    try:
        location = Location()
        location.name = loc['country']
        location.region = loc['country_iso']
        location.latitude = loc['latitude']
        location.longitude = loc['longitude']
        location.timezone = loc['time_zone']
    except ValueError as e:
        logger.error(str(e))
        return

    sunrise = location.sun()['sunrise'].replace(second=0) + timedelta(
        minutes=0)
    sunset = location.sun()['sunset'].replace(second=0) + timedelta(minutes=0)
    today = datetime.now().astimezone(
        pytz.timezone("Asia/Yerevan")) + timedelta(minutes=0)

    dawn = sunrise.astimezone(
        pytz.timezone("Asia/Yerevan")).strftime('%H:%M:%S')
    dusk = sunset.astimezone(
        pytz.timezone("Asia/Yerevan")).strftime('%H:%M:%S')
    now = today.strftime('%H:%M:%S')
    print(f'Dawn: {dawn}')
    print(f'Dusk: {dusk}')
    print(f'Now: {now}')
    print(
        f"You are in {location.name} and the timezone is {location.timezone}")

    if now < dawn:
        print("oh still dark")
        os.system(
            "gsettings set org.gnome.desktop.interface gtk-theme 'Mc-OS-CTLina-Gnome-Dark-1.3.2'"
        )
    elif dawn < now < dusk:
        print("it a brand new day")
        os.system(
            "gsettings set org.gnome.desktop.interface gtk-theme 'McOS-CTLina-Gnome-1.3.2'"
        )
    else:
        print("oh is dark")
        os.system(
            "gsettings set org.gnome.desktop.interface gtk-theme 'Mc-OS-CTLina-Gnome-Dark-1.3.2'"
        )

    return sunrise.astimezone(
        pytz.timezone("Asia/Yerevan")), sunset.astimezone(
            pytz.timezone("Asia/Yerevan"))
    def test_LocationEquality_NotALocation(self, london_info):
        location = Location(london_info)

        class NotALocation:
            _location_info = london_info

        assert NotALocation() != location
Exemple #10
0
 def __init__(
     self,
     client: InfluxDBClient,
     location_info: LocationInfo,
     endpoints: List[str],
     tz: Any,
     bucket_name: str,
     ignore_sundown: bool = False,
     data_collection_interval=60,
 ) -> None:
     self.client = client
     self.write_api = client.write_api(write_options=SYNCHRONOUS)
     self.location = Location(location_info)
     self.endpoints = endpoints
     self.tz = tz
     self.data: Dict[Any, Any] = {}
     self.BUCKET_NAME = bucket_name
     self.IGNORE_SUN_DOWN = ignore_sundown
     self.DATA_COLLECTION_INTERVAL = data_collection_interval
Exemple #11
0
def day_length(date: datetime, coordinate: Coordinate):
    """
    :param date: A date or datetime object of the day in question.
    :param coordinate: the position on the globe
    :return: a floating point duration of the day at
    """
    try:
        sunrise, sunset = sun.daylight(Observer(coordinate.lat, coordinate.lng), date)
    except ValueError:
        # check if sun in above or below horizon
        if Location(LocationInfo(latitude=coordinate.lat, longitude=coordinate.lng)).solar_elevation(date) > 0:
            return 24
        else:
            return 0
    return (sunset - sunrise) / datetime.timedelta(hours=1)
    def get_astral_location(self) -> Location:
        """
        ASTRAL_LOCATION is a comma separated string in the format of Astral's LocationInfo
        name,region,timezone,latitude,longitude
        eg: 'London,England,Europe/London,51°30'N,00°07'W'

        name and country are purely for labelling, can anything
        lat/long can be either in degrees and minutes or as a float (positive = North/East)
        elevation is in metres
        :return:
        """
        location_tuple = os.environ["ASTRAL_LOCATION"].split(",")
        location_info = LocationInfo(*location_tuple)
        location = Location(location_info)

        return location
def get_astral_location(
    hass: HomeAssistant,
) -> tuple[astral.location.Location, astral.Elevation]:
    """Get an astral location for the current Home Assistant configuration."""
    from astral import LocationInfo  # pylint: disable=import-outside-toplevel
    from astral.location import Location  # pylint: disable=import-outside-toplevel

    latitude = hass.config.latitude
    longitude = hass.config.longitude
    timezone = str(hass.config.time_zone)
    elevation = hass.config.elevation
    info = ("", "", timezone, latitude, longitude)

    # Cache astral locations so they aren't recreated with the same args
    if DATA_LOCATION_CACHE not in hass.data:
        hass.data[DATA_LOCATION_CACHE] = {}

    if info not in hass.data[DATA_LOCATION_CACHE]:
        hass.data[DATA_LOCATION_CACHE][info] = Location(LocationInfo(*info))

    return hass.data[DATA_LOCATION_CACHE][info], elevation
Exemple #14
0
def resh_hours(local_name, region, latitude, longitude, time_zone, elevation, resh_date=date.today()):
    """
    :param local_name: str
    :param region: str
    :param latitude: str
    :param longitude: str
    :param time_zone: str
    :param elevation: int
    :param resh_date: date
    :return: dict
    """
    city = LocationInfo(local_name, region, time_zone, latitude, longitude)

    location = Location(city)
    return {"Nascer do sol": location.sunrise(resh_date, observer_elevation=elevation),
            "Meio-dia solar": location.noon(resh_date),
            "Pôr do sol": location.sunset(resh_date, observer_elevation=elevation),
            "Meia-noite solar": location.midnight(resh_date + timedelta(days=1))}
Exemple #15
0
class Controller(polyinterface.Controller):
    def __init__(self, polyglot):
        super().__init__(polyglot)
        self.name = 'Sun Position'
        self.address = 'sunctrl'
        self.primary = self.address
        self.location = None
        self.tz = None
        self.today = None
        self.sunrise = None
        self.sunset = None
        self.sun_above_horizon = False

    def start(self):
        LOGGER.info('Started Sun Position')
        if not 'longitude' in self.polyConfig[
                'customParams'] or not 'latitude' in self.polyConfig[
                    'customParams']:
            LOGGER.error(
                'Please specify latitude and longitude configuration parameters'
            )
        else:
            self.tz = get_localzone()
            self.today = datetime.date.today()
            self.location = Location()
            self.location.timezone = str(self.tz)
            self.location.longitude = float(
                self.polyConfig['customParams']['longitude'])
            self.location.latitude = float(
                self.polyConfig['customParams']['latitude'])
            if 'elevation' in self.polyConfig['customParams']:
                self.location.elevation = int(
                    self.polyConfig['customParams']['elevation'])
            self.sunrise = self.location.sunrise()
            self.sunset = self.location.sunset()
            ts_now = datetime.datetime.now(self.tz)
            if self.sunrise < ts_now < self.sunset:
                self.sun_above_horizon = True
            self.updateInfo()

    def stop(self):
        LOGGER.info('Sun Position is stopping')

    def shortPoll(self):
        self.updateInfo()

    def updateInfo(self):
        if self.location is None:
            return
        ts_now = datetime.datetime.now(self.tz)
        self.setDriver('GV0', round(self.location.solar_azimuth(), 2))
        self.setDriver('GV1', round(self.location.solar_elevation(), 2))
        self.setDriver('GV2', round(self.location.solar_zenith(ts_now), 2))
        self.setDriver('GV3', round(self.location.moon_phase(), 2))
        date_now = datetime.date.today()
        if date_now != self.today:
            LOGGER.debug('It\'s a new day! Calculating sunrise and sunset...')
            self.today = date_now
            self.sunrise = self.location.sunrise()
            self.sunset = self.location.sunset()
        ts_now = datetime.datetime.now(self.tz)
        if self.sunrise < ts_now < self.sunset:
            if not self.sun_above_horizon:
                LOGGER.info('Sunrise')
                self.reportCmd('DOF')
                self.sun_above_horizon = True
        else:
            if self.sun_above_horizon:
                LOGGER.info('Sunset')
                self.reportCmd('DON')
                self.sun_above_horizon = False

    def query(self):
        for node in self.nodes:
            self.nodes[node].reportDrivers()

    id = 'SUNCTRL'
    commands = {'QUERY': query}
    drivers = [{
        'driver': 'ST',
        'value': 1,
        'uom': 2
    }, {
        'driver': 'GV0',
        'value': 0,
        'uom': 14
    }, {
        'driver': 'GV1',
        'value': 0,
        'uom': 14
    }, {
        'driver': 'GV2',
        'value': 0,
        'uom': 14
    }, {
        'driver': 'GV3',
        'value': 0,
        'uom': 56
    }]
Exemple #16
0
def riyadh(riyadh_info) -> Location:
    return Location(riyadh_info)
Exemple #17
0
def new_delhi(new_delhi_info) -> Location:
    return Location(new_delhi_info)
Exemple #18
0
def london(london_info) -> Location:
    return Location(london_info)
Exemple #19
0
def run():
    defaults = {'log': "info"}

    # Parse any config file specification. We make this parser with add_help=False so
    # that it doesn't parse -h and print help.
    conf_parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter, add_help=False)
    conf_parser.add_argument("--config",
                             help="Specify config file",
                             metavar='FILE')
    args, remaining_argv = conf_parser.parse_known_args()

    # Read configuration file and add it to the defaults hash.
    if args.config:
        config = ConfigParser()
        config.read(args.config)
        if "Defaults" in config:
            defaults.update(dict(config.items("Defaults")))
        else:
            logging.error("Bad config file, missing Defaults section")
            sys.exit(1)

    # Parse rest of arguments
    parser = argparse.ArgumentParser(
        description=__doc__,
        parents=[conf_parser],
    )
    parser.set_defaults(**defaults)
    parser.add_argument("--gw-station-id",
                        help="GoodWe station ID",
                        metavar='ID')
    parser.add_argument("--gw-account",
                        help="GoodWe account",
                        metavar='ACCOUNT')
    parser.add_argument("--gw-password",
                        help="GoodWe password",
                        metavar='PASSWORD')
    parser.add_argument("--mqtt-host",
                        help="MQTT hostname",
                        metavar='MQTT_HOST')
    parser.add_argument("--mqtt-port", help="MQTT port", metavar='MQTT_USER')
    parser.add_argument("--mqtt-user",
                        help="MQTT username",
                        metavar='MQTT_USER')
    parser.add_argument("--mqtt-password",
                        help="MQTT password",
                        metavar='MQTT_PASS')
    parser.add_argument("--mqtt-topic",
                        help="MQTT topic",
                        metavar='MQTT_TOPIC')
    parser.add_argument("--pvo-system-id",
                        help="PVOutput system ID",
                        metavar='ID')
    parser.add_argument("--pvo-api-key",
                        help="PVOutput API key",
                        metavar='KEY')
    parser.add_argument("--pvo-interval",
                        help="PVOutput interval in minutes",
                        type=int,
                        choices=[5, 10, 15])
    parser.add_argument("--telegram-token",
                        help="Telegram bot token",
                        metavar='TELEGRAM_TOKEN')
    parser.add_argument("--telegram-chatid",
                        help="Telegram chat id",
                        metavar='TELEGRAM_CHATID')
    parser.add_argument("--darksky-api-key", help="Dark Sky Weather API key")
    parser.add_argument("--openweather-api-key", help="Open Weather API key")
    parser.add_argument("--log",
                        help="Set log level (default info)",
                        choices=['debug', 'info', 'warning', 'critical'])
    parser.add_argument("--date",
                        help="Copy all readings (max 14/90 days ago)",
                        metavar='YYYY-MM-DD')
    parser.add_argument(
        "--upload-csv",
        help="Upload all readings from csv file (max 14/90 days ago)")
    parser.add_argument("--pv-voltage",
                        help="Send pv voltage instead of grid voltage",
                        action='store_true')
    parser.add_argument("--skip-offline",
                        help="Skip uploads when inverter is offline",
                        action='store_true')
    parser.add_argument(
        "--city", help="Sets timezone and skip uploads from dusk till dawn")
    parser.add_argument(
        '--csv',
        help=
        "Append readings to a Excel compatible CSV file, DATE in the name will be replaced by the current date"
    )
    parser.add_argument('--version',
                        action='version',
                        version='%(prog)s ' + __version__)
    args = parser.parse_args()

    # Configure the logging
    numeric_level = getattr(logging, args.log.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: %s' % loglevel)
    logging.basicConfig(format='%(levelname)-8s %(message)s',
                        level=numeric_level)

    logging.debug("gw2pvo version " + __version__)

    if isinstance(args.skip_offline, str):
        args.skip_offline = args.skip_offline.lower() in [
            'true', 'yes', 'on', '1'
        ]

    if args.upload_csv is None:
        if args.gw_station_id is None or args.gw_account is None or args.gw_password is None:
            if args.mqtt_host is None or args.mqtt_topic is None:
                logging.error(
                    "Missing configuation. Either MQTT configuration or Goodwe (SEMS Portal) credentails need to be provided.\nPlease add either --gw-station-id, --gw-account and --gw-password OR add --mqtt-host and --mqtt-topic (at a minimum). Alternatively, one of these options can also be configured in a configuration file."
                )
                sys.exit(1)

    if args.city:
        city = Location(lookup(args.city, database()))
        os.environ['TZ'] = city.timezone
        time.tzset()
    else:
        city = None
    logging.debug("Timezone {}".format(datetime.now().astimezone().tzinfo))

    # Check if we want to copy old data
    if args.date:
        try:
            copy(args)
        except KeyboardInterrupt:
            sys.exit(1)
        except Exception as exp:
            logging.error(exp)
        sys.exit()
    elif args.upload_csv:
        try:
            copy_csv(args)
        except Exception as exp:
            logging.error(exp)
            sys.exit(1)

    startTime = datetime.now()

    while True:
        currentTime = datetime.now()
        try:
            run_once(args, city)
        except KeyboardInterrupt:
            sys.exit(1)
        except Exception as exp:
            errorMsg = ("Failed to publish data PVOutput - " + str(exp))
            logging.error(str(currentTime) + " - " + str(errorMsg))
            try:
                telegram_notify(args.telegram_token, args.telegram_chatid,
                                errorMsg)
            except Exception as exp:
                logging.error(
                    str(currentTime) +
                    " - Failed to send telegram notification - " + str(exp))

        if args.pvo_interval is None:
            break

        interval = args.pvo_interval * 60
        time.sleep(interval - (datetime.now() - startTime).seconds % interval)
Exemple #20
0
 def test_TimezoneName(self):
     """Test the default timezone and that the timezone is changeable"""
     c = Location()
     assert c.timezone == "Europe/London"
     c.name = "Asia/Riyadh"
     assert c.name == "Asia/Riyadh"
Exemple #21
0
class Scheduler:
    def __init__(self, ad: AppDaemon):
        self.AD = ad

        self.logger = ad.logging.get_child("_scheduler")
        self.error = ad.logging.get_error()
        self.diag = ad.logging.get_diag()
        self.last_fired = None
        self.sleep_task = None
        self.active = False
        self.location = None
        self.schedule = {}

        self.now = pytz.utc.localize(datetime.datetime.utcnow())

        #
        # If we were waiting for a timezone from metadata, we have it now.
        #
        tz = pytz.timezone(self.AD.time_zone)
        self.AD.tz = tz
        self.AD.logging.set_tz(tz)

        self.stopping = False
        self.realtime = True

        self.set_start_time()

        if self.AD.endtime is not None:
            unaware_end = None
            try:
                unaware_end = datetime.datetime.strptime(
                    self.AD.endtime, "%Y-%m-%d %H:%M:%S")
            except ValueError:
                try:
                    unaware_end = datetime.datetime.strptime(
                        self.AD.endtime, "%Y-%m-%d#%H:%M:%S")
                except ValueError:
                    pass
            if unaware_end is None:
                raise ValueError("Invalid end time for time travel")
            aware_end = self.AD.tz.localize(unaware_end)
            self.endtime = aware_end.astimezone(pytz.utc)
        else:
            self.endtime = None

        # Setup sun

        self.init_sun()

    def set_start_time(self):
        tt = False
        unaware_now = None
        if self.AD.starttime is not None:
            tt = True
            try:
                unaware_now = datetime.datetime.strptime(
                    self.AD.starttime, "%Y-%m-%d %H:%M:%S")
            except ValueError:
                # Support "#" as date and time separator as well
                try:
                    unaware_now = datetime.datetime.strptime(
                        self.AD.starttime, "%Y-%m-%d#%H:%M:%S")
                except ValueError:
                    # Catching this allows us to raise a single exception and avoid a nested exception
                    pass
            if unaware_now is None:
                raise ValueError("Invalid start time for time travel")
            aware_now = self.AD.tz.localize(unaware_now)
            self.now = aware_now.astimezone(pytz.utc)
        else:
            self.now = pytz.utc.localize(datetime.datetime.utcnow())

        if self.AD.timewarp != 1:
            tt = True

        return tt

    def stop(self):
        self.logger.debug("stop() called for scheduler")
        self.stopping = True

    async def cancel_timer(self, name, handle):
        executed = False
        self.logger.debug("Canceling timer for %s", name)
        if self.timer_running(name, handle):
            del self.schedule[name][handle]
            await self.AD.state.remove_entity(
                "admin", "scheduler_callback.{}".format(handle))
            executed = True

        if name in self.schedule and self.schedule[name] == {}:
            del self.schedule[name]

        if not executed:
            self.logger.warning(
                "Invalid callback handle '{}' in cancel_timer() from app {}".
                format(handle, name))

        return executed

    def timer_running(self, name, handle):
        """Check if the handler is valid
        by ensuring the timer is still running"""

        if name in self.schedule and handle in self.schedule[name]:
            return True

        return False

    # noinspection PyBroadException
    async def exec_schedule(self, name, args, uuid_):
        try:
            # Call function
            if "__entity" in args["kwargs"]:
                #
                # it's a "duration" entry
                #

                # first remove the duration parameter
                if args["kwargs"].get("__duration"):
                    del args["kwargs"]["__duration"]

                executed = await self.AD.threading.dispatch_worker(
                    name,
                    {
                        "id": uuid_,
                        "name": name,
                        "objectid": self.AD.app_management.objects[name]["id"],
                        "type": "state",
                        "function": args["callback"],
                        "attribute": args["kwargs"]["__attribute"],
                        "entity": args["kwargs"]["__entity"],
                        "new_state": args["kwargs"]["__new_state"],
                        "old_state": args["kwargs"]["__old_state"],
                        "pin_app": args["pin_app"],
                        "pin_thread": args["pin_thread"],
                        "kwargs": args["kwargs"],
                    },
                )

                if executed is True:
                    remove = args["kwargs"].get("oneshot", False)
                    if remove is True:
                        await self.AD.state.cancel_state_callback(
                            args["kwargs"]["__handle"], name)

                        if "__timeout" in args["kwargs"] and self.timer_running(
                                name, args["kwargs"]["__timeout"]
                        ):  # meaning there is a timeout for this callback
                            await self.cancel_timer(
                                name, args["kwargs"]["__timeout"]
                            )  # cancel it as no more needed

            elif "__state_handle" in args["kwargs"]:
                #
                # It's a state timeout entry - just delete the callback
                #
                await self.AD.state.cancel_state_callback(
                    args["kwargs"]["__state_handle"], name)
            elif "__event_handle" in args["kwargs"]:
                #
                # It's an event timeout entry - just delete the callback
                #
                await self.AD.events.cancel_event_callback(
                    name, args["kwargs"]["__event_handle"])
            elif "__log_handle" in args["kwargs"]:
                #
                # It's a log timeout entry - just delete the callback
                #
                await self.AD.logging.cancel_log_callback(
                    name, args["kwargs"]["__log_handle"])
            else:
                #
                # A regular callback
                #
                await self.AD.threading.dispatch_worker(
                    name,
                    {
                        "id": uuid_,
                        "name": name,
                        "objectid": self.AD.app_management.objects[name]["id"],
                        "type": "scheduler",
                        "function": args["callback"],
                        "pin_app": args["pin_app"],
                        "pin_thread": args["pin_thread"],
                        "kwargs": args["kwargs"],
                    },
                )
            # If it is a repeating entry, rewrite with new timestamp
            if args["repeat"]:
                if args["type"] == "next_rising" or args[
                        "type"] == "next_setting":
                    c_offset = self.get_offset(args)
                    args["timestamp"] = self.sun(args["type"], c_offset)
                    args["offset"] = c_offset
                else:
                    # Not sunrise or sunset so just increment
                    # the timestamp with the repeat interval
                    args["basetime"] += timedelta(seconds=args["interval"])
                    args["timestamp"] = args["basetime"] + timedelta(
                        seconds=self.get_offset(args))
                # Update entity

                await self.AD.state.set_state(
                    "_scheduler",
                    "admin",
                    "scheduler_callback.{}".format(uuid_),
                    execution_time=utils.dt_to_str(
                        args["timestamp"].replace(microsecond=0), self.AD.tz),
                )
            else:
                # Otherwise just delete
                await self.AD.state.remove_entity(
                    "admin", "scheduler_callback.{}".format(uuid_))

                del self.schedule[name][uuid_]

        except Exception:
            error_logger = logging.getLogger("Error.{}".format(name))
            error_logger.warning("-" * 60)
            error_logger.warning(
                "Unexpected error during exec_schedule() for App: %s", name)
            error_logger.warning("Args: %s", args)
            error_logger.warning("-" * 60)
            error_logger.warning(traceback.format_exc())
            error_logger.warning("-" * 60)
            if self.AD.logging.separate_error_log() is True:
                self.logger.warning("Logged an error to %s",
                                    self.AD.logging.get_filename("error_log"))
            error_logger.warning("Scheduler entry has been deleted")
            error_logger.warning("-" * 60)
            await self.AD.state.remove_entity(
                "admin", "scheduler_callback.{}".format(uuid_))
            del self.schedule[name][uuid_]

    def init_sun(self):
        latitude = self.AD.latitude
        longitude = self.AD.longitude

        if latitude < -90 or latitude > 90:
            raise ValueError("Latitude needs to be -90 .. 90")

        if longitude < -180 or longitude > 180:
            raise ValueError("Longitude needs to be -180 .. 180")

        self.location = Location(
            LocationInfo("", "", self.AD.tz.zone, latitude, longitude))

    def sun(self, type: str, secs_offset: int):
        return self.get_next_sun_event(
            type, secs_offset) + datetime.timedelta(seconds=secs_offset)

    def get_next_sun_event(self, type: str, day_offset: int):
        if type == "next_rising":
            return self.next_sunrise(day_offset)
        else:
            return self.next_sunset(day_offset)

    def next_sunrise(self, offset: int = 0):
        day_offset = 0
        while True:
            try:
                candidate_date = (
                    self.now + datetime.timedelta(days=day_offset)).astimezone(
                        self.AD.tz).date()
                next_rising_dt = self.location.sunrise(
                    date=candidate_date,
                    local=False,
                    observer_elevation=self.AD.elevation)
                if next_rising_dt + datetime.timedelta(seconds=offset) > (
                        self.now + datetime.timedelta(seconds=1)):
                    break
            except ValueError:
                pass
            day_offset += 1

        return next_rising_dt

    def next_sunset(self, offset: int = 0):
        day_offset = 0
        while True:
            try:
                candidate_date = (
                    self.now + datetime.timedelta(days=day_offset)).astimezone(
                        self.AD.tz).date()
                next_setting_dt = self.location.sunset(
                    date=candidate_date,
                    local=False,
                    observer_elevation=self.AD.elevation)
                if next_setting_dt + datetime.timedelta(seconds=offset) > (
                        self.now + datetime.timedelta(seconds=1)):
                    break
            except ValueError:
                pass
            day_offset += 1

        return next_setting_dt

    @staticmethod
    def get_offset(kwargs: dict):
        if "offset" in kwargs["kwargs"]:
            if "random_start" in kwargs["kwargs"] or "random_end" in kwargs[
                    "kwargs"]:
                raise ValueError(
                    "Can't specify offset as well as 'random_start' or "
                    "'random_end' in 'run_at_sunrise()' or 'run_at_sunset()'")
            else:
                offset = kwargs["kwargs"]["offset"]
        else:
            rbefore = kwargs["kwargs"].get("random_start", 0)
            rafter = kwargs["kwargs"].get("random_end", 0)
            offset = random.randint(rbefore, rafter)
            # self.logger.debug("get_offset(): offset = %s", offset)
        return offset

    async def insert_schedule(self, name, aware_dt, callback, repeat, type_,
                              **kwargs):

        # aware_dt will include a timezone of some sort - convert to utc timezone
        utc = aware_dt.astimezone(pytz.utc)

        # Round to nearest second

        utc = self.my_dt_round(utc, base=1)

        if "pin" in kwargs:
            pin_app = kwargs["pin"]
        else:
            pin_app = self.AD.app_management.objects[name]["pin_app"]

        if "pin_thread" in kwargs:
            pin_thread = kwargs["pin_thread"]
            pin_app = True
        else:
            pin_thread = self.AD.app_management.objects[name]["pin_thread"]

        if name not in self.schedule:
            self.schedule[name] = {}
        handle = uuid.uuid4().hex
        c_offset = self.get_offset({"kwargs": kwargs})
        ts = utc + timedelta(seconds=c_offset)
        interval = kwargs.get("interval", 0)

        self.schedule[name][handle] = {
            "name": name,
            "id": self.AD.app_management.objects[name]["id"],
            "callback": callback,
            "timestamp": ts,
            "interval": interval,
            "basetime": utc,
            "repeat": repeat,
            "offset": c_offset,
            "type": type_,
            "pin_app": pin_app,
            "pin_thread": pin_thread,
            "kwargs": kwargs,
        }

        if callback is None:
            function_name = "cancel_callback"
        else:
            function_name = callback.__name__

        await self.AD.state.add_entity(
            "admin",
            "scheduler_callback.{}".format(handle),
            "active",
            {
                "app":
                name,
                "execution_time":
                utils.dt_to_str(ts.replace(microsecond=0), self.AD.tz),
                "repeat":
                str(datetime.timedelta(seconds=interval)),
                "function":
                function_name,
                "pinned":
                pin_app,
                "pinned_thread":
                pin_thread,
                "fired":
                0,
                "executed":
                0,
                "kwargs":
                kwargs,
            },
        )
        # verbose_log(conf.logger, "INFO", conf.schedule[name][handle])

        if self.active is True:
            await self.kick()
        return handle

    async def terminate_app(self, name):
        if name in self.schedule:
            for id in self.schedule[name]:
                await self.AD.state.remove_entity(
                    "admin", "scheduler_callback.{}".format(id))
            del self.schedule[name]

    def is_realtime(self):
        return self.realtime

    #
    # Timer
    #

    def get_next_entries(self):

        next_exec = datetime.datetime.now(pytz.utc).replace(
            year=datetime.MAXYEAR, month=12, day=31)
        for name in self.schedule.keys():
            for entry in self.schedule[name].keys():
                if self.schedule[name][entry]["timestamp"] < next_exec:
                    next_exec = self.schedule[name][entry]["timestamp"]

        next_entries = []

        for name in self.schedule.keys():
            for entry in self.schedule[name].keys():
                if self.schedule[name][entry]["timestamp"] == next_exec:
                    next_entries.append({
                        "name":
                        name,
                        "uuid":
                        entry,
                        "timestamp":
                        self.schedule[name][entry]["timestamp"]
                    })

        return next_entries

    async def process_dst(self, old, new):
        #
        # Rewrite timestamps to new local time
        #
        offset = old - new
        self.logger.debug("Process_dst()")
        self.logger.debug("offset  %s", offset)
        for app in self.schedule:
            for entry in self.schedule[app]:
                args = self.schedule[app][entry]
                # Sunrise and sunset will already be correct. Anything else needs to be reset to a new local time
                self.logger.debug("Before rewrite: %s", args)
                if args["type"] != "next_rising" and args[
                        "type"] != "next_setting":
                    # If our interval is less than the jump don't rewrite the timestamp
                    if float(args["interval"]) > abs(offset.total_seconds()):
                        args["timestamp"] += offset
                        args["basetime"] += offset
                self.logger.debug("After rewrite: %s", args)

    def get_next_dst_offset(self, base, limit):
        #
        # I can't believe there isn't a better way to find the next DST transition but ...
        # We know the lower and upper bounds of DST so do a search to find the actual transition time
        # I don't want to rely on heuristics such as "it occurs at 2am" because I don't know if that holds
        # true for every timezone. With this method, as long as pytz's dst() function is correct, this should work
        #

        # TODO: Convert this to some sort of binary search for efficiency
        # TODO: This really should support sub 1 second periods better
        self.logger.debug("get_next_dst_offset() base=%s limit=%s", base,
                          limit)
        current = base.astimezone(self.AD.tz).dst()
        self.logger.debug("current=%s", current)
        for offset in range(1, int(limit) + 1):
            candidate = (base + timedelta(seconds=offset)).astimezone(
                self.AD.tz)
            # print(candidate)
            if candidate.dst() != current:
                return offset
        return limit

    async def loop(self):  # noqa: C901
        self.active = True
        self.logger.debug("Starting scheduler loop()")
        self.AD.booted = await self.get_now_naive()

        tt = self.set_start_time()
        self.last_fired = pytz.utc.localize(datetime.datetime.utcnow())
        if tt is True:
            self.realtime = False
            self.logger.info("Starting time travel ...")
            self.logger.info("Setting clocks to %s", await
                             self.get_now_naive())
            if self.AD.timewarp == 0:
                self.logger.info("Time displacement factor infinite")
            else:
                self.logger.info("Time displacement factor %s",
                                 self.AD.timewarp)
        else:
            self.logger.info("Scheduler running in realtime")

        next_entries = []
        result = False
        idle_time = 1
        delay = 0
        old_dst_offset = (await self.get_now()).astimezone(self.AD.tz).dst()
        while not self.stopping:
            try:
                if self.endtime is not None and self.now >= self.endtime:
                    self.logger.info("End time reached, exiting")
                    if self.AD.stop_function is not None:
                        self.AD.stop_function()
                    else:
                        self.stop()
                now = pytz.utc.localize(datetime.datetime.utcnow())
                if self.realtime is True:
                    self.now = now
                else:
                    if result is True:
                        # We got kicked so lets figure out the elapsed pseudo time
                        delta = (now - self.last_fired
                                 ).total_seconds() * self.AD.timewarp
                    else:
                        if len(next_entries) > 0:
                            # Time is progressing infinitely fast and it's already time for our next callback
                            delta = delay
                        else:
                            # No kick, no scheduler expiry ...
                            delta = idle_time

                    self.now = self.now + timedelta(seconds=delta)

                self.last_fired = pytz.utc.localize(datetime.datetime.utcnow())
                self.logger.debug("self.now = %s", self.now)
                #
                # Now we're awake and know what time it is
                #
                dst_offset = (await
                              self.get_now()).astimezone(self.AD.tz).dst()
                self.logger.debug(
                    "local now=%s old_dst_offset=%s new_dst_offset=%s",
                    self.now.astimezone(self.AD.tz),
                    old_dst_offset,
                    dst_offset,
                )
                if old_dst_offset != dst_offset:
                    #
                    # DST began or ended, we need to go fix any existing scheduler entries to match the new local time
                    #
                    self.logger.info(
                        "Daylight Savings Time transition detected - rewriting events to new local time"
                    )
                    await self.process_dst(old_dst_offset, dst_offset)
                    #
                    # Re calculate next entries
                    #
                    next_entries = self.get_next_entries()

                old_dst_offset = dst_offset
                #
                # OK, lets fire the entries
                #
                for entry in next_entries:
                    # Check timestamps as we might have been interrupted to add a callback
                    if entry["timestamp"] <= self.now:
                        name = entry["name"]
                        uuid_ = entry["uuid"]
                        # Things may have changed since we last woke up
                        # so check our callbacks are still valid before we execute them
                        if name in self.schedule and uuid_ in self.schedule[
                                name]:
                            args = self.schedule[name][uuid_]
                            self.logger.debug("Executing: %s", args)
                            await self.exec_schedule(name, args, uuid_)
                    else:
                        break
                for k, v in list(self.schedule.items()):
                    if v == {}:
                        del self.schedule[k]

                next_entries = self.get_next_entries()
                self.logger.debug("Next entries: %s", next_entries)
                if len(next_entries) > 0:
                    delay = (next_entries[0]["timestamp"] -
                             self.now).total_seconds()
                else:
                    # Nothing to do, lets wait for a while, we will get woken up if anything new comes along
                    delay = idle_time

                # Initially we don't want to skip over any events that haven't had a chance to be registered yet, but now
                # we can loosen up a little
                idle_time = 60

                #
                # We are about to go to sleep, but we need to ensure we don't miss a DST transition or we will
                # sleep in and potentially miss an event that should happen earlier than expected due to the time change
                #

                next = self.now + timedelta(seconds=delay)

                self.logger.debug("next event=%s", next)

                if await self.is_dst() != await self.is_dst(next):
                    #
                    # Reset delay to wake up at the DST change so we can re-jig everything
                    #

                    delay = self.get_next_dst_offset(self.now, delay)
                    self.logger.debug(
                        "DST transition before next event: %s %s", await
                        self.is_dst(), await self.is_dst(next))

                self.logger.debug("Delay = %s seconds", delay)

                if delay > 0 and self.AD.timewarp > 0:
                    #
                    # Sleep until the next event
                    #
                    result = await self.sleep(delay / self.AD.timewarp)
                    self.logger.debug("result = %s", result)
                else:
                    # Not sleeping but lets be fair to the rest of AD
                    await asyncio.sleep(0)

            except Exception:
                self.logger.warning("-" * 60)
                self.logger.warning("Unexpected error in scheduler loop")
                self.logger.warning("-" * 60)
                self.logger.warning(traceback.format_exc())
                self.logger.warning("-" * 60)
                # Prevent spamming of the logs
                await self.sleep(1)

    async def sleep(self, delay):
        coro = asyncio.sleep(delay)
        self.sleep_task = asyncio.ensure_future(coro)
        try:
            await self.sleep_task
            self.sleep_task = None
            return False
        except asyncio.CancelledError:
            return True

    async def kick(self):
        while self.sleep_task is None:
            await asyncio.sleep(1)
        self.sleep_task.cancel()

    #
    # App API Calls
    #

    async def sun_up(self):
        return self.next_sunrise() > self.next_sunset()

    async def sun_down(self):
        return self.next_sunrise() < self.next_sunset()

    async def info_timer(self, handle, name):
        if self.timer_running(name, handle):
            callback = self.schedule[name][handle]
            return (
                self.make_naive(callback["timestamp"]),
                callback["interval"],
                self.sanitize_timer_kwargs(
                    self.AD.app_management.objects[name]["object"],
                    callback["kwargs"]),
            )
        else:
            self.logger.warning("Invalid timer handle given as: %s", handle)
            return None

    async def get_scheduler_entries(self):
        schedule = {}
        for name in self.schedule.keys():
            schedule[name] = {}
            for entry in sorted(
                    self.schedule[name].keys(),
                    key=lambda uuid_: self.schedule[name][uuid_]["timestamp"],
            ):
                schedule[name][str(entry)] = {}
                schedule[name][str(entry)]["timestamp"] = str(
                    self.AD.sched.make_naive(
                        self.schedule[name][entry]["timestamp"]))
                schedule[name][str(
                    entry)]["type"] = self.schedule[name][entry]["type"]
                schedule[name][str(
                    entry)]["name"] = self.schedule[name][entry]["name"]
                schedule[name][str(entry)]["basetime"] = str(
                    self.AD.sched.make_naive(
                        self.schedule[name][entry]["basetime"]))
                schedule[name][str(
                    entry)]["repeat"] = self.schedule[name][entry]["repeat"]
                if self.schedule[name][entry]["type"] == "next_rising":
                    schedule[name][str(
                        entry)]["interval"] = "sunrise:{}".format(
                            utils.format_seconds(
                                self.schedule[name][entry]["offset"]))
                elif self.schedule[name][entry]["type"] == "next_setting":
                    schedule[name][str(
                        entry)]["interval"] = "sunset:{}".format(
                            utils.format_seconds(
                                self.schedule[name][entry]["offset"]))
                elif self.schedule[name][entry]["repeat"] is True:
                    schedule[name][str(
                        entry)]["interval"] = utils.format_seconds(
                            self.schedule[name][entry]["interval"])
                else:
                    schedule[name][str(entry)]["interval"] = "None"

                schedule[name][str(
                    entry)]["offset"] = self.schedule[name][entry]["offset"]
                schedule[name][str(entry)]["kwargs"] = ""
                for kwarg in self.schedule[name][entry]["kwargs"]:
                    schedule[name][str(entry)]["kwargs"] = utils.get_kwargs(
                        self.schedule[name][entry]["kwargs"])
                schedule[name][str(entry)]["callback"] = self.schedule[name][
                    entry]["callback"].__name__
                schedule[name][str(entry)]["pin_thread"] = (
                    self.schedule[name][entry]["pin_thread"] if
                    self.schedule[name][entry]["pin_thread"] != -1 else "None")
                schedule[name][str(entry)]["pin_app"] = (
                    "True" if self.schedule[name][entry]["pin_app"] is True
                    else "False")

        # Order it

        ordered_schedule = OrderedDict(
            sorted(schedule.items(), key=lambda x: x[0]))

        return ordered_schedule

    async def is_dst(self, dt=None):
        if dt is None:
            return (await self.get_now()).astimezone(
                self.AD.tz).dst() != datetime.timedelta(0)
        else:
            return dt.astimezone(self.AD.tz).dst() != datetime.timedelta(0)

    async def get_now(self):
        if self.realtime is True:
            return pytz.utc.localize(datetime.datetime.utcnow())
        else:
            return self.now

    # Non async version of get_now(), required for logging time formatter - no locking but only used during time travel so should be OK ...
    def get_now_sync(self):
        if self.realtime is True:
            return pytz.utc.localize(datetime.datetime.utcnow())
        else:
            return self.now

    async def get_now_ts(self):
        return (await self.get_now()).timestamp()

    async def get_now_naive(self):
        return self.make_naive(await self.get_now())

    async def now_is_between(self, start_time_str, end_time_str, name=None):
        start_time = (await self._parse_time(start_time_str, name))["datetime"]
        end_time = (await self._parse_time(end_time_str, name))["datetime"]
        now = (await self.get_now()).astimezone(self.AD.tz)
        start_date = now.replace(hour=start_time.hour,
                                 minute=start_time.minute,
                                 second=start_time.second)
        end_date = now.replace(hour=end_time.hour,
                               minute=end_time.minute,
                               second=end_time.second)
        if end_date < start_date:
            # Spans midnight
            if now < start_date and now < end_date:
                now = now + datetime.timedelta(days=1)
            end_date = end_date + datetime.timedelta(days=1)
        return start_date <= now <= end_date

    async def sunset(self, aware):
        if aware is True:
            return self.next_sunset().astimezone(self.AD.tz)
        else:
            return self.make_naive(self.next_sunset().astimezone(self.AD.tz))

    async def sunrise(self, aware):
        if aware is True:
            return self.next_sunrise().astimezone(self.AD.tz)
        else:
            return self.make_naive(self.next_sunrise().astimezone(self.AD.tz))

    async def parse_time(self, time_str, name=None, aware=False):
        if aware is True:
            return (await self._parse_time(
                time_str, name))["datetime"].astimezone(self.AD.tz).time()
        else:
            return self.make_naive(
                (await self._parse_time(time_str, name))["datetime"]).time()

    async def parse_datetime(self, time_str, name=None, aware=False):
        if aware is True:
            return (await
                    self._parse_time(time_str,
                                     name))["datetime"].astimezone(self.AD.tz)
        else:
            return self.make_naive((await self._parse_time(time_str,
                                                           name))["datetime"])

    async def _parse_time(self, time_str, name=None):
        parsed_time = None
        sun = None
        offset = 0
        parts = re.search(r"^(\d+)-(\d+)-(\d+)\s+(\d+):(\d+):(\d+)$", time_str)
        if parts:
            this_time = datetime.datetime(
                int(parts.group(1)),
                int(parts.group(2)),
                int(parts.group(3)),
                int(parts.group(4)),
                int(parts.group(5)),
                int(parts.group(6)),
                0,
            )
            parsed_time = self.AD.tz.localize(this_time)
        else:
            parts = re.search(r"^(\d+):(\d+):(\d+)$", time_str)
            if parts:
                today = (await self.get_now()).astimezone(self.AD.tz)
                time = datetime.time(int(parts.group(1)), int(parts.group(2)),
                                     int(parts.group(3)), 0)
                parsed_time = today.replace(
                    hour=time.hour,
                    minute=time.minute,
                    second=time.second,
                    microsecond=0,
                )

            else:
                if time_str == "sunrise":
                    parsed_time = await self.sunrise(True)
                    sun = "sunrise"
                    offset = 0
                elif time_str == "sunset":
                    parsed_time = await self.sunset(True)
                    sun = "sunset"
                    offset = 0
                else:
                    parts = re.search(
                        r"^sunrise\s*([+-])\s*(\d+):(\d+):(\d+)$", time_str)
                    if parts:
                        sun = "sunrise"
                        if parts.group(1) == "+":
                            td = datetime.timedelta(
                                hours=int(parts.group(2)),
                                minutes=int(parts.group(3)),
                                seconds=int(parts.group(4)),
                            )
                            offset = td.total_seconds()
                            parsed_time = await self.sunrise(True) + td
                        else:
                            td = datetime.timedelta(
                                hours=int(parts.group(2)),
                                minutes=int(parts.group(3)),
                                seconds=int(parts.group(4)),
                            )
                            offset = td.total_seconds() * -1
                            parsed_time = await self.sunrise(True) - td
                    else:
                        parts = re.search(
                            r"^sunset\s*([+-])\s*(\d+):(\d+):(\d+)$", time_str)
                        if parts:
                            sun = "sunset"
                            if parts.group(1) == "+":
                                td = datetime.timedelta(
                                    hours=int(parts.group(2)),
                                    minutes=int(parts.group(3)),
                                    seconds=int(parts.group(4)),
                                )
                                offset = td.total_seconds()
                                parsed_time = await self.sunset(True) + td
                            else:
                                td = datetime.timedelta(
                                    hours=int(parts.group(2)),
                                    minutes=int(parts.group(3)),
                                    seconds=int(parts.group(4)),
                                )
                                offset = td.total_seconds() * -1
                                parsed_time = await self.sunset(True) - td
        if parsed_time is None:
            if name is not None:
                raise ValueError("%s: invalid time string: %s", name, time_str)
            else:
                raise ValueError("invalid time string: %s", time_str)
        return {"datetime": parsed_time, "sun": sun, "offset": offset}

    #
    # Diagnostics
    #

    async def dump_sun(self):
        self.diag.info("--------------------------------------------------")
        self.diag.info("Sun")
        self.diag.info("--------------------------------------------------")
        self.diag.info("Next Sunrise: %s", self.next_sunrise())
        self.diag.info("Next Sunset: %s", self.next_sunset())
        self.diag.info("--------------------------------------------------")

    async def dump_schedule(self):
        if self.schedule == {}:
            self.diag.info("Scheduler Table is empty")
        else:
            self.diag.info(
                "--------------------------------------------------")
            self.diag.info("Scheduler Table")
            self.diag.info(
                "--------------------------------------------------")
            for name in self.schedule.keys():
                self.diag.info("%s:", name)
                for entry in sorted(
                        self.schedule[name].keys(),
                        key=lambda uuid_: self.schedule[name][uuid_][
                            "timestamp"],
                ):
                    self.diag.info(
                        " Next Event Time: %s - data: %s",
                        self.make_naive(
                            self.schedule[name][entry]["timestamp"]),
                        self.schedule[name][entry],
                    )
            self.diag.info(
                "--------------------------------------------------")

    #
    # Utilities
    #
    @staticmethod
    def sanitize_timer_kwargs(app, kwargs):
        kwargs_copy = kwargs.copy()
        return utils._sanitize_kwargs(
            kwargs_copy,
            [
                "interval", "constrain_days", "constrain_input_boolean",
                "_pin_app", "_pin_thread", "__silent"
            ] + app.list_constraints(),
        )

    @staticmethod
    def myround(x, base=1, prec=10):
        if base == 0:
            return x
        else:
            return round(base * round(float(x) / base), prec)

    @staticmethod
    def my_dt_round(dt, base=1, prec=10):
        if base == 0:
            return dt
        else:
            ts = dt.timestamp()
            rounded = round(base * round(float(ts) / base), prec)
            result = datetime.datetime.utcfromtimestamp(rounded)
            aware_result = pytz.utc.localize(result)
            return aware_result

    def convert_naive(self, dt):
        # Is it naive?
        result = None
        if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
            # Localize with the configured timezone
            result = self.AD.tz.localize(dt)
        else:
            result = dt

        return result

    def make_naive(self, dt):
        local = dt.astimezone(self.AD.tz)
        return datetime.datetime(
            local.year,
            local.month,
            local.day,
            local.hour,
            local.minute,
            local.second,
            local.microsecond,
        )
Exemple #22
0
 def _update_sun_info(self):
     location = Location(('name', 'region', float(self._latitude),
                          float(self._longitude), 'GMT+0', 0))
     self.sun = location.sun()
Exemple #23
0
    def test_SetLongitudeString(self):
        loc = Location()
        loc.longitude = "24°28'S"

        assert loc.longitude == pytest.approx(-24.46666666666666)
Exemple #24
0
def run():
    defaults = {
        'log': "info"
    }

    # Parse any config file specification. We make this parser with add_help=False so
    # that it doesn't parse -h and print help.
    conf_parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        add_help=False
    )
    conf_parser.add_argument("--config", help="Specify config file", metavar='FILE')
    args, remaining_argv = conf_parser.parse_known_args()

    # Read configuration file and add it to the defaults hash.
    if args.config:
        config = ConfigParser()
        config.read(args.config)
        if "Defaults" in config:
            defaults.update(dict(config.items("Defaults")))
        else:
            sys.exit("Bad config file, missing Defaults section")

    # Parse rest of arguments
    parser = argparse.ArgumentParser(
        description=__doc__,
        parents=[conf_parser],
    )
    parser.set_defaults(**defaults)
    parser.add_argument("--gw-station-id", help="GoodWe station ID", metavar='ID')
    parser.add_argument("--gw-account", help="GoodWe account", metavar='ACCOUNT')
    parser.add_argument("--gw-password", help="GoodWe password", metavar='PASSWORD')
    parser.add_argument("--pvo-system-id", help="PVOutput system ID", metavar='ID')
    parser.add_argument("--pvo-api-key", help="PVOutput API key", metavar='KEY')
    parser.add_argument("--pvo-interval", help="PVOutput interval in minutes", type=int, choices=[5, 10, 15])
    parser.add_argument("--darksky-api-key", help="Dark Sky Weather API key")
    parser.add_argument("--openweather-api-key", help="Open Weather API key")
    parser.add_argument("--netatmo-username", help="Netatmo username")
    parser.add_argument("--netatmo-password", help="Netatmo password")
    parser.add_argument("--netatmo-client-id", help="Netatmo OAuth client id")
    parser.add_argument("--netatmo-client-secret", help="Netatmo OAuth client secret")
    parser.add_argument("--netatmo-device-id", help="Netatmo device id")
    parser.add_argument("--log", help="Set log level (default info)", choices=['debug', 'info', 'warning', 'critical'])
    parser.add_argument("--date", help="Copy all readings (max 14/90 days ago)", metavar='YYYY-MM-DD')
    parser.add_argument("--pv-voltage", help="Send pv voltage instead of grid voltage", action='store_true')
    parser.add_argument("--skip-offline", help="Skip uploads when inverter is offline", action='store_true')
    parser.add_argument("--city", help="Sets timezone and skip uploads from dusk till dawn")
    parser.add_argument('--csv', help="Append readings to a Excel compatible CSV file, DATE in the name will be replaced by the current date")
    parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
    args = parser.parse_args()

    # Configure the logging
    numeric_level = getattr(logging, args.log.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: %s' % loglevel)
    logging.basicConfig(format='%(levelname)-8s %(message)s', level=numeric_level)

    logging.debug("gw2pvo version " + __version__)

    if isinstance(args.skip_offline, str):
        args.skip_offline = args.skip_offline.lower() in ['true', 'yes', 'on', '1']

    if args.gw_station_id is None or args.gw_account is None or args.gw_password is None:
        sys.exit("Missing --gw-station-id, --gw-account and/or --gw-password")

    if args.city:
        city = Location(lookup(args.city, database()))
        os.environ['TZ'] = city.timezone
        time.tzset()
    else:
        city = None
    logging.debug("Timezone {}".format(datetime.now().astimezone().tzinfo))

    # Check if we want to copy old data
    if args.date:
        try:
            copy(args)
        except KeyboardInterrupt:
            sys.exit(1)
        except Exception as exp:
            logging.error(exp)
        sys.exit()

    startTime = datetime.now()

    while True:
        try:
            run_once(args, city)
        except KeyboardInterrupt:
            sys.exit(1)
        except Exception as exp:
            logging.error(exp)

        if args.pvo_interval is None:
            break

        interval = args.pvo_interval * 60
        time.sleep(interval - (datetime.now() - startTime).seconds % interval)
Exemple #25
0
 def test_SetBadLongitudeString(self):
     loc = Location()
     with pytest.raises(ValueError):
         loc.longitude = "wibble"
Exemple #26
0
 def test_TimezoneNameBad(self):
     """Test that an exception is raised if an invalid timezone is specified"""
     c = Location()
     with pytest.raises(ValueError):
         c.timezone = "bad/timezone"
Exemple #27
0
class FroniusToInflux:
    BACKOFF_INTERVAL = 0.1
    IGNORE_SUN_DOWN: bool
    BUCKET: str
    BUCKET_NAME: str
    DATA_COLLECTION_INTERVAL: int

    def __init__(
        self,
        client: InfluxDBClient,
        location_info: LocationInfo,
        endpoints: List[str],
        tz: Any,
        bucket_name: str,
        ignore_sundown: bool = False,
        data_collection_interval=60,
    ) -> None:
        self.client = client
        self.write_api = client.write_api(write_options=SYNCHRONOUS)
        self.location = Location(location_info)
        self.endpoints = endpoints
        self.tz = tz
        self.data: Dict[Any, Any] = {}
        self.BUCKET_NAME = bucket_name
        self.IGNORE_SUN_DOWN = ignore_sundown
        self.DATA_COLLECTION_INTERVAL = data_collection_interval

    def get_float_or_zero(self, value: str) -> float:
        internal_data: Dict[Any, Any] = {}
        try:
            internal_data = self.data["Body"]["Data"]
        except KeyError:
            raise WrongFroniusData("Response structure is not healthy.")
        return float(internal_data.get(value, {}).get("Value", 0))

    def translate_response(self) -> List[Dict]:
        collection = self.data["Head"]["RequestArguments"]["DataCollection"]
        timestamp = self.data["Head"]["Timestamp"]
        if collection == "CommonInverterData":
            error_code = self.data["Body"]["Data"]["DeviceStatus"]["ErrorCode"]
            return [
                {
                    "measurement": "DeviceStatus",
                    "time": timestamp,
                    "fields": {
                        "ErrorCode":
                        error_code,
                        "ErrorCodeMessage":
                        error_codes[error_code],
                        "LEDColor":
                        self.data["Body"]["Data"]["DeviceStatus"]["LEDColor"],
                        "LEDState":
                        self.data["Body"]["Data"]["DeviceStatus"]["LEDState"],
                        "MgmtTimerRemainingTime":
                        self.data["Body"]["Data"]["DeviceStatus"]
                        ["MgmtTimerRemainingTime"],
                        "StateToReset":
                        self.data["Body"]["Data"]["DeviceStatus"]
                        ["StateToReset"],
                        "StatusCode":
                        self.data["Body"]["Data"]["DeviceStatus"]
                        ["StatusCode"],
                    },
                },
                {
                    "measurement": collection,
                    "time": timestamp,
                    "fields": {
                        "FAC": self.get_float_or_zero("FAC"),
                        "IAC": self.get_float_or_zero("IAC"),
                        "IDC": self.get_float_or_zero("IDC"),
                        "PAC": self.get_float_or_zero("PAC"),
                        "UAC": self.get_float_or_zero("UAC"),
                        "UDC": self.get_float_or_zero("UDC"),
                        "DAY_ENERGY": self.get_float_or_zero("DAY_ENERGY"),
                        "YEAR_ENERGY": self.get_float_or_zero("YEAR_ENERGY"),
                        "TOTAL_ENERGY": self.get_float_or_zero("TOTAL_ENERGY"),
                    },
                },
            ]
        elif collection == "3PInverterData":
            return [{
                "measurement": collection,
                "time": timestamp,
                "fields": {
                    "IAC_L1": self.get_float_or_zero("IAC_L1"),
                    "IAC_L2": self.get_float_or_zero("IAC_L2"),
                    "IAC_L3": self.get_float_or_zero("IAC_L3"),
                    "UAC_L1": self.get_float_or_zero("UAC_L1"),
                    "UAC_L2": self.get_float_or_zero("UAC_L2"),
                    "UAC_L3": self.get_float_or_zero("UAC_L3"),
                },
            }]
        elif collection == "MinMaxInverterData":
            return [{
                "measurement": collection,
                "time": timestamp,
                "fields": {
                    "DAY_PMAX": self.get_float_or_zero("DAY_PMAX"),
                    "DAY_UACMAX": self.get_float_or_zero("DAY_UACMAX"),
                    "DAY_UDCMAX": self.get_float_or_zero("DAY_UDCMAX"),
                    "YEAR_PMAX": self.get_float_or_zero("YEAR_PMAX"),
                    "YEAR_UACMAX": self.get_float_or_zero("YEAR_UACMAX"),
                    "YEAR_UDCMAX": self.get_float_or_zero("YEAR_UDCMAX"),
                    "TOTAL_PMAX": self.get_float_or_zero("TOTAL_PMAX"),
                    "TOTAL_UACMAX": self.get_float_or_zero("TOTAL_UACMAX"),
                    "TOTAL_UDCMAX": self.get_float_or_zero("TOTAL_UDCMAX"),
                },
            }]
        else:
            raise DataCollectionError("Unknown data collection type.")

    def sun_is_shining(self) -> None:
        sun = self.location.sun()
        if (not self.IGNORE_SUN_DOWN and not sun["sunrise"] <
                datetime.datetime.now(tz=self.tz) < sun["sunset"]):
            raise SunIsDown
        return None

    def run(self) -> None:
        try:
            while True:
                try:
                    self.sun_is_shining()
                    collected_datas = []
                    for url in self.endpoints:
                        print(f"getting {url}...")
                        response = get(url)
                        response.raise_for_status()
                        self.data = response.json()
                        collected_datas.extend(self.translate_response())
                        sleep(self.BACKOFF_INTERVAL)
                    self.write_api.write(self.BUCKET_NAME,
                                         record=collected_datas)
                    print("Data written")
                    sleep(self.DATA_COLLECTION_INTERVAL)
                except SunIsDown:
                    print("Waiting for sunrise")
                    sleep(300)
                except ConnectionError:
                    print("Waiting for connection...")
                    sleep(60)
                except KeyError:
                    raise WrongFroniusData("Response structure is not healthy")

        except KeyboardInterrupt:
            print("Finishing. Goodbye!")
Exemple #28
0
#Random seed
seed = 42

#input files:
weather_irradiation = 'input/weather/solarirradiation_twenthe.csv'
weather_timebaseDataset = 3600  #in seconds per interval

#Simulation:
#number of days to simulate and skipping of initial days. Simulation starts at Sunday January 1.
numDays = 365  # number of days
startDay = 0  # Initial day

#Select the geographic location. Refer to the Astral plugin to see available locations (or give a lon+lat)
# Use e.g. https://www.latlong.net/

location = Location()
location.solar_depression = 'civil'
location.latitude = 52.239095
location.longitude = 6.857018
location.timezone = 'Europe/Amsterdam'
location.elevation = 0

#Select the devices in the neighbourhood

#Devices
#Scale overall consumption:
consumptionFactor = 1.0  #consumption was a bit too high

# Penetration of emerging technology in percentages
# all values must be between 0-100
# These indicate what percentage of the houses has a certain device
Exemple #29
0
 def test_TimezoneLookup(self):
     """Test that tz refers to a timezone object"""
     c = Location()
     assert c.tz == pytz.timezone("Europe/London")
     c.timezone = "Europe/Stockholm"
     assert c.tz == pytz.timezone("Europe/Stockholm")
Exemple #30
0
 def test_Region(self):
     """Test the default region and that the region is changeable"""
     c = Location()
     assert c.region == "England"
     c.region = "Australia"
     assert c.region == "Australia"