예제 #1
0
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
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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)
예제 #5
0
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)
예제 #6
0
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
예제 #7
0
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}>"
예제 #8
0
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,
        )
예제 #9
0
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