class WeatherSensorType(db.Model): """ This model is now considered legacy. See GenericAssetType. """ name = db.Column(db.String(80), primary_key=True) display_name = db.Column(db.String(80), default="", unique=True) daily_seasonality = True weekly_seasonality = False yearly_seasonality = True def __init__(self, **kwargs): generic_asset_type = GenericAssetType( name=kwargs["name"], description=kwargs.get("hover_label", None) ) db.session.add(generic_asset_type) super(WeatherSensorType, self).__init__(**kwargs) if "display_name" not in kwargs: self.display_name = humanize(self.name) def __repr__(self): return "<WeatherSensorType %r>" % self.name
class MarketType(db.Model): """ Describing market types for our purposes. This model is now considered legacy. See GenericAssetType. """ name = db.Column(db.String(80), primary_key=True) display_name = db.Column(db.String(80), default="", unique=True) daily_seasonality = db.Column(db.Boolean(), nullable=False, default=False) weekly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) yearly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) def __init__(self, **kwargs): kwargs["name"] = kwargs["name"].replace(" ", "_").lower() if "display_name" not in kwargs: kwargs["display_name"] = humanize(kwargs["name"]) super(MarketType, self).__init__(**kwargs) generic_asset_type = GenericAssetType(name=kwargs["name"], description=kwargs.get( "hover_label", None)) db.session.add(generic_asset_type) @property def preconditions(self) -> Dict[str, bool]: """Assumptions about the time series data set, such as normality and stationarity For now, this is usable input for Prophet (see init), but it might evolve or go away.""" return dict( daily_seasonality=self.daily_seasonality, weekly_seasonality=self.weekly_seasonality, yearly_seasonality=self.yearly_seasonality, ) def __repr__(self): return "<MarketType %r>" % self.name
class LatestTaskRun(db.Model): """ " Log the (latest) running of a task. This is intended to be used for live monitoring. For a full analysis, there are log files. """ name = db.Column(db.String(80), primary_key=True) datetime = db.Column(db.DateTime(timezone=True), default=datetime.utcnow().replace(tzinfo=pytz.utc)) status = db.Column(db.Boolean, default=True) def __repr__(self): return "<TaskRun [%s] at %s (status: %s)>" % ( self.name, self.datetime, { True: "ok", False: "err" }[self.status], ) @staticmethod def record_run(task_name: str, status: bool): """ Record the latest task run (overwriting previous ones). If the row is not yet in the table, create it first. Does not commit. """ task_run = LatestTaskRun.query.filter( LatestTaskRun.name == task_name).one_or_none() if task_run is None: task_run = LatestTaskRun(name=task_name) db.session.add(task_run) task_run.datetime = datetime.utcnow().replace(tzinfo=pytz.utc) task_run.status = status
class WeatherSensor(db.Model, tb.SensorDBMixin): """ A weather sensor has a location on Earth and measures weather values of a certain weather sensor type, such as temperature, wind speed and irradiance. This model is now considered legacy. See GenericAsset and Sensor. """ id = db.Column( db.Integer, db.ForeignKey("sensor.id"), primary_key=True, autoincrement=True ) name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=False) weather_sensor_type_name = db.Column( db.String(80), db.ForeignKey("weather_sensor_type.name"), nullable=False ) # latitude is the North/South coordinate latitude = db.Column(db.Float, nullable=False) # longitude is the East/West coordinate longitude = db.Column(db.Float, nullable=False) # only one sensor of any type is needed at one location __table_args__ = ( UniqueConstraint( "weather_sensor_type_name", "latitude", "longitude", name="weather_sensor_type_name_latitude_longitude_key", ), ) def __init__(self, **kwargs): super(WeatherSensor, self).__init__(**kwargs) # Create a new Sensor with unique id across assets, markets and weather sensors if "id" not in kwargs: weather_sensor_type = get_old_model_type( kwargs, WeatherSensorType, "weather_sensor_type_name", "sensor_type", # NB not "weather_sensor_type" (slight inconsistency in this old sensor class) ) generic_asset_kwargs = { **kwargs, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[], old_sensor_attributes=[ "display_name", ], old_sensor_type=weather_sensor_type, ), } new_generic_asset = create_generic_asset( "weather_sensor", **generic_asset_kwargs ) new_sensor = Sensor( name=kwargs["name"], generic_asset=new_generic_asset, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[ "daily_seasonality", "weekly_seasonality", "yearly_seasonality", ], old_sensor_attributes=[ "display_name", ], old_sensor_type=weather_sensor_type, ), ) db.session.add(new_sensor) db.session.flush() # generates the pkey for new_sensor new_sensor_id = new_sensor.id else: # The UI may initialize WeatherSensor objects from API form data with a known id new_sensor_id = kwargs["id"] self.id = new_sensor_id # Copy over additional columns from (newly created) WeatherSensor to (newly created) Sensor if "id" not in kwargs: db.session.add(self) db.session.flush() # make sure to generate each column for the old sensor new_sensor.unit = self.unit new_sensor.event_resolution = self.event_resolution new_sensor.knowledge_horizon_fnc = self.knowledge_horizon_fnc new_sensor.knowledge_horizon_par = self.knowledge_horizon_par @property def entity_address_fm0(self) -> str: """Entity address under the fm0 scheme for entity addresses.""" return build_entity_address( dict( weather_sensor_type_name=self.weather_sensor_type_name, latitude=self.latitude, longitude=self.longitude, ), "weather_sensor", fm_scheme="fm0", ) @property def entity_address(self) -> str: """Entity address under the latest fm scheme for entity addresses.""" return build_entity_address( dict(sensor_id=self.id), "sensor", ) @property def corresponding_sensor(self) -> Sensor: return db.session.query(Sensor).get(self.id) @property def generic_asset(self) -> GenericAsset: return db.session.query(GenericAsset).get(self.corresponding_sensor.id) def get_attribute(self, attribute: str): """Looks for the attribute on the corresponding Sensor. This should be used by all code to read these attributes, over accessing them directly on this class, as this table is in the process to be replaced by the Sensor table. """ return self.corresponding_sensor.get_attribute(attribute) @property def weather_unit(self) -> float: """Return the 'unit' property of the generic asset, just with a more insightful name.""" return self.unit @property def location(self) -> Tuple[float, float]: return self.latitude, self.longitude @hybrid_method def great_circle_distance(self, **kwargs): """Query great circle distance (in km). Can be called with an object that has latitude and longitude properties, for example: great_circle_distance(object=asset) Can also be called with latitude and longitude parameters, for example: great_circle_distance(latitude=32, longitude=54) great_circle_distance(lat=32, lng=54) """ other_location = geo_utils.parse_lat_lng(kwargs) if None in other_location: return None return geo_utils.earth_distance(self.location, other_location) @great_circle_distance.expression def great_circle_distance(self, **kwargs): """Query great circle distance (unclear if in km or in miles). Can be called with an object that has latitude and longitude properties, for example: great_circle_distance(object=asset) Can also be called with latitude and longitude parameters, for example: great_circle_distance(latitude=32, longitude=54) great_circle_distance(lat=32, lng=54) """ other_location = geo_utils.parse_lat_lng(kwargs) if None in other_location: return None return func.earth_distance( func.ll_to_earth(self.latitude, self.longitude), func.ll_to_earth(*other_location), ) sensor_type = db.relationship( "WeatherSensorType", backref=db.backref("sensors", lazy=True) ) def __repr__(self): return "<WeatherSensor %s:%r (%r), res.:%s>" % ( self.id, self.name, self.weather_sensor_type_name, self.event_resolution, ) def to_dict(self) -> Dict[str, str]: return dict(name=self.name, sensor_type=self.weather_sensor_type_name)
class Market(db.Model, tb.SensorDBMixin): """ Each market is a pricing service. This model is now considered legacy. See GenericAsset and Sensor. """ id = db.Column(db.Integer, db.ForeignKey("sensor.id"), primary_key=True, autoincrement=True) name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=True) market_type_name = db.Column(db.String(80), db.ForeignKey("market_type.name"), nullable=False) def __init__(self, **kwargs): # Set default knowledge horizon function for an economic sensor if "knowledge_horizon_fnc" not in kwargs: kwargs[ "knowledge_horizon_fnc"] = knowledge_horizons.ex_ante.__name__ if "knowledge_horizon_par" not in kwargs: kwargs["knowledge_horizon_par"] = { knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H" } kwargs["name"] = kwargs["name"].replace(" ", "_").lower() if "display_name" not in kwargs: kwargs["display_name"] = humanize(kwargs["name"]) super(Market, self).__init__(**kwargs) # Create a new Sensor with unique id across assets, markets and weather sensors if "id" not in kwargs: market_type = get_old_model_type(kwargs, MarketType, "market_type_name", "market_type") generic_asset_kwargs = { **kwargs, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[], old_sensor_attributes=[ "display_name", ], old_sensor_type=market_type, ), } new_generic_asset = create_generic_asset("market", **generic_asset_kwargs) new_sensor = Sensor( name=kwargs["name"], generic_asset=new_generic_asset, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[ "daily_seasonality", "weekly_seasonality", "yearly_seasonality", ], old_sensor_attributes=[ "display_name", ], old_sensor_type=market_type, ), ) db.session.add(new_sensor) db.session.flush() # generates the pkey for new_sensor new_sensor_id = new_sensor.id else: # The UI may initialize Market objects from API form data with a known id new_sensor_id = kwargs["id"] self.id = new_sensor_id # Copy over additional columns from (newly created) Market to (newly created) Sensor if "id" not in kwargs: db.session.add(self) db.session.flush( ) # make sure to generate each column for the old sensor new_sensor.unit = self.unit new_sensor.event_resolution = self.event_resolution new_sensor.knowledge_horizon_fnc = self.knowledge_horizon_fnc new_sensor.knowledge_horizon_par = self.knowledge_horizon_par @property def entity_address_fm0(self) -> str: """Entity address under the fm0 scheme for entity addresses.""" return build_entity_address(dict(market_name=self.name), "market", fm_scheme="fm0") @property def entity_address(self) -> str: """Entity address under the latest fm scheme for entity addresses.""" return build_entity_address(dict(sensor_id=self.id), "sensor") @property def corresponding_sensor(self) -> Sensor: return db.session.query(Sensor).get(self.id) @property def generic_asset(self) -> GenericAsset: return db.session.query(GenericAsset).get(self.corresponding_sensor.id) def get_attribute(self, attribute: str): """Looks for the attribute on the corresponding Sensor. This should be used by all code to read these attributes, over accessing them directly on this class, as this table is in the process to be replaced by the Sensor table. """ return self.corresponding_sensor.get_attribute(attribute) @property def price_unit(self) -> str: """Return the 'unit' property of the generic asset, just with a more insightful name.""" return self.unit market_type = db.relationship("MarketType", backref=db.backref("markets", lazy=True)) def __repr__(self): return "<Market %s:%r (%r) res.: %s>" % ( self.id, self.name, self.market_type_name, self.event_resolution, ) def to_dict(self) -> Dict[str, str]: return dict(name=self.name, market_type=self.market_type.name)
class DataSource(db.Model, tb.BeliefSourceDBMixin): """Each data source is a data-providing entity.""" __tablename__ = "data_source" __table_args__ = (db.UniqueConstraint("name", "user_id", "model", "version"), ) # The type of data source (e.g. user, forecasting script or scheduling script) type = db.Column(db.String(80), default="") # The id of the user source (can link e.g. to fm_user table) user_id = db.Column(db.Integer, db.ForeignKey("fm_user.id"), nullable=True, unique=True) user = db.relationship("User", backref=db.backref("data_source", lazy=True)) # The model and version of a script source model = db.Column(db.String(80), nullable=True) version = db.Column( db.String(17), # length supports up to version 999.999.999dev999 nullable=True, ) def __init__( self, name: Optional[str] = None, type: Optional[str] = None, user: Optional[User] = None, **kwargs, ): if user is not None: name = user.username type = "user" self.user = user elif user is None and type == "user": raise TypeError( "A data source cannot have type 'user' but no user set.") self.type = type tb.BeliefSourceDBMixin.__init__(self, name=name) db.Model.__init__(self, **kwargs) @property def label(self): """Human-readable label (preferably not starting with a capital letter so it can be used in a sentence).""" if self.type == "user": return f"data entered by user {self.user.username}" # todo: give users a display name elif self.type == "forecasting script": return f"forecast by {self.name}" # todo: give DataSource an optional db column to persist versioned models separately to the name of the data source? elif self.type == "scheduling script": return f"schedule by {self.name}" elif self.type == "crawling script": return f"data retrieved from {self.name}" elif self.type in ("demo script", "CLI script"): return f"demo data entered by {self.name}" else: return f"data from {self.name}" @property def description(self): """Extended description For example: >>> DataSource("Seita", type="forecasting script", model="naive", version="1.2").description <<< "Seita's naive model v1.2.0" """ descr = self.name if self.model: descr += f"'s {self.model} model" if self.version: descr += f" v{self.version}" return descr def __repr__(self): return "<Data source %r (%s)>" % (self.id, self.description) def __str__(self): return self.description
class Annotation(db.Model): """An annotation is a nominal value that applies to a specific time or time span. Examples of annotation types: - user annotation: annotation.type == "label" and annotation.source.type == "user" - unresolved alert: annotation.type == "alert" - resolved alert: annotation.type == "label" and annotation.source.type == "alerting script" - organisation holiday: annotation.type == "holiday" and annotation.source.type == "user" - public holiday: annotation.type == "holiday" and annotation.source.name == "workalendar" """ id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) start = db.Column(db.DateTime(timezone=True), nullable=False) end = db.Column(db.DateTime(timezone=True), nullable=False) belief_time = db.Column(db.DateTime(timezone=True), nullable=True) source_id = db.Column(db.Integer, db.ForeignKey("data_source.id")) source = db.relationship( "DataSource", foreign_keys=[source_id], backref=db.backref("annotations", lazy=True), ) type = db.Column( db.Enum("alert", "holiday", "label", "feedback", name="annotation_type")) content = db.Column(db.String(1024), nullable=False) __table_args__ = (db.UniqueConstraint( "content", "start", "belief_time", "source_id", "type", name="annotation_content_key", ), ) @property def duration(self) -> timedelta: return self.end - self.start @classmethod def add( cls, df: pd.DataFrame, annotation_type: str, expunge_session: bool = False, allow_overwrite: bool = False, bulk_save_objects: bool = False, commit_transaction: bool = False, ) -> List["Annotation"]: """Add a data frame describing annotations to the database and return the Annotation objects. :param df: Data frame describing annotations. Expects the following columns (or multi-index levels): - start - end or duration - content - belief_time - source :param annotation_type: One of the possible Enum values for annotation.type :param expunge_session: if True, all non-flushed instances are removed from the session before adding annotations. Expunging can resolve problems you might encounter with states of objects in your session. When using this option, you might want to flush newly-created objects which are not annotations (e.g. a sensor or data source object). :param allow_overwrite: if True, new objects are merged if False, objects are added to the session or bulk saved :param bulk_save_objects: if True, objects are bulk saved with session.bulk_save_objects(), which is quite fast but has several caveats, see: https://docs.sqlalchemy.org/orm/persistence_techniques.html#bulk-operations-caveats if False, objects are added to the session with session.add_all() :param commit_transaction: if True, the session is committed if False, you can still add other data to the session and commit it all within an atomic transaction """ df = df.reset_index() starts = df["start"] if "end" in df.columns: ends = df["end"] elif "start" in df.columns and "duration" in df.columns: ends = df["start"] + df["duration"] else: raise ValueError( "Missing 'end' column cannot be derived from columns 'start' and 'duration'." ) values = df["content"] belief_times = df["belief_time"] sources = df["source"] annotations = [ cls( content=row[0], start=row[1], end=row[2], belief_time=row[3], source=row[4], type=annotation_type, ) for row in zip(values, starts, ends, belief_times, sources) ] # Deal with the database session if expunge_session: db.session.expunge_all() if not allow_overwrite: if bulk_save_objects: db.session.bulk_save_objects(annotations) else: db.session.add_all(annotations) else: for annotation in annotations: db.session.merge(annotation) if commit_transaction: db.session.commit() return annotations def __repr__(self) -> str: return f"<Annotation {self.id}: {self.content} ({self.type}), start: {self.start} end: {self.end}, source: {self.source}>"
class Asset(db.Model, tb.SensorDBMixin): """ Each asset is an energy- consuming or producing hardware. This model is now considered legacy. See GenericAsset and Sensor. """ id = db.Column( db.Integer, db.ForeignKey("sensor.id", ondelete="CASCADE"), primary_key=True, autoincrement=True, ) # The name name = db.Column(db.String(80), default="", unique=True) # The name we want to see (don't unnecessarily capitalize, so it can be used in a sentence) display_name = db.Column(db.String(80), default="", unique=True) # The name of the assorted AssetType asset_type_name = db.Column( db.String(80), db.ForeignKey("asset_type.name"), nullable=False ) # How many MW at peak usage capacity_in_mw = db.Column(db.Float, nullable=False) # State of charge in MWh and its datetime and udi event min_soc_in_mwh = db.Column(db.Float, nullable=True) max_soc_in_mwh = db.Column(db.Float, nullable=True) soc_in_mwh = db.Column(db.Float, nullable=True) soc_datetime = db.Column(db.DateTime(timezone=True), nullable=True) soc_udi_event_id = db.Column(db.Integer, nullable=True) # latitude is the North/South coordinate latitude = db.Column(db.Float, nullable=False) # longitude is the East/West coordinate longitude = db.Column(db.Float, nullable=False) # owner owner_id = db.Column(db.Integer, db.ForeignKey("fm_user.id", ondelete="CASCADE")) # market market_id = db.Column(db.Integer, db.ForeignKey("market.id"), nullable=True) def __init__(self, **kwargs): if "unit" not in kwargs: kwargs["unit"] = "MW" # current default super(Asset, self).__init__(**kwargs) # Create a new Sensor with unique id across assets, markets and weather sensors # Also keep track of ownership by creating a GenericAsset and assigning the new Sensor to it. if "id" not in kwargs: asset_type = get_old_model_type( kwargs, AssetType, "asset_type_name", "asset_type" ) # Set up generic asset generic_asset_kwargs = { **kwargs, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[ "can_curtail", "can_shift", ], old_sensor_attributes=[ "display_name", "min_soc_in_mwh", "max_soc_in_mwh", "soc_in_mwh", "soc_datetime", "soc_udi_event_id", ], old_sensor_type=asset_type, ), } if "owner_id" in kwargs: owner = User.query.get(kwargs["owner_id"]) if owner: generic_asset_kwargs.update(account_id=owner.account_id) new_generic_asset = create_generic_asset("asset", **generic_asset_kwargs) # Set up sensor new_sensor = Sensor( name=kwargs["name"], generic_asset=new_generic_asset, **copy_old_sensor_attributes( self, old_sensor_type_attributes=[ "is_consumer", "is_producer", "daily_seasonality", "weekly_seasonality", "yearly_seasonality", "weather_correlations", ], old_sensor_attributes=[ "display_name", "capacity_in_mw", "market_id", ], old_sensor_type=asset_type, ), ) db.session.add(new_sensor) db.session.flush() # generates the pkey for new_sensor sensor_id = new_sensor.id else: # The UI may initialize Asset objects from API form data with a known id sensor_id = kwargs["id"] self.id = sensor_id if self.unit != "MW": raise Exception("FlexMeasures only supports MW as unit for now.") self.name = self.name.replace(" (MW)", "") if "display_name" not in kwargs: self.display_name = humanize(self.name) # Copy over additional columns from (newly created) Asset to (newly created) Sensor if "id" not in kwargs: db.session.add(self) db.session.flush() # make sure to generate each column for the old sensor new_sensor.unit = self.unit new_sensor.event_resolution = self.event_resolution new_sensor.knowledge_horizon_fnc = self.knowledge_horizon_fnc new_sensor.knowledge_horizon_par = self.knowledge_horizon_par asset_type = db.relationship("AssetType", backref=db.backref("assets", lazy=True)) owner = db.relationship( "User", backref=db.backref( "assets", lazy=True, cascade="all, delete-orphan", passive_deletes=True ), ) market = db.relationship("Market", backref=db.backref("assets", lazy=True)) def latest_state(self, event_ends_before: Optional[datetime] = None) -> "Power": """Search the most recent event for this sensor, optionally before some datetime.""" # todo: replace with Sensor.latest_state power_query = ( Power.query.filter(Power.sensor_id == self.id) .filter(Power.horizon <= timedelta(hours=0)) .order_by(Power.datetime.desc()) ) if event_ends_before is not None: power_query = power_query.filter( Power.datetime + self.event_resolution <= event_ends_before ) return power_query.first() @property def corresponding_sensor(self) -> Sensor: return db.session.query(Sensor).get(self.id) @property def generic_asset(self) -> GenericAsset: return db.session.query(GenericAsset).get(self.corresponding_sensor.id) def get_attribute(self, attribute: str): """Looks for the attribute on the corresponding Sensor. This should be used by all code to read these attributes, over accessing them directly on this class, as this table is in the process to be replaced by the Sensor table. """ return self.corresponding_sensor.get_attribute(attribute) @property def power_unit(self) -> float: """Return the 'unit' property of the generic asset, just with a more insightful name.""" return self.unit @property def entity_address_fm0(self) -> str: """Entity address under the fm0 scheme for entity addresses.""" return build_entity_address( dict(owner_id=self.owner_id, asset_id=self.id), "connection", fm_scheme="fm0", ) @property def entity_address(self) -> str: """Entity address under the latest fm scheme for entity addresses.""" return build_entity_address(dict(sensor_id=self.id), "sensor") @property def location(self) -> Tuple[float, float]: return self.latitude, self.longitude def capacity_factor_in_percent_for(self, load_in_mw) -> int: if self.capacity_in_mw == 0: return 0 return min(round((load_in_mw / self.capacity_in_mw) * 100, 2), 100) @property def is_pure_consumer(self) -> bool: """Return True if this asset is consuming but not producing.""" return self.asset_type.is_consumer and not self.asset_type.is_producer @property def is_pure_producer(self) -> bool: """Return True if this asset is producing but not consuming.""" return self.asset_type.is_producer and not self.asset_type.is_consumer def to_dict(self) -> Dict[str, Union[str, float]]: return dict( name=self.name, display_name=self.display_name, asset_type_name=self.asset_type_name, latitude=self.latitude, longitude=self.longitude, capacity_in_mw=self.capacity_in_mw, ) def __repr__(self): return "<Asset %s:%r (%s), res.: %s on market %s>" % ( self.id, self.name, self.asset_type_name, self.event_resolution, self.market, )
class AssetType(db.Model): """ Describing asset types for our purposes This model is now considered legacy. See GenericAssetType. """ name = db.Column(db.String(80), primary_key=True) # The name we want to see (don't unnecessarily capitalize, so it can be used in a sentence) display_name = db.Column(db.String(80), default="", unique=True) # The explanatory hovel label (don't unnecessarily capitalize, so it can be used in a sentence) hover_label = db.Column(db.String(80), nullable=True, unique=False) is_consumer = db.Column(db.Boolean(), nullable=False, default=False) is_producer = db.Column(db.Boolean(), nullable=False, default=False) can_curtail = db.Column(db.Boolean(), nullable=False, default=False, index=True) can_shift = db.Column(db.Boolean(), nullable=False, default=False, index=True) daily_seasonality = db.Column(db.Boolean(), nullable=False, default=False) weekly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) yearly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) def __init__(self, **kwargs): generic_asset_type = GenericAssetType.query.filter_by( name=kwargs["name"] ).one_or_none() if not generic_asset_type: generic_asset_type = GenericAssetType( name=kwargs["name"], description=kwargs.get("hover_label", None) ) db.session.add(generic_asset_type) super(AssetType, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: self.display_name = humanize(self.name) @property def plural_name(self) -> str: return pluralize(self.display_name) @property def preconditions(self) -> Dict[str, bool]: """Assumptions about the time series data set, such as normality and stationarity For now, this is usable input for Prophet (see init), but it might evolve or go away.""" return dict( daily_seasonality=self.daily_seasonality, weekly_seasonality=self.weekly_seasonality, yearly_seasonality=self.yearly_seasonality, ) @property def weather_correlations(self) -> List[str]: """Known correlations of weather sensor type and asset type.""" correlations = [] if self.name == "solar": correlations.append("irradiance") if self.name == "wind": correlations.append("wind speed") if self.name in ( "one-way_evse", "two-way_evse", "battery", "building", ): correlations.append("temperature") return correlations def __repr__(self): return "<AssetType %r>" % self.name