Repository: FlexMeasures/flexmeasures (language: python)

Issue:
Move meta data from asset/market/weather classes and types to generic asset (types)
Most of this metadata doesn't required dedicated table columns, and can be moved to a new GenericAsset column reserved for attributes in JSON format.
Move meta data from asset/market/weather classes and types to generic asset (types)
Most of this metadata doesn't required dedicated table columns, and can be moved to a new GenericAsset column reserved for attributes in JSON format.


Discussion / hints:
Branch [issue-239-Move_meta_data_from_asset/market/weather_classes_and_types_to_generic_asset_types](https://github.com/SeitaBV/flexmeasures/tree/issue-239-Move_meta_data_from_asset/market/weather_classes_and_types_to_generic_asset_types) created!
Branch [issue-239-Move_meta_data_from_asset/market/weather_classes_and_types_to_generic_asset_types](https://github.com/SeitaBV/flexmeasures/tree/issue-239-Move_meta_data_from_asset/market/weather_classes_and_types_to_generic_asset_types) created!

Fix patch:
diff --git a/flexmeasures/api/v1/implementations.py b/flexmeasures/api/v1/implementations.py
index 9203161b3..51c0df6cb 100644
--- a/flexmeasures/api/v1/implementations.py
+++ b/flexmeasures/api/v1/implementations.py
@@ -201,7 +201,7 @@ def collect_connection_and_value_groups(
         # Get the power values
         # TODO: fill NaN for non-existing values
         power_bdf_dict: Dict[str, tb.BeliefsDataFrame] = Power.collect(
-            generic_asset_names=asset_names,
+            old_sensor_names=asset_names,
             query_window=(start, end),
             resolution=resolution,
             belief_horizon_window=belief_horizon_window,
diff --git a/flexmeasures/api/v1_2/implementations.py b/flexmeasures/api/v1_2/implementations.py
index ac674f935..85cc03bb9 100644
--- a/flexmeasures/api/v1_2/implementations.py
+++ b/flexmeasures/api/v1_2/implementations.py
@@ -34,7 +34,10 @@
 from flexmeasures.data.config import db
 from flexmeasures.data.models.assets import Asset
 from flexmeasures.data.models.planning.battery import schedule_battery
-from flexmeasures.data.models.planning.exceptions import UnknownPricesException
+from flexmeasures.data.models.planning.exceptions import (
+    UnknownMarketException,
+    UnknownPricesException,
+)
 from flexmeasures.data.services.resources import has_assets, can_access_asset
 
 
@@ -87,16 +90,10 @@ def get_device_message_response(generic_asset_name_groups, duration):
             start = asset.soc_datetime
             resolution = asset.event_resolution
 
-            # Look for the Market object
-            market = asset.market
-            if market is None:
-                return invalid_market()
-
             # Schedule the asset
             try:
                 schedule = schedule_battery(
-                    asset,
-                    market,
+                    asset.corresponding_sensor,
                     start,
                     start + planning_horizon,
                     resolution,
@@ -105,6 +102,8 @@ def get_device_message_response(generic_asset_name_groups, duration):
                 )
             except UnknownPricesException:
                 return unknown_prices()
+            except UnknownMarketException:
+                return invalid_market()
             else:
                 # Update the planning window
                 start = schedule.index[0]
diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py
index 01501fa6b..649c2e9b5 100755
--- a/flexmeasures/cli/data_add.py
+++ b/flexmeasures/cli/data_add.py
@@ -619,7 +619,7 @@ def create_forecasts(
             # Note that this time period refers to the period of events we are forecasting, while in create_forecasting_jobs
             # the time period refers to the period of belief_times, therefore we are subtracting the horizon.
             create_forecasting_jobs(
-                asset_id=asset_id,
+                old_sensor_id=asset_id,
                 timed_value_type=value_type,
                 horizons=[horizon],
                 start_of_roll=forecast_start - horizon,
@@ -634,8 +634,8 @@ def create_forecasts(
             forecast_start=forecast_start,
             forecast_end=forecast_end,
             event_resolution=event_resolution,
-            generic_asset_type=asset_type,
-            generic_asset_id=asset_id,
+            old_sensor_class_name=asset_type,
+            old_sensor_id=asset_id,
         )
 
 
diff --git a/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py b/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py
new file mode 100644
index 000000000..d8128ff3e
--- /dev/null
+++ b/flexmeasures/data/migrations/versions/6cf5b241b85f_copy_attributes_from_old_data_models_to_GenericAsset.py
@@ -0,0 +1,366 @@
+"""Copy attributes from old data models to GenericAsset
+
+Revision ID: 6cf5b241b85f
+Revises: 1ae32ffc8c3f
+Create Date: 2021-11-11 17:18:15.395915
+
+"""
+import json
+from datetime import datetime
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "6cf5b241b85f"
+down_revision = "1ae32ffc8c3f"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column(
+        "generic_asset", sa.Column("attributes", sa.JSON(), nullable=True, default="{}")
+    )
+    op.add_column(
+        "sensor", sa.Column("attributes", sa.JSON(), nullable=True, default="{}")
+    )
+
+    """
+    - For each OldModel (Market/WeatherSensor/Asset), get the Sensor with the same id as the OldModel,
+      and then get the GenericAsset of that Sensor.
+    - Add the OldModel's display name to the corresponding GenericAsset's attributes,
+      and other attributes we want to copy.
+    - Find the OldModelType (MarketType/WeatherSensorType/AssetType) of the OldModel,
+      and copy its seasonalities to the GenericAsset's attributes.
+    """
+    # todo: find places where we look for seasonality and get it from the corresponding GenericAsset instead
+    # todo: find places where we look for old_model_type and get it from the corresponding GenericAsset instead
+
+    # Declare ORM table views
+    t_generic_asset = sa.Table(
+        "generic_asset",
+        sa.MetaData(),
+        sa.Column("id"),
+        sa.Column("attributes"),
+    )
+    t_sensor = sa.Table(
+        "sensor",
+        sa.MetaData(),
+        sa.Column("id"),
+        sa.Column("attributes"),
+        sa.Column("generic_asset_id"),
+        sa.Column("unit"),
+        sa.Column("event_resolution"),
+        sa.Column("knowledge_horizon_fnc"),
+        sa.Column("knowledge_horizon_par"),
+    )
+    t_market = sa.Table(
+        "market",
+        sa.MetaData(),
+        sa.Column("id", sa.Integer),
+        sa.Column("market_type_name", sa.String(80)),
+        sa.Column(
+            "display_name", sa.String(80)
+        ),  # Copy to both Sensor and to GenericAsset
+        sa.Column("unit"),  # Copy to Sensor [done]
+        sa.Column("event_resolution"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_fnc"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_par"),  # Copy to Sensor [done]
+    )
+    t_market_type = sa.Table(
+        "market_type",
+        sa.MetaData(),
+        sa.Column("name", sa.String(80)),
+        sa.Column("daily_seasonality", sa.Boolean),  # Copy to Sensor
+        sa.Column("weekly_seasonality", sa.Boolean),  # Copy to Sensor
+        sa.Column("yearly_seasonality", sa.Boolean),  # Copy to Sensor
+    )
+    t_asset = sa.Table(
+        "asset",
+        sa.MetaData(),
+        sa.Column("id"),
+        sa.Column("asset_type_name"),
+        sa.Column("display_name"),  # Copy to both Sensor and to GenericAsset
+        sa.Column("capacity_in_mw"),  # Copy to Sensor
+        sa.Column("min_soc_in_mwh"),  # Copy to GenericAsset [1]
+        sa.Column("max_soc_in_mwh"),  # Copy to GenericAsset [1]
+        sa.Column("soc_in_mwh"),  # Copy to GenericAsset [1]
+        sa.Column("soc_datetime"),  # Copy to GenericAsset [1]
+        sa.Column("soc_udi_event_id"),  # Copy to GenericAsset [2]
+        sa.Column("market_id"),  # Copy to Sensor [3]
+        sa.Column("unit"),  # Copy to Sensor [done]
+        sa.Column("event_resolution"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_fnc"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_par"),  # Copy to Sensor [done]
+    )
+    # [1] will be moved to a separate sensor later
+    # [2] deprecated in favour of Redis job id since api v1.3
+    # [3] will be deprecated in favour of something like a weighed by relationship (could be multiple)
+    t_asset_type = sa.Table(
+        "asset_type",
+        sa.MetaData(),
+        sa.Column("name", sa.String(80)),
+        sa.Column("is_consumer"),  # Copy to Sensor
+        sa.Column("is_producer"),  # Copy to Sensor
+        sa.Column("can_curtail"),  # Copy to GenericAsset [4]
+        sa.Column("can_shift"),  # Copy to GenericAsset [4]
+        sa.Column("daily_seasonality", sa.Boolean),  # Copy to Sensor
+        sa.Column("weekly_seasonality", sa.Boolean),  # Copy to Sensor
+        sa.Column("yearly_seasonality", sa.Boolean),  # Copy to Sensor
+    )
+    # [4] will be deprecated in favour of actuator functionality
+    t_weather_sensor = sa.Table(
+        "weather_sensor",
+        sa.MetaData(),
+        sa.Column("id"),
+        sa.Column("weather_sensor_type_name"),
+        sa.Column("display_name"),  # Copy to both Sensor and to GenericAsset
+        sa.Column("unit"),  # Copy to Sensor [done]
+        sa.Column("event_resolution"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_fnc"),  # Copy to Sensor [done]
+        sa.Column("knowledge_horizon_par"),  # Copy to Sensor [done]
+    )
+    t_weather_sensor_type = sa.Table(
+        "weather_sensor_type",
+        sa.MetaData(),
+        sa.Column("name", sa.String(80)),
+    )
+
+    # Use SQLAlchemy's connection and transaction to go through the data
+    connection = op.get_bind()
+
+    # Set default attributes
+    connection.execute(
+        t_sensor.update().values(
+            attributes=json.dumps({}),
+        )
+    )
+    connection.execute(
+        t_generic_asset.update().values(
+            attributes=json.dumps({}),
+        )
+    )
+
+    copy_attributes(
+        connection,
+        t_market,
+        t_sensor,
+        t_target=t_sensor,
+        t_old_model_type=t_market_type,
+        old_model_attributes=["id", "market_type_name", "display_name"],
+        old_model_type_attributes=[
+            "daily_seasonality",
+            "weekly_seasonality",
+            "yearly_seasonality",
+        ],
+    )
+    copy_attributes(
+        connection,
+        t_market,
+        t_sensor,
+        t_target=t_generic_asset,
+        t_old_model_type=t_market_type,
+        old_model_attributes=["id", "market_type_name", "display_name"],
+    )
+    copy_attributes(
+        connection,
+        t_weather_sensor,
+        t_sensor,
+        t_target=t_sensor,
+        t_old_model_type=t_weather_sensor_type,
+        old_model_attributes=["id", "weather_sensor_type_name", "display_name"],
+        extra_attributes={
+            "daily_seasonality": True,
+            "weekly_seasonality": False,
+            "yearly_seasonality": True,
+        },  # The WeatherSensor table had these hardcoded (d, w, y) seasonalities
+    )
+    copy_attributes(
+        connection,
+        t_weather_sensor,
+        t_sensor,
+        t_target=t_generic_asset,
+        t_old_model_type=t_weather_sensor_type,
+        old_model_attributes=["id", "weather_sensor_type_name", "display_name"],
+    )
+    copy_attributes(
+        connection,
+        t_asset,
+        t_sensor,
+        t_target=t_sensor,
+        t_old_model_type=t_asset_type,
+        old_model_attributes=[
+            "id",
+            "asset_type_name",
+            "display_name",
+            "capacity_in_mw",
+            "market_id",
+        ],
+        old_model_type_attributes=[
+            "is_consumer",
+            "is_producer",
+            "daily_seasonality",
+            "weekly_seasonality",
+            "yearly_seasonality",
+        ],
+    )
+    copy_attributes(
+        connection,
+        t_asset,
+        t_sensor,
+        t_target=t_generic_asset,
+        t_old_model_type=t_asset_type,
+        old_model_attributes=[
+            "id",
+            "asset_type_name",
+            "display_name",
+            "min_soc_in_mwh",
+            "max_soc_in_mwh",
+            "soc_in_mwh",
+            "soc_datetime",
+            "soc_udi_event_id",
+        ],
+        old_model_type_attributes=[
+            "can_curtail",
+            "can_shift",
+        ],
+    )
+    op.alter_column(
+        "sensor",
+        "attributes",
+        nullable=False,
+    )
+    op.alter_column(
+        "generic_asset",
+        "attributes",
+        nullable=False,
+    )
+    copy_sensor_columns(connection, t_market, t_sensor)
+    copy_sensor_columns(connection, t_weather_sensor, t_sensor)
+    copy_sensor_columns(connection, t_asset, t_sensor)
+
+
+def downgrade():
+    op.drop_column("sensor", "attributes")
+    op.drop_column("generic_asset", "attributes")
+
+
+def copy_sensor_columns(connection, t_old_model, t_sensor):
+    old_model_attributes = [
+        "id",
+        "unit",
+        "event_resolution",
+        "knowledge_horizon_fnc",
+        "knowledge_horizon_par",
+    ]
+
+    # Get columns from old model
+    results = connection.execute(
+        sa.select([getattr(t_old_model.c, a) for a in old_model_attributes])
+    ).fetchall()
+
+    for sensor_id, *args in results:
+
+        # Obtain columns we want to copy over, from the old model
+        old_model_columns_to_copy = {
+            k: v if not isinstance(v, dict) else json.dumps(v)
+            for k, v in zip(old_model_attributes[-len(args) :], args)
+        }
+
+        # Fill in the Sensor's columns
+        connection.execute(
+            t_sensor.update()
+            .where(t_sensor.c.id == sensor_id)
+            .values(
+                **old_model_columns_to_copy,
+            )
+        )
+
+
+def copy_attributes(
+    connection,
+    t_old_model,
+    t_sensor,
+    t_target,
+    t_old_model_type,
+    old_model_attributes,
+    old_model_type_attributes=[],
+    extra_attributes={},
+):
+    """
+
+    :param old_model_attributes: first two attributes should be id and old_model_type_name, then any other columns we want to copy over from the old model
+    :param old_model_type_attributes: columns we want to copy over from the old model type
+    :param extra_attributes: any additional attributes we want to set
+    """
+    # Get attributes from old model
+    results = connection.execute(
+        sa.select([getattr(t_old_model.c, a) for a in old_model_attributes])
+    ).fetchall()
+
+    for id, type_name, *args in results:
+
+        # Obtain attributes we want to copy over, from the old model
+        old_model_attributes_to_copy = {
+            k: v if not isinstance(v, datetime) else v.isoformat()
+            for k, v in zip(old_model_attributes[-len(args) :], args)
+        }
+
+        # Obtain seasonality attributes we want to copy over, from the old model type
+        old_model_type_attributes_to_copy = get_old_model_type_attributes(
+            connection,
+            type_name,
+            t_old_model_type,
+            old_model_type_attributes=old_model_type_attributes,
+        )
+
+        # Find out where to copy over the attributes
+        if t_target.name == "generic_asset":
+            target_id = get_generic_asset_id(connection, id, t_sensor)
+        elif t_target.name == "sensor":
+            target_id = id
+        else:
+            raise ValueError
+
+        # Fill in the target class's attributes
+        connection.execute(
+            t_target.update()
+            .where(t_target.c.id == target_id)
+            .values(
+                attributes=json.dumps(
+                    {
+                        **old_model_attributes_to_copy,
+                        **old_model_type_attributes_to_copy,
+                        **extra_attributes,
+                    }
+                )
+            )
+        )
+
+
+def get_generic_asset_id(connection, old_model_id: int, t_sensors) -> int:
+    """Get the Sensor with the same id as the OldModel, and then get the id of the GenericAsset of that Sensor."""
+    (generic_asset_id,) = connection.execute(
+        sa.select(
+            [
+                t_sensors.c.generic_asset_id,
+            ]
+        ).filter(t_sensors.c.id == old_model_id)
+    ).one_or_none()
+    assert generic_asset_id is not None
+    return generic_asset_id
+
+
+def get_old_model_type_attributes(
+    connection, old_model_type_name, t_old_model_types, old_model_type_attributes
+) -> dict:
+    """Get the attributes from the OldModelType."""
+    values = connection.execute(
+        sa.select(
+            [getattr(t_old_model_types.c, a) for a in old_model_type_attributes]
+        ).filter(t_old_model_types.c.name == old_model_type_name)
+    ).one_or_none()
+    assert values is not None
+    return {k: v for k, v in zip(old_model_type_attributes, values)}
diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py
index 214825c61..2cbd9eaac 100644
--- a/flexmeasures/data/models/assets.py
+++ b/flexmeasures/data/models/assets.py
@@ -10,6 +10,7 @@
 from flexmeasures.data.models.time_series import Sensor, TimedValue
 from flexmeasures.data.models.generic_assets import (
     create_generic_asset,
+    GenericAsset,
     GenericAssetType,
 )
 from flexmeasures.utils.entity_address_utils import build_entity_address
@@ -110,25 +111,105 @@ class Asset(db.Model, tb.SensorDBMixin):
 
     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.
+        # Also keep track of ownership by creating a GenericAsset and assigning the new Sensor to it.
         if "id" not in kwargs:
+
+            # Set up generic asset
             generic_assets_arg = kwargs.copy()
+            if "asset_type_name" in generic_assets_arg:
+                asset_type = db.session.query(AssetType).get(
+                    generic_assets_arg["asset_type_name"]
+                )
+            else:
+                asset_type = generic_assets_arg["asset_type"]
+            asset_type_attributes_for_generic_asset = [
+                "can_curtail",
+                "can_shift",
+            ]
+            asset_attributes_for_generic_asset = [
+                "display_name",
+                "min_soc_in_mwh",
+                "max_soc_in_mwh",
+                "soc_in_mwh",
+                "soc_datetime",
+                "soc_udi_event_id",
+            ]
+            generic_asset_attributes_from_asset_type = {
+                a: getattr(asset_type, a)
+                for a in asset_type_attributes_for_generic_asset
+            }
+            generic_asset_attributes_from_asset = {
+                a: getattr(self, a)
+                if not isinstance(getattr(self, a), datetime)
+                else getattr(self, a).isoformat()
+                for a in asset_attributes_for_generic_asset
+            }
+            generic_assets_arg = {
+                **generic_assets_arg,
+                **{
+                    "attributes": {
+                        **generic_asset_attributes_from_asset_type,
+                        **generic_asset_attributes_from_asset,
+                    },
+                },
+            }
+
             if "owner_id" in kwargs:
                 owner = User.query.get(kwargs["owner_id"])
                 if owner:
                     generic_assets_arg.update(account_id=owner.account_id)
             new_generic_asset = create_generic_asset("asset", **generic_assets_arg)
-            new_sensor = Sensor(name=kwargs["name"], generic_asset=new_generic_asset)
+
+            # Set up sensor
+            sensor_kwargs = dict(
+                name=kwargs["name"],
+                generic_asset=new_generic_asset,
+            )
+            asset_type_attributes_for_sensor = [
+                "is_consumer",
+                "is_producer",
+                "daily_seasonality",
+                "weekly_seasonality",
+                "yearly_seasonality",
+                "weather_correlations",
+            ]
+            asset_attributes_for_sensor = [
+                "display_name",
+                "capacity_in_mw",
+                "market_id",
+            ]
+            sensor_attributes_from_asset_type = {
+                a: getattr(asset_type, a) for a in asset_type_attributes_for_sensor
+            }
+            sensor_attributes_from_asset = {
+                a: getattr(self, a)
+                if not isinstance(getattr(self, a), datetime)
+                else getattr(self, a).isoformat()
+                for a in asset_attributes_for_sensor
+            }
+            sensor_kwargs = {
+                **sensor_kwargs,
+                **{
+                    "attributes": {
+                        **sensor_attributes_from_asset_type,
+                        **sensor_attributes_from_asset,
+                    },
+                },
+            }
+            new_sensor = Sensor(
+                **sensor_kwargs,
+            )
             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"]
-        if "unit" not in kwargs:
-            kwargs["unit"] = "MW"  # current default
-        super(Asset, self).__init__(**kwargs)
         self.id = sensor_id
         if self.unit != "MW":
             raise Exception("FlexMeasures only supports MW as unit for now.")
@@ -136,6 +217,15 @@ def __init__(self, **kwargs):
         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",
@@ -159,6 +249,23 @@ def latest_state(self, event_ends_before: Optional[datetime] = None) -> "Power":
             )
         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."""
@@ -257,7 +364,7 @@ def make_query(
         **kwargs,
     ) -> Query:
         """Construct the database query."""
-        return super().make_query(asset_class=Asset, **kwargs)
+        return super().make_query(old_sensor_class=Asset, **kwargs)
 
     def to_dict(self):
         return {
diff --git a/flexmeasures/data/models/forecasting/__init__.py b/flexmeasures/data/models/forecasting/__init__.py
index e16317205..faba9c160 100644
--- a/flexmeasures/data/models/forecasting/__init__.py
+++ b/flexmeasures/data/models/forecasting/__init__.py
@@ -42,7 +42,7 @@ def lookup_model_specs_configurator(
 
     Model meta data in this context means a tuple of:
         * timetomodel.ModelSpecs. To fill in those specs, a configurator should accept:
-          - generic_asset: Union[Asset, Market, WeatherSensor],
+          - old_sensor: Union[Asset, Market, WeatherSensor],
           - start: datetime,  # Start of forecast period
           - end: datetime,  # End of forecast period
           - horizon: timedelta,  # Duration between time of forecasting and time which is forecast
diff --git a/flexmeasures/data/models/forecasting/model_spec_factory.py b/flexmeasures/data/models/forecasting/model_spec_factory.py
index 5a01c2898..13f0ac3bb 100644
--- a/flexmeasures/data/models/forecasting/model_spec_factory.py
+++ b/flexmeasures/data/models/forecasting/model_spec_factory.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Type, Union
 from datetime import datetime, timedelta, tzinfo
 from pprint import pformat
 import logging
@@ -19,13 +19,10 @@
 )
 import pandas as pd
 
-from flexmeasures.data.models.assets import AssetType, Asset
-from flexmeasures.data.models.markets import MarketType, Market
-from flexmeasures.data.models.weather import WeatherSensorType, WeatherSensor, Weather
-from flexmeasures.data.models.utils import (
-    determine_asset_type_by_asset,
-    determine_asset_value_class_by_asset,
-)
+from flexmeasures.data.models.assets import Power
+from flexmeasures.data.models.markets import Price
+from flexmeasures.data.models.time_series import Sensor
+from flexmeasures.data.models.weather import Weather, WeatherSensor
 from flexmeasures.data.models.forecasting.utils import (
     create_lags,
     set_training_and_testing_dates,
@@ -46,19 +43,20 @@
 class TBSeriesSpecs(SeriesSpecs):
     """Compatibility for using timetomodel.SeriesSpecs with timely_beliefs.BeliefsDataFrames.
 
-    This implements _load_series such that TimedValue.collect is called on the generic asset class,
+    This implements _load_series such that <time_series_class>.collect is called,
     with the parameters in collect_params.
     The collect function is expected to return a BeliefsDataFrame.
     """
 
-    generic_asset_value_class: Any  # with collect method
+    time_series_class: Any  # with <collect_fnc> method (named "collect" by default)
     collect_params: dict
 
     def __init__(
         self,
-        generic_asset_value_class,
+        time_series_class,
         collect_params: dict,
         name: str,
+        collect_fnc: str = "collect",
         original_tz: Optional[tzinfo] = pytz.utc,  # postgres stores naive datetimes
         feature_transformation: Optional[ReversibleTransformation] = None,
         post_load_processing: Optional[Transformation] = None,
@@ -73,15 +71,14 @@ def __init__(
             resampling_config,
             interpolation_config,
         )
-        self.generic_asset_value_class = generic_asset_value_class
+        self.time_series_class = time_series_class
         self.collect_params = collect_params
+        self.collect_fnc = collect_fnc
 
     def _load_series(self) -> pd.Series:
-        logger.info(
-            "Reading %s data from database" % self.generic_asset_value_class.__name__
-        )
+        logger.info("Reading %s data from database" % self.time_series_class.__name__)
 
-        bdf: BeliefsDataFrame = self.generic_asset_value_class.collect(
+        bdf: BeliefsDataFrame = getattr(self.time_series_class, self.collect_fnc)(
             **self.collect_params
         )
         assert isinstance(bdf, BeliefsDataFrame)
@@ -101,7 +98,7 @@ def check_data(self, df: pd.DataFrame):
                 "No values found in database for the requested %s data. It's no use to continue I'm afraid."
                 " Here's a print-out of what I tried to collect:\n\n%s\n\n"
                 % (
-                    self.generic_asset_value_class.__name__,
+                    self.time_series_class.__name__,
                     pformat(self.collect_params, sort_dicts=False),
                 )
             )
@@ -110,14 +107,15 @@ def check_data(self, df: pd.DataFrame):
                 "Nan values found in database for the requested %s data. It's no use to continue I'm afraid."
                 " Here's a print-out of what I tried to collect:\n\n%s\n\n"
                 % (
-                    self.generic_asset_value_class.__name__,
+                    self.time_series_class.__name__,
                     pformat(self.collect_params, sort_dicts=False),
                 )
             )
 
 
 def create_initial_model_specs(  # noqa: C901
-    generic_asset: Union[Asset, Market, WeatherSensor],
+    sensor: Sensor,
+    time_series_class: Type[Union[Power, Price, Weather]],
     forecast_start: datetime,  # Start of forecast period
     forecast_end: datetime,  # End of forecast period
     forecast_horizon: timedelta,  # Duration between time of forecasting and end time of the event that is forecast
@@ -138,17 +136,14 @@ def create_initial_model_specs(  # noqa: C901
           calendar day.
     """
 
-    generic_asset_type = determine_asset_type_by_asset(generic_asset)
-    generic_asset_value_class = determine_asset_value_class_by_asset(generic_asset)
-
     params = _parameterise_forecasting_by_asset_and_asset_type(
-        generic_asset, generic_asset_type, transform_to_normal
+        sensor, transform_to_normal
     )
     params.update(custom_model_params if custom_model_params is not None else {})
 
     lags = create_lags(
         params["n_lags"],
-        generic_asset_type,
+        sensor,
         forecast_horizon,
         params["resolution"],
         use_periodicity,
@@ -168,8 +163,7 @@ def create_initial_model_specs(  # noqa: C901
                     "regressor_transformation", {}
                 )
         regressor_specs = configure_regressors_for_nearest_weather_sensor(
-            generic_asset,
-            generic_asset_type,
+            sensor,
             query_window,
             forecast_horizon,
             regressor_transformation,
@@ -180,10 +174,10 @@ def create_initial_model_specs(  # noqa: C901
         ex_post_horizon = timedelta(hours=0)
 
     outcome_var_spec = TBSeriesSpecs(
-        name=generic_asset_type.name,
-        generic_asset_value_class=generic_asset_value_class,
+        name=sensor.generic_asset.generic_asset_type.name,
+        time_series_class=time_series_class,
         collect_params=dict(
-            generic_asset_names=[generic_asset.name],
+            old_sensor_names=[sensor.name],
             query_window=query_window,
             belief_horizon_window=(None, ex_post_horizon),
         ),
@@ -192,7 +186,7 @@ def create_initial_model_specs(  # noqa: C901
     )
     # Set defaults if needed
     if params.get("event_resolution", None) is None:
-        params["event_resolution"] = generic_asset.event_resolution
+        params["event_resolution"] = sensor.event_resolution
     if params.get("remodel_frequency", None) is None:
         params["remodel_frequency"] = timedelta(days=7)
     specs = ModelSpecs(
@@ -214,8 +208,7 @@ def create_initial_model_specs(  # noqa: C901
 
 
 def _parameterise_forecasting_by_asset_and_asset_type(
-    generic_asset: Union[Asset, Market, WeatherSensor],
-    generic_asset_type: Union[AssetType, MarketType, WeatherSensorType],
+    sensor: Sensor,
     transform_to_normal: bool,
 ) -> dict:
     """Fill in the best parameters we know (generic or by asset (type))"""
@@ -224,48 +217,44 @@ def _parameterise_forecasting_by_asset_and_asset_type(
     params["training_and_testing_period"] = timedelta(days=30)
     params["ratio_training_testing_data"] = 14 / 15
     params["n_lags"] = 7
-    params["resolution"] = generic_asset.event_resolution
+    params["resolution"] = sensor.event_resolution
 
     if transform_to_normal:
         params[
             "outcome_var_transformation"
-        ] = get_normalization_transformation_by_asset_type(generic_asset_type)
+        ] = get_normalization_transformation_from_sensor_attributes(sensor)
 
     return params
 
 
-def get_normalization_transformation_by_asset_type(
-    generic_asset_type: Union[AssetType, MarketType, WeatherSensorType]
+def get_normalization_transformation_from_sensor_attributes(
+    sensor: Union[Sensor, WeatherSensor],
 ) -> Optional[Transformation]:
     """
     Transform data to be normal, using the BoxCox transformation. Lambda parameter is chosen
-    according ot the asset type.
+    according to the asset type.
     """
-    if isinstance(generic_asset_type, AssetType):
-        if (generic_asset_type.is_consumer and not generic_asset_type.is_producer) or (
-            generic_asset_type.is_producer and not generic_asset_type.is_consumer
-        ):
-            return BoxCoxTransformation(lambda2=0.1)
-        else:
-            return None
-    elif isinstance(generic_asset_type, MarketType):
-        return None
-    elif isinstance(generic_asset_type, WeatherSensorType):
-        if generic_asset_type.name in ["wind_speed", "radiation"]:
-            # Values cannot be negative and are often zero
-            return BoxCoxTransformation(lambda2=0.1)
-        elif generic_asset_type.name == "temperature":
-            # Values can be positive or negative when given in degrees Celsius, but non-negative only in Kelvin
-            return BoxCoxTransformation(lambda2=273.16)
-        else:
-            return None
+    if (
+        sensor.get_attribute("is_consumer") and not sensor.get_attribute("is_producer")
+    ) or (
+        sensor.get_attribute("is_producer") and not sensor.get_attribute("is_consumer")
+    ):
+        return BoxCoxTransformation(lambda2=0.1)
+    elif sensor.generic_asset.generic_asset_type.name in [
+        "wind_speed",
+        "radiation",
+    ]:
+        # Values cannot be negative and are often zero
+        return BoxCoxTransformation(lambda2=0.1)
+    elif sensor.generic_asset.generic_asset_type.name == "temperature":
+        # Values can be positive or negative when given in degrees Celsius, but non-negative only in Kelvin
+        return BoxCoxTransformation(lambda2=273.16)
     else:
-        raise TypeError("Unknown generic asset type.")
+        return None
 
 
 def configure_regressors_for_nearest_weather_sensor(
-    generic_asset,
-    generic_asset_type,
+    sensor: Sensor,
     query_window,
     horizon,
     regressor_transformation,  # the regressor transformation can be passed in
@@ -273,41 +262,38 @@ def configure_regressors_for_nearest_weather_sensor(
 ) -> List[TBSeriesSpecs]:
     """For Assets, we use weather data as regressors. Here, we configure them."""
     regressor_specs = []
-    if isinstance(generic_asset, Asset):
-        sensor_types = generic_asset_type.weather_correlations
+    sensor_types = sensor.get_attribute("weather_correlations")
+    if sensor_types:
         current_app.logger.info(
-            "For %s, I need sensors: %s" % (generic_asset, sensor_types)
+            "For %s, I need sensors: %s" % (sensor.name, sensor_types)
         )
         for sensor_type in sensor_types:
 
             # Find nearest weather sensor
-            closest_sensor = find_closest_weather_sensor(
-                sensor_type, object=generic_asset
-            )
+            closest_sensor = find_closest_weather_sensor(sensor_type, object=sensor)
             if closest_sensor is None:
                 current_app.logger.warning(
                     "No sensor found of sensor type %s to use as regressor for %s."
-                    % (sensor_type, generic_asset)
+                    % (sensor_type, sensor.name)
                 )
             else:
                 current_app.logger.info(
-                    "Using sensor %s as regressor for %s."
-                    % (sensor_type, generic_asset)
+                    "Using sensor %s as regressor for %s." % (sensor_type, sensor.name)
                 )
                 # Collect the weather data for the requested time window
                 regressor_specs_name = "%s_l0" % sensor_type
                 if len(regressor_transformation.keys()) == 0 and transform_to_normal:
                     regressor_transformation = (
-                        get_normalization_transformation_by_asset_type(
-                            WeatherSensorType(name=sensor_type)
+                        get_normalization_transformation_from_sensor_attributes(
+                            closest_sensor,
                         )
                     )
                 regressor_specs.append(
                     TBSeriesSpecs(
                         name=regressor_specs_name,
-                        generic_asset_value_class=Weather,
+                        time_series_class=Weather,
                         collect_params=dict(
-                            generic_asset_names=[closest_sensor.name],
+                            old_sensor_names=[closest_sensor.name],
                             query_window=query_window,
                             belief_horizon_window=(horizon, None),
                         ),
diff --git a/flexmeasures/data/models/forecasting/utils.py b/flexmeasures/data/models/forecasting/utils.py
index 895aa9261..e095ff01e 100644
--- a/flexmeasures/data/models/forecasting/utils.py
+++ b/flexmeasures/data/models/forecasting/utils.py
@@ -1,14 +1,14 @@
 from typing import Tuple, List, Union
 from datetime import datetime, timedelta
 
-from flexmeasures.data.config import db
 from flexmeasures.data.models.forecasting.exceptions import NotEnoughDataException
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.utils.time_utils import as_server_time
 
 
 def check_data_availability(
-    generic_asset,
-    generic_asset_value_class,
+    old_sensor_model,
+    old_time_series_data_model,
     forecast_start: datetime,
     forecast_end: datetime,
     query_window: Tuple[datetime, datetime],
@@ -18,11 +18,11 @@ def check_data_availability(
     for training window and lagged variables. Otherwise, suggest new forecast period.
     TODO: we could also check regressor data, if we get regressor specs passed in here.
     """
-    q = generic_asset_value_class.query.join(generic_asset.__class__).filter(
-        generic_asset.__class__.name == generic_asset.name
+    q = old_time_series_data_model.query.join(old_sensor_model.__class__).filter(
+        old_sensor_model.__class__.name == old_sensor_model.name
     )
-    first_value = q.order_by(generic_asset_value_class.datetime.asc()).first()
-    last_value = q.order_by(generic_asset_value_class.datetime.desc()).first()
+    first_value = q.order_by(old_time_series_data_model.datetime.asc()).first()
+    last_value = q.order_by(old_time_series_data_model.datetime.desc()).first()
     if first_value is None:
         raise NotEnoughDataException(
             "No data available at all. Forecasting impossible."
@@ -32,26 +32,26 @@ def check_data_availability(
     if query_window[0] < first:
         suggested_start = forecast_start + (first - query_window[0])
         raise NotEnoughDataException(
-            f"Not enough data to forecast {generic_asset.name} "
+            f"Not enough data to forecast {old_sensor_model.name} "
             f"for the forecast window {as_server_time(forecast_start)} to {as_server_time(forecast_end)}. "
             f"I needed to query from {as_server_time(query_window[0])}, "
-            f"but the first value available is from {first} to {first + generic_asset.event_resolution}. "
+            f"but the first value available is from {first} to {first + old_sensor_model.event_resolution}. "
             f"Consider setting the start date to {as_server_time(suggested_start)}."
         )
-    if query_window[1] - horizon > last + generic_asset.event_resolution:
+    if query_window[1] - horizon > last + old_sensor_model.event_resolution:
         suggested_end = forecast_end + (last - (query_window[1] - horizon))
         raise NotEnoughDataException(
-            f"Not enough data to forecast {generic_asset.name} "
+            f"Not enough data to forecast {old_sensor_model.name} "
             f"for the forecast window {as_server_time(forecast_start)} to {as_server_time(forecast_end)}. "
             f"I needed to query until {as_server_time(query_window[1] - horizon)}, "
-            f"but the last value available is from {last} to {last + generic_asset.event_resolution}. "
+            f"but the last value available is from {last} to {last + old_sensor_model.event_resolution}. "
             f"Consider setting the end date to {as_server_time(suggested_end)}."
         )
 
 
 def create_lags(
     n_lags: int,
-    generic_asset_type: db.Model,
+    sensor: Sensor,
     horizon: timedelta,
     resolution: timedelta,
     use_periodicity: bool,
@@ -71,12 +71,11 @@ def create_lags(
         lags.append((L + number_of_nan_lags) * lag_period)
 
     # Include relevant measurements given the asset's periodicity
-    if use_periodicity and hasattr(generic_asset_type, "daily_seasonality"):
-        if generic_asset_type.daily_seasonality:
-            lag_period = timedelta(days=1)
-            number_of_nan_lags = 1 + (horizon - resolution) // lag_period
-            for L in range(n_lags):
-                lags.append((L + number_of_nan_lags) * lag_period)
+    if use_periodicity and sensor.get_attribute("daily_seasonality"):
+        lag_period = timedelta(days=1)
+        number_of_nan_lags = 1 + (horizon - resolution) // lag_period
+        for L in range(n_lags):
+            lags.append((L + number_of_nan_lags) * lag_period)
 
     # Remove possible double entries
     return list(set(lags))
diff --git a/flexmeasures/data/models/generic_assets.py b/flexmeasures/data/models/generic_assets.py
index b1d791f81..585f00820 100644
--- a/flexmeasures/data/models/generic_assets.py
+++ b/flexmeasures/data/models/generic_assets.py
@@ -25,6 +25,7 @@ class GenericAsset(db.Model):
     name = db.Column(db.String(80), default="")
     latitude = db.Column(db.Float, nullable=True)
     longitude = db.Column(db.Float, nullable=True)
+    attributes = db.Column(db.JSON, nullable=False, default="{}")
 
     generic_asset_type_id = db.Column(
         db.Integer, db.ForeignKey("generic_asset_type.id"), nullable=False
@@ -56,6 +57,10 @@ def location(self) -> Optional[Tuple[float, float]]:
             return self.latitude, self.longitude
         return None
 
+    def get_attribute(self, attribute: str):
+        if attribute in self.attributes:
+            return self.attributes[attribute]
+
 
 def create_generic_asset(generic_asset_type: str, **kwargs) -> GenericAsset:
     """Create a GenericAsset and assigns it an id.
@@ -80,7 +85,9 @@ def create_generic_asset(generic_asset_type: str, **kwargs) -> GenericAsset:
     if generic_asset_type is None:
         raise ValueError(f"Cannot find GenericAssetType {asset_type_name} in database.")
     new_generic_asset = GenericAsset(
-        name=kwargs["name"], generic_asset_type_id=generic_asset_type.id
+        name=kwargs["name"],
+        generic_asset_type_id=generic_asset_type.id,
+        attributes=kwargs["attributes"] if "attributes" in kwargs else {},
     )
     for arg in ("latitude", "longitude", "account_id"):
         if arg in kwargs:
diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py
index 5d59e8640..d74a03ea6 100644
--- a/flexmeasures/data/models/markets.py
+++ b/flexmeasures/data/models/markets.py
@@ -7,6 +7,7 @@
 from flexmeasures.data.config import db
 from flexmeasures.data.models.generic_assets import (
     create_generic_asset,
+    GenericAsset,
     GenericAssetType,
 )
 from flexmeasures.data.models.time_series import Sensor, TimedValue
@@ -100,6 +101,23 @@ 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."""
@@ -135,7 +153,7 @@ class Price(TimedValue, db.Model):
     @classmethod
     def make_query(cls, **kwargs) -> Query:
         """Construct the database query."""
-        return super().make_query(asset_class=Market, **kwargs)
+        return super().make_query(old_sensor_class=Market, **kwargs)
 
     def __init__(self, **kwargs):
         super(Price, self).__init__(**kwargs)
diff --git a/flexmeasures/data/models/planning/battery.py b/flexmeasures/data/models/planning/battery.py
index 07a345213..bc725c138 100644
--- a/flexmeasures/data/models/planning/battery.py
+++ b/flexmeasures/data/models/planning/battery.py
@@ -3,8 +3,7 @@
 
 import pandas as pd
 
-from flexmeasures.data.models.assets import Asset
-from flexmeasures.data.models.markets import Market
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.models.planning.solver import device_scheduler
 from flexmeasures.data.models.planning.utils import (
     initialize_df,
@@ -15,8 +14,7 @@
 
 
 def schedule_battery(
-    asset: Asset,
-    market: Market,
+    sensor: Sensor,
     start: datetime,
     end: datetime,
     resolution: timedelta,
@@ -31,7 +29,7 @@ def schedule_battery(
 
     # Check for known prices or price forecasts, trimming planning window accordingly
     prices, (start, end) = get_prices(
-        market, (start, end), resolution, allow_trimmed_query_window=True
+        sensor, (start, end), resolution, allow_trimmed_query_window=True
     )
     if soc_targets is not None:
         # soc targets are at the end of each time slot, while prices are indexed by the start of each time slot
@@ -71,14 +69,16 @@ def schedule_battery(
         )  # shift "equals" constraint for target SOC by one resolution (the target defines a state at a certain time,
         # while the "equals" constraint defines what the total stock should be at the end of a time slot,
         # where the time slot is indexed by its starting time)
-    device_constraints[0]["min"] = (asset.min_soc_in_mwh - soc_at_start) * (
-        timedelta(hours=1) / resolution
+    device_constraints[0]["min"] = (
+        sensor.get_attribute("min_soc_in_mwh") - soc_at_start
+    ) * (timedelta(hours=1) / resolution)
+    device_constraints[0]["max"] = (
+        sensor.get_attribute("max_soc_in_mwh") - soc_at_start
+    ) * (timedelta(hours=1) / resolution)
+    device_constraints[0]["derivative min"] = (
+        sensor.get_attribute("capacity_in_mw") * -1
     )
-    device_constraints[0]["max"] = (asset.max_soc_in_mwh - soc_at_start) * (
-        timedelta(hours=1) / resolution
-    )
-    device_constraints[0]["derivative min"] = asset.capacity_in_mw * -1
-    device_constraints[0]["derivative max"] = asset.capacity_in_mw
+    device_constraints[0]["derivative max"] = sensor.get_attribute("capacity_in_mw")
 
     # Set up EMS constraints (no additional constraints)
     columns = ["derivative max", "derivative min"]
diff --git a/flexmeasures/data/models/planning/charging_station.py b/flexmeasures/data/models/planning/charging_station.py
index 5074cdcc7..6677c8e98 100644
--- a/flexmeasures/data/models/planning/charging_station.py
+++ b/flexmeasures/data/models/planning/charging_station.py
@@ -3,8 +3,7 @@
 
 from pandas import Series, Timestamp
 
-from flexmeasures.data.models.assets import Asset
-from flexmeasures.data.models.markets import Market
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.models.planning.solver import device_scheduler
 from flexmeasures.data.models.planning.utils import (
     initialize_df,
@@ -15,8 +14,7 @@
 
 
 def schedule_charging_station(
-    asset: Asset,
-    market: Market,
+    sensor: Sensor,
     start: datetime,
     end: datetime,
     resolution: timedelta,
@@ -32,7 +30,7 @@ def schedule_charging_station(
 
     # Check for known prices or price forecasts, trimming planning window accordingly
     prices, (start, end) = get_prices(
-        market, (start, end), resolution, allow_trimmed_query_window=True
+        sensor, (start, end), resolution, allow_trimmed_query_window=True
     )
     # soc targets are at the end of each time slot, while prices are indexed by the start of each time slot
     soc_targets = soc_targets.tz_convert("UTC")
@@ -81,14 +79,16 @@ def schedule_charging_station(
     ) - soc_at_start * (
         timedelta(hours=1) / resolution
     )  # Lacking information about the battery's nominal capacity, we use the highest target value as the maximum state of charge
-    if asset.is_pure_consumer:
+    if sensor.get_attribute("is_pure_consumer"):
         device_constraints[0]["derivative min"] = 0
     else:
-        device_constraints[0]["derivative min"] = asset.capacity_in_mw * -1
-    if asset.is_pure_producer:
+        device_constraints[0]["derivative min"] = (
+            sensor.get_attribute("capacity_in_mw") * -1
+        )
+    if sensor.get_attribute("is_pure_producer"):
         device_constraints[0]["derivative max"] = 0
     else:
-        device_constraints[0]["derivative max"] = asset.capacity_in_mw
+        device_constraints[0]["derivative max"] = sensor.get_attribute("capacity_in_mw")
 
     # Set up EMS constraints (no additional constraints)
     columns = ["derivative max", "derivative min"]
diff --git a/flexmeasures/data/models/planning/exceptions.py b/flexmeasures/data/models/planning/exceptions.py
index da897abfc..3337120b1 100644
--- a/flexmeasures/data/models/planning/exceptions.py
+++ b/flexmeasures/data/models/planning/exceptions.py
@@ -1,2 +1,6 @@
+class UnknownMarketException(Exception):
+    pass
+
+
 class UnknownPricesException(Exception):
     pass
diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py
index f6fc06cb6..97b2968ce 100644
--- a/flexmeasures/data/models/planning/utils.py
+++ b/flexmeasures/data/models/planning/utils.py
@@ -8,7 +8,11 @@
 import timely_beliefs as tb
 
 from flexmeasures.data.models.markets import Market, Price
-from flexmeasures.data.models.planning.exceptions import UnknownPricesException
+from flexmeasures.data.models.time_series import Sensor
+from flexmeasures.data.models.planning.exceptions import (
+    UnknownMarketException,
+    UnknownPricesException,
+)
 from flexmeasures.data.queries.utils import simplify_index
 
 
@@ -56,8 +60,15 @@ def add_tiny_price_slope(
     return prices
 
 
+def get_market(sensor: Sensor) -> Market:
+    market = Market.query.get(sensor.get_attribute("market_id"))
+    if market is None:
+        raise UnknownMarketException
+    return market
+
+
 def get_prices(
-    market: Market,
+    sensor: Sensor,
     query_window: Tuple[datetime, datetime],
     resolution: timedelta,
     allow_trimmed_query_window: bool = True,
@@ -66,6 +77,10 @@ def get_prices(
     todo: set a horizon to avoid collecting prices that are not known at the time of constructing the schedule
           (this may require implementing a belief time for scheduling jobs).
     """
+
+    # Look for the applicable market
+    market = get_market(sensor)
+
     price_bdf: tb.BeliefsDataFrame = Price.collect(
         market.name,
         query_window=query_window,
diff --git a/flexmeasures/data/models/time_series.py b/flexmeasures/data/models/time_series.py
index ce10d43b4..cce8cb86e 100644
--- a/flexmeasures/data/models/time_series.py
+++ b/flexmeasures/data/models/time_series.py
@@ -28,6 +28,8 @@
 class Sensor(db.Model, tb.SensorDBMixin):
     """A sensor measures events. """
 
+    attributes = db.Column(db.JSON, nullable=False, default="{}")
+
     generic_asset_id = db.Column(
         db.Integer,
         db.ForeignKey("generic_asset.id", ondelete="CASCADE"),
@@ -46,6 +48,7 @@ def __init__(
         name: str,
         generic_asset: Optional[GenericAsset] = None,
         generic_asset_id: Optional[int] = None,
+        attributes: Optional[dict] = None,
         **kwargs,
     ):
         assert (generic_asset is None) ^ (
@@ -57,12 +60,37 @@ def __init__(
             kwargs["generic_asset"] = generic_asset
         else:
             kwargs["generic_asset_id"] = generic_asset_id
+        if attributes is not None:
+            kwargs["attributes"] = attributes
         db.Model.__init__(self, **kwargs)
 
     @property
     def entity_address(self) -> str:
         return build_entity_address(dict(sensor_id=self.id), "sensor")
 
+    @property
+    def latitude(self) -> float:
+        return self.generic_asset.latitude
+
+    @property
+    def longitude(self) -> float:
+        return self.generic_asset.longitude
+
+    @property
+    def location(self) -> Optional[Tuple[float, float]]:
+        if None not in (self.latitude, self.longitude):
+            return self.latitude, self.longitude
+        return None
+
+    def get_attribute(self, attribute: str):
+        """Looks for the attribute on the Sensor.
+        If not found, looks for the attribute on the Sensor's GenericAsset.
+        """
+        if attribute in self.attributes:
+            return self.attributes[attribute]
+        elif attribute in self.generic_asset.attributes:
+            return self.generic_asset.attributes[attribute]
+
     def latest_state(
         self,
         source: Optional[
@@ -383,8 +411,8 @@ def data_source_id(cls):  # noqa: B902
     @classmethod
     def make_query(
         cls,
-        asset_class: db.Model,
-        asset_names: Tuple[str],
+        old_sensor_class: db.Model,
+        old_sensor_names: Tuple[str],
         query_window: Tuple[Optional[datetime_type], Optional[datetime_type]],
         belief_horizon_window: Tuple[Optional[timedelta], Optional[timedelta]] = (
             None,
@@ -421,9 +449,11 @@ def make_query(
         if session is None:
             session = db.session
         start, end = query_window
-        query = create_beliefs_query(cls, session, asset_class, asset_names, start, end)
+        query = create_beliefs_query(
+            cls, session, old_sensor_class, old_sensor_names, start, end
+        )
         query = add_belief_timing_filter(
-            cls, query, asset_class, belief_horizon_window, belief_time_window
+            cls, query, old_sensor_class, belief_horizon_window, belief_time_window
         )
         if user_source_ids:
             query = add_user_source_filter(cls, query, user_source_ids)
@@ -440,7 +470,7 @@ def make_query(
     @classmethod
     def collect(
         cls,
-        generic_asset_names: Union[str, List[str]],
+        old_sensor_names: Union[str, List[str]],
         query_window: Tuple[Optional[datetime_type], Optional[datetime_type]] = (
             None,
             None,
@@ -465,7 +495,7 @@ def collect(
         where time series data collection is implemented.
         """
         return collect_time_series_data(
-            generic_asset_names=generic_asset_names,
+            old_sensor_names=old_sensor_names,
             make_query=cls.make_query,
             query_window=query_window,
             belief_horizon_window=belief_horizon_window,
diff --git a/flexmeasures/data/models/utils.py b/flexmeasures/data/models/utils.py
index 34c0d18b1..f45040931 100644
--- a/flexmeasures/data/models/utils.py
+++ b/flexmeasures/data/models/utils.py
@@ -1,31 +1,18 @@
 from typing import Union, Type
 
-from flexmeasures.data.models.assets import AssetType, Asset, Power
-from flexmeasures.data.models.markets import MarketType, Market, Price
-from flexmeasures.data.models.weather import WeatherSensorType, WeatherSensor, Weather
+from flexmeasures.data.models.assets import Asset, Power
+from flexmeasures.data.models.markets import Market, Price
+from flexmeasures.data.models.weather import WeatherSensor, Weather
 
 
-def determine_asset_type_by_asset(
-    generic_asset: Union[Asset, Market, WeatherSensor]
-) -> Union[AssetType, MarketType, WeatherSensorType]:
-    if isinstance(generic_asset, Asset):
-        return generic_asset.asset_type
-    elif isinstance(generic_asset, Market):
-        return generic_asset.market_type
-    elif isinstance(generic_asset, WeatherSensor):
-        return generic_asset.sensor_type
-    else:
-        raise TypeError("Unknown generic asset type.")
-
-
-def determine_asset_value_class_by_asset(
-    generic_asset: Union[Asset, Market, WeatherSensor]
+def determine_old_time_series_class_by_old_sensor(
+    old_sensor: Union[Asset, Market, WeatherSensor]
 ) -> Type[Union[Power, Price, Weather]]:
-    if isinstance(generic_asset, Asset):
+    if isinstance(old_sensor, Asset):
         return Power
-    elif isinstance(generic_asset, Market):
+    elif isinstance(old_sensor, Market):
         return Price
-    elif isinstance(generic_asset, WeatherSensor):
+    elif isinstance(old_sensor, WeatherSensor):
         return Weather
     else:
-        raise TypeError("Unknown generic asset type.")
+        raise TypeError("Unknown old sensor type.")
diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py
index 3d64a61cf..bad08fab1 100644
--- a/flexmeasures/data/models/weather.py
+++ b/flexmeasures/data/models/weather.py
@@ -11,6 +11,7 @@
 from flexmeasures.data.models.time_series import Sensor, TimedValue
 from flexmeasures.data.models.generic_assets import (
     create_generic_asset,
+    GenericAsset,
     GenericAssetType,
 )
 from flexmeasures.utils.geo_utils import parse_lat_lng
@@ -109,6 +110,23 @@ def entity_address(self) -> str:
             "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."""
@@ -213,7 +231,7 @@ class Weather(TimedValue, db.Model):
     @classmethod
     def make_query(cls, **kwargs) -> Query:
         """Construct the database query."""
-        return super().make_query(asset_class=WeatherSensor, **kwargs)
+        return super().make_query(old_sensor_class=WeatherSensor, **kwargs)
 
     def __init__(self, **kwargs):
         super(Weather, self).__init__(**kwargs)
diff --git a/flexmeasures/data/queries/utils.py b/flexmeasures/data/queries/utils.py
index e2c025cba..211752045 100644
--- a/flexmeasures/data/queries/utils.py
+++ b/flexmeasures/data/queries/utils.py
@@ -15,22 +15,22 @@
 def create_beliefs_query(
     cls: "ts.TimedValue",
     session: Session,
-    asset_class: db.Model,
-    asset_names: Tuple[str],
+    old_sensor_class: db.Model,
+    old_sensor_names: Tuple[str],
     start: Optional[datetime],
     end: Optional[datetime],
 ) -> Query:
     query = (
         session.query(
-            asset_class.name, cls.datetime, cls.value, cls.horizon, DataSource
+            old_sensor_class.name, cls.datetime, cls.value, cls.horizon, DataSource
         )
         .join(DataSource)
         .filter(cls.data_source_id == DataSource.id)
-        .join(asset_class)
-        .filter(asset_class.name.in_(asset_names))
+        .join(old_sensor_class)
+        .filter(old_sensor_class.name.in_(old_sensor_names))
     )
     if start is not None:
-        query = query.filter((cls.datetime > start - asset_class.event_resolution))
+        query = query.filter((cls.datetime > start - old_sensor_class.event_resolution))
     if end is not None:
         query = query.filter((cls.datetime < end))
     return query
diff --git a/flexmeasures/data/scripts/data_gen.py b/flexmeasures/data/scripts/data_gen.py
index de1f94a85..bdabb0f08 100644
--- a/flexmeasures/data/scripts/data_gen.py
+++ b/flexmeasures/data/scripts/data_gen.py
@@ -27,6 +27,7 @@
 from flexmeasures.data.models.user import User, Role, RolesUsers
 from flexmeasures.data.models.forecasting import lookup_model_specs_configurator
 from flexmeasures.data.models.forecasting.exceptions import NotEnoughDataException
+from flexmeasures.data.models.utils import determine_old_time_series_class_by_old_sensor
 from flexmeasures.utils.time_utils import ensure_local_timezone
 from flexmeasures.data.transactional import as_transaction
 
@@ -204,8 +205,8 @@ def populate_time_series_forecasts(  # noqa: C901
     forecast_start: datetime,
     forecast_end: datetime,
     event_resolution: Optional[timedelta] = None,
-    generic_asset_type: Optional[str] = None,
-    generic_asset_id: Optional[int] = None,
+    old_sensor_class_name: Optional[str] = None,
+    old_sensor_id: Optional[int] = None,
 ):
     training_and_testing_period = timedelta(days=30)
 
@@ -219,51 +220,54 @@ def populate_time_series_forecasts(  # noqa: C901
         name="Seita", type="demo script"
     ).one_or_none()
 
-    # List all generic assets for which to forecast.
-    # Look into asset type if no asset name is given. If an asset name is given,
-    generic_assets = []
-    if generic_asset_id is None:
-        if generic_asset_type is None or generic_asset_type == "WeatherSensor":
+    # List all old sensors for which to forecast.
+    # Look into their type if no name is given. If a name is given,
+    old_sensors = []
+    if old_sensor_id is None:
+        if old_sensor_class_name is None or old_sensor_class_name == "WeatherSensor":
             sensors = WeatherSensor.query.all()
-            generic_assets.extend(sensors)
-        if generic_asset_type is None or generic_asset_type == "Asset":
+            old_sensors.extend(sensors)
+        if old_sensor_class_name is None or old_sensor_class_name == "Asset":
             assets = Asset.query.all()
-            generic_assets.extend(assets)
-        if generic_asset_type is None or generic_asset_type == "Market":
+            old_sensors.extend(assets)
+        if old_sensor_class_name is None or old_sensor_class_name == "Market":
             markets = Market.query.all()
-            generic_assets.extend(markets)
+            old_sensors.extend(markets)
     else:
-        if generic_asset_type is None:
+        if old_sensor_class_name is None:
             click.echo(
                 "If you specify --asset-name, please also specify --asset-type, so we can look it up."
             )
             return
-        if generic_asset_type == "WeatherSensor":
+        if old_sensor_class_name == "WeatherSensor":
             sensors = WeatherSensor.query.filter(
-                WeatherSensor.id == generic_asset_id
+                WeatherSensor.id == old_sensor_id
             ).one_or_none()
             if sensors is not None:
-                generic_assets.append(sensors)
-        if generic_asset_type == "Asset":
-            assets = Asset.query.filter(Asset.id == generic_asset_id).one_or_none()
+                old_sensors.append(sensors)
+        if old_sensor_class_name == "Asset":
+            assets = Asset.query.filter(Asset.id == old_sensor_id).one_or_none()
             if assets is not None:
-                generic_assets.append(assets)
-        if generic_asset_type == "Market":
-            markets = Market.query.filter(Market.id == generic_asset_id).one_or_none()
+                old_sensors.append(assets)
+        if old_sensor_class_name == "Market":
+            markets = Market.query.filter(Market.id == old_sensor_id).one_or_none()
             if markets is not None:
-                generic_assets.append(markets)
-    if not generic_assets:
+                old_sensors.append(markets)
+    if not old_sensors:
         click.echo("No such assets in db, so I will not add any forecasts.")
         return
 
-    # Make a model for each asset and horizon, make rolling forecasts and save to database.
+    # Make a model for each old sensor and horizon, make rolling forecasts and save to database.
     # We cannot use (faster) bulk save, as forecasts might become regressors in other forecasts.
-    for generic_asset in generic_assets:
+    for old_sensor in old_sensors:
         for horizon in horizons:
             try:
                 default_model = lookup_model_specs_configurator()
                 model_specs, model_identifier, model_fallback = default_model(
-                    generic_asset=generic_asset,
+                    sensor=old_sensor.corresponding_sensor,
+                    time_series_class=determine_old_time_series_class_by_old_sensor(
+                        old_sensor
+                    ),
                     forecast_start=forecast_start,
                     forecast_end=forecast_end,
                     forecast_horizon=horizon,
@@ -277,7 +281,7 @@ def populate_time_series_forecasts(  # noqa: C901
                     "from %s to %s with a training and testing period of %s, using %s ..."
                     % (
                         naturaldelta(horizon),
-                        generic_asset.name,
+                        old_sensor.name,
                         forecast_start,
                         forecast_end,
                         naturaldelta(training_and_testing_period),
@@ -289,15 +293,15 @@ def populate_time_series_forecasts(  # noqa: C901
                     start=forecast_start, end=forecast_end, model_specs=model_specs
                 )
                 # Upsample to sensor resolution if needed
-                if forecasts.index.freq > pd.Timedelta(generic_asset.event_resolution):
+                if forecasts.index.freq > pd.Timedelta(old_sensor.event_resolution):
                     forecasts = model_specs.outcome_var.resample_data(
                         forecasts,
                         time_window=(forecasts.index.min(), forecasts.index.max()),
-                        expected_frequency=generic_asset.event_resolution,
+                        expected_frequency=old_sensor.event_resolution,
                     )
             except (NotEnoughDataException, MissingData, NaNData) as e:
                 click.echo(
-                    "Skipping forecasts for asset %s: %s" % (generic_asset, str(e))
+                    "Skipping forecasts for old sensor %s: %s" % (old_sensor, str(e))
                 )
                 continue
             """
@@ -314,35 +318,35 @@ def populate_time_series_forecasts(  # noqa: C901
             """
 
             beliefs = []
-            if isinstance(generic_asset, Asset):
+            if isinstance(old_sensor, Asset):
                 beliefs = [
                     Power(
                         datetime=ensure_local_timezone(dt, tz_name=LOCAL_TIME_ZONE),
                         horizon=horizon,
                         value=value,
-                        asset_id=generic_asset.id,
+                        asset_id=old_sensor.id,
                         data_source_id=data_source.id,
                     )
                     for dt, value in forecasts.items()
                 ]
-            elif isinstance(generic_asset, Market):
+            elif isinstance(old_sensor, Market):
                 beliefs = [
                     Price(
                         datetime=ensure_local_timezone(dt, tz_name=LOCAL_TIME_ZONE),
                         horizon=horizon,
                         value=value,
-                        market_id=generic_asset.id,
+                        market_id=old_sensor.id,
                         data_source_id=data_source.id,
                     )
                     for dt, value in forecasts.items()
                 ]
-            elif isinstance(generic_asset, WeatherSensor):
+            elif isinstance(old_sensor, WeatherSensor):
                 beliefs = [
                     Weather(
                         datetime=ensure_local_timezone(dt, tz_name=LOCAL_TIME_ZONE),
                         horizon=horizon,
                         value=value,
-                        sensor_id=generic_asset.id,
+                        sensor_id=old_sensor.id,
                         data_source_id=data_source.id,
                     )
                     for dt, value in forecasts.items()
@@ -350,7 +354,7 @@ def populate_time_series_forecasts(  # noqa: C901
 
             print(
                 "Saving %s %s-forecasts for %s..."
-                % (len(beliefs), naturaldelta(horizon), generic_asset.name)
+                % (len(beliefs), naturaldelta(horizon), old_sensor.name)
             )
             for belief in beliefs:
                 db.session.add(belief)
@@ -403,8 +407,8 @@ def depopulate_structure(db: SQLAlchemy):
 @as_transaction
 def depopulate_measurements(
     db: SQLAlchemy,
-    generic_asset_type: Optional[str] = None,
-    generic_asset_id: Optional[id] = None,
+    old_sensor_class_name: Optional[str] = None,
+    old_sensor_id: Optional[id] = None,
 ):
     click.echo("Depopulating (time series) data from the database %s ..." % db.engine)
     num_prices_deleted = 0
@@ -413,35 +417,35 @@ def depopulate_measurements(
 
     # TODO: simplify this when sensors moved to one unified table
 
-    if generic_asset_id is None:
-        if generic_asset_type is None or generic_asset_type == "Market":
+    if old_sensor_id is None:
+        if old_sensor_class_name is None or old_sensor_class_name == "Market":
             num_prices_deleted = (
                 db.session.query(Price)
                 .filter(Price.horizon <= timedelta(hours=0))
                 .delete()
             )
-        if generic_asset_type is None or generic_asset_type == "Asset":
+        if old_sensor_class_name is None or old_sensor_class_name == "Asset":
             num_power_measurements_deleted = (
                 db.session.query(Power)
                 .filter(Power.horizon <= timedelta(hours=0))
                 .delete()
             )
-        if generic_asset_type is None or generic_asset_type == "WeatherSensor":
+        if old_sensor_class_name is None or old_sensor_class_name == "WeatherSensor":
             num_weather_measurements_deleted = (
                 db.session.query(Weather)
                 .filter(Weather.horizon <= timedelta(hours=0))
                 .delete()
             )
     else:
-        if generic_asset_type is None:
+        if old_sensor_class_name is None:
             click.echo(
                 "If you specify --asset-name, please also specify --asset-type, so we can look it up."
             )
             return
-        if generic_asset_type == "Market":
+        if old_sensor_class_name == "Market":
             market = (
                 db.session.query(Market)
-                .filter(Market.id == generic_asset_id)
+                .filter(Market.id == old_sensor_id)
                 .one_or_none()
             )
             if market is not None:
@@ -454,11 +458,9 @@ def depopulate_measurements(
             else:
                 num_prices_deleted = 0
 
-        elif generic_asset_type == "Asset":
+        elif old_sensor_class_name == "Asset":
             asset = (
-                db.session.query(Asset)
-                .filter(Asset.id == generic_asset_id)
-                .one_or_none()
+                db.session.query(Asset).filter(Asset.id == old_sensor_id).one_or_none()
             )
             if asset is not None:
                 num_power_measurements_deleted = (
@@ -470,10 +472,10 @@ def depopulate_measurements(
             else:
                 num_power_measurements_deleted = 0
 
-        elif generic_asset_type == "WeatherSensor":
+        elif old_sensor_class_name == "WeatherSensor":
             sensor = (
                 db.session.query(WeatherSensor)
-                .filter(WeatherSensor.id == generic_asset_id)
+                .filter(WeatherSensor.id == old_sensor_id)
                 .one_or_none()
             )
             if sensor is not None:
@@ -494,8 +496,8 @@ def depopulate_measurements(
 @as_transaction
 def depopulate_prognoses(
     db: SQLAlchemy,
-    generic_asset_type: Optional[str] = None,
-    generic_asset_id: Optional[id] = None,
+    old_sensor_class_name: Optional[str] = None,
+    old_sensor_id: Optional[id] = None,
 ):
     click.echo(
         "Depopulating (time series) forecasts and schedules data from the database %s ..."
@@ -510,20 +512,20 @@ def depopulate_prognoses(
     num_scheduling_jobs_deleted = app.queues["scheduling"].empty()
 
     # Clear all forecasts (data with positive horizon)
-    if generic_asset_id is None:
-        if generic_asset_type is None or generic_asset_type == "Market":
+    if old_sensor_id is None:
+        if old_sensor_class_name is None or old_sensor_class_name == "Market":
             num_prices_deleted = (
                 db.session.query(Price)
                 .filter(Price.horizon > timedelta(hours=0))
                 .delete()
             )
-        if generic_asset_type is None or generic_asset_type == "Asset":
+        if old_sensor_class_name is None or old_sensor_class_name == "Asset":
             num_power_measurements_deleted = (
                 db.session.query(Power)
                 .filter(Power.horizon > timedelta(hours=0))
                 .delete()
             )
-        if generic_asset_type is None or generic_asset_type == "WeatherSensor":
+        if old_sensor_class_name is None or old_sensor_class_name == "WeatherSensor":
             num_weather_measurements_deleted = (
                 db.session.query(Weather)
                 .filter(Weather.horizon > timedelta(hours=0))
@@ -532,13 +534,13 @@ def depopulate_prognoses(
     else:
         click.echo(
             "Depopulating (time series) forecasts and schedules for %s from the database %s ..."
-            % (generic_asset_id, db.engine)
+            % (old_sensor_id, db.engine)
         )
 
-        if generic_asset_type == "Market":
+        if old_sensor_class_name == "Market":
             market = (
                 db.session.query(Market)
-                .filter(Market.id == generic_asset_id)
+                .filter(Market.id == old_sensor_id)
                 .one_or_none()
             )
             if market is not None:
@@ -551,11 +553,9 @@ def depopulate_prognoses(
             else:
                 num_prices_deleted = 0
 
-        if generic_asset_type == "Asset":
+        if old_sensor_class_name == "Asset":
             asset = (
-                db.session.query(Asset)
-                .filter(Asset.id == generic_asset_id)
-                .one_or_none()
+                db.session.query(Asset).filter(Asset.id == old_sensor_id).one_or_none()
             )
             if asset is not None:
                 num_power_measurements_deleted = (
@@ -567,10 +567,10 @@ def depopulate_prognoses(
             else:
                 num_power_measurements_deleted = 0
 
-        if generic_asset_type == "WeatherSensor":
+        if old_sensor_class_name == "WeatherSensor":
             sensor = (
                 db.session.query(WeatherSensor)
-                .filter(WeatherSensor.id == generic_asset_id)
+                .filter(WeatherSensor.id == old_sensor_id)
                 .one_or_none()
             )
             if sensor is not None:
diff --git a/flexmeasures/data/services/forecasting.py b/flexmeasures/data/services/forecasting.py
index 230d914cd..a0d98c303 100644
--- a/flexmeasures/data/services/forecasting.py
+++ b/flexmeasures/data/services/forecasting.py
@@ -13,7 +13,7 @@
 from flexmeasures.data.models.forecasting import lookup_model_specs_configurator
 from flexmeasures.data.models.forecasting.exceptions import InvalidHorizonException
 from flexmeasures.data.models.markets import Market, Price
-from flexmeasures.data.models.utils import determine_asset_value_class_by_asset
+from flexmeasures.data.models.utils import determine_old_time_series_class_by_old_sensor
 from flexmeasures.data.models.forecasting.utils import (
     get_query_window,
     check_data_availability,
@@ -47,7 +47,7 @@ class MisconfiguredForecastingJobException(Exception):
 
 def create_forecasting_jobs(
     timed_value_type: str,
-    asset_id: int,
+    old_sensor_id: int,
     start_of_roll: datetime,
     end_of_roll: datetime,
     resolution: timedelta = None,
@@ -100,7 +100,7 @@ def create_forecasting_jobs(
         job = Job.create(
             make_rolling_viewpoint_forecasts,
             kwargs=dict(
-                asset_id=asset_id,
+                old_sensor_id=old_sensor_id,
                 timed_value_type=timed_value_type,
                 horizon=horizon,
                 start=start_of_roll + horizon,
@@ -123,7 +123,7 @@ def create_forecasting_jobs(
 
 
 def make_fixed_viewpoint_forecasts(
-    asset_id: int,
+    old_sensor_id: int,
     timed_value_type: str,
     horizon: timedelta,
     start: datetime,
@@ -141,7 +141,7 @@ def make_fixed_viewpoint_forecasts(
 
 
 def make_rolling_viewpoint_forecasts(
-    asset_id: int,
+    old_sensor_id: int,
     timed_value_type: str,
     horizon: timedelta,
     start: datetime,
@@ -157,8 +157,8 @@ def make_rolling_viewpoint_forecasts(
 
     Parameters
     ----------
-    :param asset_id: int
-        To identify which asset to forecast
+    :param old_sensor_id: int
+        To identify which old sensor to forecast (note: old_sensor_id == sensor_id)
     :param timed_value_type: str
         This should go away after a refactoring - we now use it to create the DB entry for the forecasts
     :param horizon: timedelta
@@ -181,15 +181,15 @@ def make_rolling_viewpoint_forecasts(
     # find out which model to run, fall back to latest recommended
     model_search_term = rq_job.meta.get("model_search_term", "linear-OLS")
 
-    # find asset
-    asset = get_asset(asset_id, timed_value_type)
+    # find old sensor
+    old_sensor = get_old_sensor(old_sensor_id, timed_value_type)
 
     click.echo(
         "Running Forecasting Job %s: %s for %s on model '%s', from %s to %s"
-        % (rq_job.id, asset, horizon, model_search_term, start, end)
+        % (rq_job.id, old_sensor, horizon, model_search_term, start, end)
     )
 
-    if hasattr(asset, "market_type"):
+    if hasattr(old_sensor, "market_type"):
         ex_post_horizon = None  # Todo: until we sorted out the ex_post_horizon, use all available price data
     else:
         ex_post_horizon = timedelta(hours=0)
@@ -197,7 +197,8 @@ def make_rolling_viewpoint_forecasts(
     # Make model specs
     model_configurator = lookup_model_specs_configurator(model_search_term)
     model_specs, model_identifier, fallback_model_search_term = model_configurator(
-        generic_asset=asset,
+        sensor=old_sensor.corresponding_sensor,
+        time_series_class=determine_old_time_series_class_by_old_sensor(old_sensor),
         forecast_start=as_server_time(start),
         forecast_end=as_server_time(end),
         forecast_horizon=horizon,
@@ -222,8 +223,8 @@ def make_rolling_viewpoint_forecasts(
         [lag * model_specs.frequency for lag in model_specs.lags],
     )
     check_data_availability(
-        asset,
-        determine_asset_value_class_by_asset(asset),
+        old_sensor,
+        determine_old_time_series_class_by_old_sensor(old_sensor),
         start,
         end,
         query_window,
@@ -244,7 +245,9 @@ def make_rolling_viewpoint_forecasts(
     click.echo("Job %s made %d forecasts." % (rq_job.id, len(forecasts)))
 
     ts_value_forecasts = [
-        make_timed_value(timed_value_type, asset_id, dt, value, horizon, data_source.id)
+        make_timed_value(
+            timed_value_type, old_sensor_id, dt, value, horizon, data_source.id
+        )
         for dt, value in forecasts.items()
     ]
 
@@ -308,44 +311,48 @@ def num_forecasts(start: datetime, end: datetime, resolution: timedelta) -> int:
 #       and store everything in one time series database.
 
 
-def get_asset(
-    asset_id: int, timed_value_type: str
+def get_old_sensor(
+    old_sensor_id: int, timed_value_type: str
 ) -> Union[Asset, Market, WeatherSensor]:
-    """Get asset for this job. Maybe simpler once we redesign timed value classes (make a generic one)"""
+    """Get old sensor for this job. Maybe simpler once we redesign timed value classes (make a generic one)"""
     if timed_value_type not in ("Power", "Price", "Weather"):
-        raise Exception("Cannot get asset for asset_type '%s'" % timed_value_type)
-    asset = None
+        raise Exception(
+            "Cannot get old sensor for timed_value_type '%s'" % timed_value_type
+        )
+    old_sensor = None
     if timed_value_type == "Power":
-        asset = Asset.query.filter_by(id=asset_id).one_or_none()
+        old_sensor = Asset.query.filter_by(id=old_sensor_id).one_or_none()
     elif timed_value_type == "Price":
-        asset = Market.query.filter_by(id=asset_id).one_or_none()
+        old_sensor = Market.query.filter_by(id=old_sensor_id).one_or_none()
     elif timed_value_type == "Weather":
-        asset = WeatherSensor.query.filter_by(id=asset_id).one_or_none()
-    if asset is None:
+        old_sensor = WeatherSensor.query.filter_by(id=old_sensor_id).one_or_none()
+    if old_sensor is None:
         raise Exception(
-            "Cannot find asset for value type %s with id %d"
-            % (timed_value_type, asset_id)
+            "Cannot find old sensor for value type %s with id %d"
+            % (timed_value_type, old_sensor_id)
         )
-    return asset
+    return old_sensor
 
 
 def make_timed_value(
     timed_value_type: str,
-    asset_id: int,
+    old_sensor_id: int,
     dt: datetime,
     value: float,
     horizon: timedelta,
     data_source_id: int,
 ) -> Union[Power, Price, Weather]:
     if timed_value_type not in ("Power", "Price", "Weather"):
-        raise Exception("Cannot get asset for asset_type '%s'" % timed_value_type)
+        raise Exception(
+            "Cannot get old sensor for timed_value_type '%s'" % timed_value_type
+        )
     ts_value = None
     if timed_value_type == "Power":
         ts_value = Power(
             datetime=dt,
             horizon=horizon,
             value=value,
-            asset_id=asset_id,
+            asset_id=old_sensor_id,
             data_source_id=data_source_id,
         )
     elif timed_value_type == "Price":
@@ -353,7 +360,7 @@ def make_timed_value(
             datetime=dt,
             horizon=horizon,
             value=value,
-            market_id=asset_id,
+            market_id=old_sensor_id,
             data_source_id=data_source_id,
         )
     elif timed_value_type == "Weather":
@@ -361,11 +368,12 @@ def make_timed_value(
             datetime=dt,
             horizon=horizon,
             value=value,
-            sensor_id=asset_id,
+            sensor_id=old_sensor_id,
             data_source_id=data_source_id,
         )
     if ts_value is None:
         raise Exception(
-            "Cannot create asset of type %s with id %d" % (timed_value_type, asset_id)
+            "Cannot create timed value of type %s with id %d"
+            % (timed_value_type, old_sensor_id)
         )
     return ts_value
diff --git a/flexmeasures/data/services/resources.py b/flexmeasures/data/services/resources.py
index 13d68af77..c73490a37 100644
--- a/flexmeasures/data/services/resources.py
+++ b/flexmeasures/data/services/resources.py
@@ -450,7 +450,7 @@ def load_sensor_data(
 
             # Query the sensors
             resource_data: Dict[str, tb.BeliefsDataFrame] = sensor_type.collect(
-                generic_asset_names=list(names_of_resource_sensors),
+                old_sensor_names=list(names_of_resource_sensors),
                 query_window=(start, end),
                 belief_horizon_window=belief_horizon_window,
                 belief_time_window=belief_time_window,
@@ -630,7 +630,7 @@ def find_closest_weather_sensor(
 
     Can be called with an object that has latitude and longitude properties, for example:
 
-        sensor = find_closest_weather_sensor("wind_speed", object=asset)
+        sensor = find_closest_weather_sensor("wind_speed", object=generic_asset)
 
     Can also be called with latitude and longitude parameters, for example:
 
diff --git a/flexmeasures/data/services/scheduling.py b/flexmeasures/data/services/scheduling.py
index 69a7e13ed..d94f88baa 100644
--- a/flexmeasures/data/services/scheduling.py
+++ b/flexmeasures/data/services/scheduling.py
@@ -11,9 +11,10 @@
 from sqlalchemy.exc import IntegrityError
 
 from flexmeasures.data.config import db
-from flexmeasures.data.models.assets import Asset, Power
+from flexmeasures.data.models.assets import Power
 from flexmeasures.data.models.planning.battery import schedule_battery
 from flexmeasures.data.models.planning.charging_station import schedule_charging_station
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.utils import save_to_session, get_data_source
 
 """
@@ -99,16 +100,19 @@ def make_schedule(
 
     rq_job = get_current_job()
 
-    # find asset
-    asset = Asset.query.filter_by(id=asset_id).one_or_none()
+    # find sensor
+    sensor = Sensor.query.filter_by(id=asset_id).one_or_none()
 
     click.echo(
-        "Running Scheduling Job %s: %s, from %s to %s" % (rq_job.id, asset, start, end)
+        "Running Scheduling Job %s: %s, from %s to %s" % (rq_job.id, sensor, start, end)
     )
 
     if soc_at_start is None:
-        if start == asset.soc_datetime and asset.soc_in_mwh is not None:
-            soc_at_start = asset.soc_in_mwh
+        if (
+            start == sensor.get_attribute("soc_datetime")
+            and sensor.get_attribute("soc_in_mwh") is not None
+        ):
+            soc_at_start = sensor.get_attribute("soc_in_mwh")
         else:
             soc_at_start = 0
 
@@ -117,20 +121,21 @@ def make_schedule(
             np.nan, index=pd.date_range(start, end, freq=resolution, closed="right")
         )
 
-    if asset.asset_type_name == "battery":
+    if sensor.generic_asset.generic_asset_type.name == "battery":
         consumption_schedule = schedule_battery(
-            asset, asset.market, start, end, resolution, soc_at_start, soc_targets
+            sensor, start, end, resolution, soc_at_start, soc_targets
         )
-    elif asset.asset_type_name in (
+    elif sensor.generic_asset.generic_asset_type.name in (
         "one-way_evse",
         "two-way_evse",
     ):
         consumption_schedule = schedule_charging_station(
-            asset, asset.market, start, end, resolution, soc_at_start, soc_targets
+            sensor, start, end, resolution, soc_at_start, soc_targets
         )
     else:
         raise ValueError(
-            "Scheduling is not (yet) supported for asset type %s." % asset.asset_type
+            "Scheduling is not (yet) supported for asset type %s."
+            % sensor.generic_asset.generic_asset_type
         )
 
     data_source = get_data_source(
diff --git a/flexmeasures/data/services/time_series.py b/flexmeasures/data/services/time_series.py
index aa8e4408c..e889f61c7 100644
--- a/flexmeasures/data/services/time_series.py
+++ b/flexmeasures/data/services/time_series.py
@@ -33,7 +33,7 @@
 
 
 def collect_time_series_data(
-    generic_asset_names: Union[str, List[str]],
+    old_sensor_names: Union[str, List[str]],
     make_query: QueryCallType,
     query_window: Tuple[Optional[datetime], Optional[datetime]] = (None, None),
     belief_horizon_window: Tuple[Optional[timedelta], Optional[timedelta]] = (
@@ -48,12 +48,12 @@ def collect_time_series_data(
     resolution: Union[str, timedelta] = None,
     sum_multiple: bool = True,
 ) -> Union[tb.BeliefsDataFrame, Dict[str, tb.BeliefsDataFrame]]:
-    """Get time series data from one or more generic assets and rescale and re-package it to order.
+    """Get time series data from one or more old sensor models and rescale and re-package it to order.
 
     We can (lazily) look up by pickle, or load from the database.
     In the latter case, we are relying on time series data (power measurements and prices at this point) to
     have the same relevant column names (datetime, value).
-    We require a list of assets or market names to find the generic asset.
+    We require an old sensor model name of list thereof.
     If the time range parameters are None, they will be gotten from the session.
     Response is a 2D BeliefsDataFrame with the column event_value.
     If data from multiple assets is retrieved, the results are being summed.
@@ -64,13 +64,13 @@ def collect_time_series_data(
     """
 
     # convert to tuple to support caching the query
-    if isinstance(generic_asset_names, str):
-        generic_asset_names = (generic_asset_names,)
-    elif isinstance(generic_asset_names, list):
-        generic_asset_names = tuple(generic_asset_names)
+    if isinstance(old_sensor_names, str):
+        old_sensor_names = (old_sensor_names,)
+    elif isinstance(old_sensor_names, list):
+        old_sensor_names = tuple(old_sensor_names)
 
     bdf_dict = query_time_series_data(
-        generic_asset_names,
+        old_sensor_names,
         make_query,
         query_window,
         belief_horizon_window,
@@ -89,7 +89,7 @@ def collect_time_series_data(
 
 
 def query_time_series_data(
-    generic_asset_names: Tuple[str],
+    old_sensor_names: Tuple[str],
     make_query: QueryCallType,
     query_window: Tuple[Optional[datetime], Optional[datetime]] = (None, None),
     belief_horizon_window: Tuple[Optional[timedelta], Optional[timedelta]] = (
@@ -121,7 +121,7 @@ def query_time_series_data(
         query_window = convert_query_window_for_demo(query_window)
 
     query = make_query(
-        asset_names=generic_asset_names,
+        old_sensor_names=old_sensor_names,
         query_window=query_window,
         belief_horizon_window=belief_horizon_window,
         belief_time_window=belief_time_window,
@@ -135,10 +135,10 @@ def query_time_series_data(
         query.all(), columns=[col["name"] for col in query.column_descriptions]
     )
     bdf_dict: Dict[str, tb.BeliefsDataFrame] = {}
-    for generic_asset_name in generic_asset_names:
+    for old_sensor_model_name in old_sensor_names:
 
         # Select data for the given asset
-        df = df_all_assets[df_all_assets["name"] == generic_asset_name].loc[
+        df = df_all_assets[df_all_assets["name"] == old_sensor_model_name].loc[
             :, df_all_assets.columns != "name"
         ]
 
@@ -182,7 +182,7 @@ def query_time_series_data(
         if current_app.config.get("FLEXMEASURES_MODE", "") == "demo":
             df.index = df.index.map(lambda t: t.replace(year=datetime.now().year))
 
-        sensor = find_sensor_by_name(name=generic_asset_name)
+        sensor = find_sensor_by_name(name=old_sensor_model_name)
         bdf = tb.BeliefsDataFrame(df.reset_index(), sensor=sensor)
 
         # re-sample data to the resolution we need to serve
@@ -205,7 +205,7 @@ def query_time_series_data(
         if query_window[1] is not None:
             bdf = bdf[bdf.index.get_level_values("event_start") < query_window[1]]
 
-        bdf_dict[generic_asset_name] = bdf
+        bdf_dict[old_sensor_model_name] = bdf
 
     return bdf_dict
 


Test patch:
diff --git a/flexmeasures/api/v1/tests/test_api_v1_fresh_db.py b/flexmeasures/api/v1/tests/test_api_v1_fresh_db.py
index c7441123d..e92749927 100644
--- a/flexmeasures/api/v1/tests/test_api_v1_fresh_db.py
+++ b/flexmeasures/api/v1/tests/test_api_v1_fresh_db.py
@@ -73,7 +73,7 @@ def test_post_and_get_meter_data(
     for asset_name in ("CS 1", "CS 2", "CS 3"):
         if asset_name in str(post_message):
             asset = Asset.query.filter_by(name=asset_name).one_or_none()
-            assert asset.id in [job.kwargs["asset_id"] for job in jobs]
+            assert asset.id in [job.kwargs["old_sensor_id"] for job in jobs]
 
     # get meter data
     get_meter_data_response = client.get(
diff --git a/flexmeasures/api/v1_1/tests/test_api_v1_1.py b/flexmeasures/api/v1_1/tests/test_api_v1_1.py
index 0504664fe..6d2fbd9cf 100644
--- a/flexmeasures/api/v1_1/tests/test_api_v1_1.py
+++ b/flexmeasures/api/v1_1/tests/test_api_v1_1.py
@@ -155,7 +155,7 @@ def test_post_price_data(setup_api_test_data, db, app, clean_redis, post_message
         assert job.kwargs["horizon"] == horizon
         assert job.kwargs["start"] == parse_date(post_message["start"]) + horizon
         assert job.kwargs["timed_value_type"] == "Price"
-        assert job.kwargs["asset_id"] == market.id
+        assert job.kwargs["old_sensor_id"] == market.id
 
 
 @pytest.mark.parametrize(
diff --git a/flexmeasures/api/v2_0/tests/test_api_v2_0_sensors_fresh_db.py b/flexmeasures/api/v2_0/tests/test_api_v2_0_sensors_fresh_db.py
index c87a6cda6..4b7adcbf4 100644
--- a/flexmeasures/api/v2_0/tests/test_api_v2_0_sensors_fresh_db.py
+++ b/flexmeasures/api/v2_0/tests/test_api_v2_0_sensors_fresh_db.py
@@ -60,4 +60,4 @@ def test_post_price_data_2_0(
         assert job.kwargs["horizon"] == horizon
         assert job.kwargs["start"] == parse_date(post_message["start"]) + horizon
         assert job.kwargs["timed_value_type"] == "Price"
-        assert job.kwargs["asset_id"] == market.id
+        assert job.kwargs["old_sensor_id"] == market.id
diff --git a/flexmeasures/cli/testing.py b/flexmeasures/cli/testing.py
index 72011a011..f9ef8bdc5 100644
--- a/flexmeasures/cli/testing.py
+++ b/flexmeasures/cli/testing.py
@@ -16,6 +16,7 @@
 from flexmeasures.data.models.markets import Market
 from flexmeasures.data.models.weather import WeatherSensor
 from flexmeasures.data.models.forecasting import lookup_model_specs_configurator
+from flexmeasures.data.models.utils import determine_old_time_series_class_by_old_sensor
 from flexmeasures.utils.time_utils import as_server_time
 from flexmeasures.data.services.forecasting import (
     create_forecasting_jobs,
@@ -52,7 +53,7 @@ def test_making_forecasts():
     click.echo("Forecasts found before : %d" % forecast_filter.count())
 
     create_forecasting_jobs(
-        asset_id=asset_id,
+        old_sensor_id=asset_id,
         timed_value_type="Power",
         horizons=[timedelta(hours=6)],
         start_of_roll=as_server_time(datetime(2015, 4, 1)),
@@ -79,7 +80,7 @@ def test_making_forecasts():
 # un-comment to use as CLI function
 # @app.cli.command()
 @click.option("--asset-type", help="Asset type name.")
-@click.option("--asset", help="Asset name.")
+@click.option("--asset", "asset_name", help="Asset name.")
 @click.option(
     "--from_date",
     default="2015-03-10",
@@ -92,7 +93,7 @@ def test_making_forecasts():
 )
 def test_generic_model(
     asset_type: str,
-    asset: Optional[str] = None,
+    asset_name: Optional[str] = None,
     from_date: str = "2015-03-10",
     period: int = 3,
     horizon_hours: int = 1,
@@ -101,10 +102,8 @@ def test_generic_model(
     """Manually test integration of timetomodel for our generic model."""
 
     asset_type_name = asset_type
-    if asset is None:
+    if asset_name is None:
         asset_name = Asset.query.filter_by(asset_type_name=asset_type_name).first().name
-    else:
-        asset_name = asset
     start = as_server_time(datetime.strptime(from_date, "%Y-%m-%d"))
     end = start + timedelta(days=period)
     training_and_testing_period = timedelta(days=training)
@@ -127,11 +126,11 @@ def test_generic_model(
             .first()
         )
         if asset:
-            generic_asset = asset
+            old_sensor = asset
         elif market:
-            generic_asset = market
+            old_sensor = market
         elif sensor:
-            generic_asset = sensor
+            old_sensor = sensor
         else:
             click.echo("No such assets in db, so I will not add any forecasts.")
             return
@@ -142,7 +141,8 @@ def test_generic_model(
             model_identifier,
             fallback_model_identifier,
         ) = linear_model_configurator(
-            generic_asset=generic_asset,
+            sensor=old_sensor.corresponding_sensor,
+            time_series_class=determine_old_time_series_class_by_old_sensor(old_sensor),
             forecast_start=start,
             forecast_end=end,
             forecast_horizon=horizon,
diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py
index fd832b586..cdbc1191a 100644
--- a/flexmeasures/data/models/planning/tests/test_solver.py
+++ b/flexmeasures/data/models/planning/tests/test_solver.py
@@ -4,8 +4,8 @@
 import numpy as np
 import pandas as pd
 
-from flexmeasures.data.models.assets import Asset
 from flexmeasures.data.models.markets import Market
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.models.planning.battery import schedule_battery
 from flexmeasures.data.models.planning.charging_station import schedule_charging_station
 from flexmeasures.utils.calculations import integrate_time_series
@@ -14,54 +14,56 @@
 
 def test_battery_solver_day_1(add_battery_assets):
     epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
-    battery = Asset.query.filter(Asset.name == "Test battery").one_or_none()
+    battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none()
+    assert Market.query.get(battery.get_attribute("market_id")) == epex_da
     start = as_server_time(datetime(2015, 1, 1))
     end = as_server_time(datetime(2015, 1, 2))
     resolution = timedelta(minutes=15)
-    soc_at_start = battery.soc_in_mwh
-    schedule = schedule_battery(battery, epex_da, start, end, resolution, soc_at_start)
+    soc_at_start = battery.get_attribute("soc_in_mwh")
+    schedule = schedule_battery(battery, start, end, resolution, soc_at_start)
     soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6)
 
     with pd.option_context("display.max_rows", None, "display.max_columns", 3):
         print(soc_schedule)
 
     # Check if constraints were met
-    assert min(schedule.values) >= battery.capacity_in_mw * -1
-    assert max(schedule.values) <= battery.capacity_in_mw
+    assert min(schedule.values) >= battery.get_attribute("capacity_in_mw") * -1
+    assert max(schedule.values) <= battery.get_attribute("capacity_in_mw")
     for soc in soc_schedule.values:
-        assert soc >= battery.min_soc_in_mwh
-        assert soc <= battery.max_soc_in_mwh
+        assert soc >= battery.get_attribute("min_soc_in_mwh")
+        assert soc <= battery.get_attribute("max_soc_in_mwh")
 
 
 def test_battery_solver_day_2(add_battery_assets):
     epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
-    battery = Asset.query.filter(Asset.name == "Test battery").one_or_none()
+    battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none()
+    assert Market.query.get(battery.get_attribute("market_id")) == epex_da
     start = as_server_time(datetime(2015, 1, 2))
     end = as_server_time(datetime(2015, 1, 3))
     resolution = timedelta(minutes=15)
-    soc_at_start = battery.soc_in_mwh
-    schedule = schedule_battery(battery, epex_da, start, end, resolution, soc_at_start)
+    soc_at_start = battery.get_attribute("soc_in_mwh")
+    schedule = schedule_battery(battery, start, end, resolution, soc_at_start)
     soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6)
 
     with pd.option_context("display.max_rows", None, "display.max_columns", 3):
         print(soc_schedule)
 
     # Check if constraints were met
-    assert min(schedule.values) >= battery.capacity_in_mw * -1
-    assert max(schedule.values) <= battery.capacity_in_mw
+    assert min(schedule.values) >= battery.get_attribute("capacity_in_mw") * -1
+    assert max(schedule.values) <= battery.get_attribute("capacity_in_mw")
     for soc in soc_schedule.values:
-        assert soc >= battery.min_soc_in_mwh
-        assert soc <= battery.max_soc_in_mwh
+        assert soc >= battery.get_attribute("min_soc_in_mwh")
+        assert soc <= battery.get_attribute("max_soc_in_mwh")
 
     # Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours
-    assert (
-        soc_schedule.iloc[-1] == battery.min_soc_in_mwh
+    assert soc_schedule.iloc[-1] == battery.get_attribute(
+        "min_soc_in_mwh"
     )  # Battery sold out at the end of its planning horizon
-    assert (
-        soc_schedule.loc[start + timedelta(hours=8)] == battery.min_soc_in_mwh
+    assert soc_schedule.loc[start + timedelta(hours=8)] == battery.get_attribute(
+        "min_soc_in_mwh"
     )  # Sell what you begin with
-    assert (
-        soc_schedule.loc[start + timedelta(hours=16)] == battery.max_soc_in_mwh
+    assert soc_schedule.loc[start + timedelta(hours=16)] == battery.get_attribute(
+        "max_soc_in_mwh"
     )  # Buy what you can to sell later
 
 
@@ -82,9 +84,10 @@ def test_charging_station_solver_day_2(target_soc, charging_station_name):
     duration_until_target = timedelta(hours=2)
 
     epex_da = Market.query.filter(Market.name == "epex_da").one_or_none()
-    charging_station = Asset.query.filter(
-        Asset.name == charging_station_name
+    charging_station = Sensor.query.filter(
+        Sensor.name == charging_station_name
     ).one_or_none()
+    assert Market.query.get(charging_station.get_attribute("market_id")) == epex_da
     start = as_server_time(datetime(2015, 1, 2))
     end = as_server_time(datetime(2015, 1, 3))
     resolution = timedelta(minutes=15)
@@ -94,15 +97,20 @@ def test_charging_station_solver_day_2(target_soc, charging_station_name):
     )
     soc_targets.loc[target_soc_datetime] = target_soc
     consumption_schedule = schedule_charging_station(
-        charging_station, epex_da, start, end, resolution, soc_at_start, soc_targets
+        charging_station, start, end, resolution, soc_at_start, soc_targets
     )
     soc_schedule = integrate_time_series(
         consumption_schedule, soc_at_start, decimal_precision=6
     )
 
     # Check if constraints were met
-    assert min(consumption_schedule.values) >= charging_station.capacity_in_mw * -1
-    assert max(consumption_schedule.values) <= charging_station.capacity_in_mw
+    assert (
+        min(consumption_schedule.values)
+        >= charging_station.get_attribute("capacity_in_mw") * -1
+    )
+    assert max(consumption_schedule.values) <= charging_station.get_attribute(
+        "capacity_in_mw"
+    )
     print(consumption_schedule.head(12))
     print(soc_schedule.head(12))
     assert abs(soc_schedule.loc[target_soc_datetime] - target_soc) < 0.00001
diff --git a/flexmeasures/data/tests/test_forecasting_jobs.py b/flexmeasures/data/tests/test_forecasting_jobs.py
index 9c6f9c6a4..f9be87b72 100644
--- a/flexmeasures/data/tests/test_forecasting_jobs.py
+++ b/flexmeasures/data/tests/test_forecasting_jobs.py
@@ -62,7 +62,7 @@ def test_forecasting_an_hour_of_wind(db, app, setup_test_data):
         start_of_roll=as_server_time(datetime(2015, 1, 1, 6)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, 7)),
         horizons=[horizon],
-        asset_id=wind_device_1.id,
+        old_sensor_id=wind_device_1.id,
         custom_model_params=custom_model_params(),
     )
 
@@ -111,7 +111,7 @@ def test_forecasting_two_hours_of_solar_at_edge_of_data_set(db, app, setup_test_
         horizons=[
             timedelta(hours=6)
         ],  # so we want forecasts for 11.15pm (Jan 1st) to 0.15am (Jan 2nd)
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         custom_model_params=custom_model_params(),
     )
     print("Job: %s" % job[0].id)
@@ -179,7 +179,7 @@ def test_failed_forecasting_insufficient_data(app, clean_redis, setup_test_data)
         start_of_roll=as_server_time(datetime(2016, 1, 1, 20)),
         end_of_roll=as_server_time(datetime(2016, 1, 1, 22)),
         horizons=[timedelta(hours=1)],
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         custom_model_params=custom_model_params(),
     )
     work_on_rq(app.queues["forecasting"], exc_handler=handle_forecasting_exception)
@@ -194,7 +194,7 @@ def test_failed_forecasting_invalid_horizon(app, clean_redis, setup_test_data):
         start_of_roll=as_server_time(datetime(2015, 1, 1, 21)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, 23)),
         horizons=[timedelta(hours=18)],
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         custom_model_params=custom_model_params(),
     )
     work_on_rq(app.queues["forecasting"], exc_handler=handle_forecasting_exception)
@@ -214,7 +214,7 @@ def test_failed_unknown_model(app, clean_redis, setup_test_data):
         start_of_roll=as_server_time(datetime(2015, 1, 1, 12)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, 14)),
         horizons=[horizon],
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         model_search_term="no-one-knows-this",
         custom_model_params=cmp,
     )
diff --git a/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py b/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
index 6ba275264..efdab97fa 100644
--- a/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
+++ b/flexmeasures/data/tests/test_forecasting_jobs_fresh_db.py
@@ -28,7 +28,7 @@ def test_forecasting_three_hours_of_wind(app, setup_fresh_test_data, clean_redis
         start_of_roll=as_server_time(datetime(2015, 1, 1, 10)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, 13)),
         horizons=[horizon],
-        asset_id=wind_device2.id,
+        old_sensor_id=wind_device2.id,
         custom_model_params=custom_model_params(),
     )
     print("Job: %s" % job[0].id)
@@ -61,7 +61,7 @@ def test_forecasting_two_hours_of_solar(app, setup_fresh_test_data, clean_redis)
         start_of_roll=as_server_time(datetime(2015, 1, 1, 12)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, 14)),
         horizons=[horizon],
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         custom_model_params=custom_model_params(),
     )
     print("Job: %s" % job[0].id)
@@ -109,7 +109,7 @@ def test_failed_model_with_too_much_training_then_succeed_with_fallback(
         start_of_roll=as_server_time(datetime(2015, 1, 1, hour_start)),
         end_of_roll=as_server_time(datetime(2015, 1, 1, hour_start + 2)),
         horizons=[horizon],
-        asset_id=solar_device1.id,
+        old_sensor_id=solar_device1.id,
         model_search_term=model_to_start_with,
         custom_model_params=cmp,
     )
diff --git a/flexmeasures/data/tests/test_scheduling_jobs.py b/flexmeasures/data/tests/test_scheduling_jobs.py
index ec7f0a4cf..42a317bdb 100644
--- a/flexmeasures/data/tests/test_scheduling_jobs.py
+++ b/flexmeasures/data/tests/test_scheduling_jobs.py
@@ -2,7 +2,8 @@
 from datetime import datetime, timedelta
 
 from flexmeasures.data.models.data_sources import DataSource
-from flexmeasures.data.models.assets import Asset, Power
+from flexmeasures.data.models.assets import Power
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.tests.utils import work_on_rq, exception_reporter
 from flexmeasures.data.services.scheduling import create_scheduling_job
 from flexmeasures.utils.time_utils import as_server_time
@@ -14,7 +15,7 @@ def test_scheduling_a_battery(db, app, add_battery_assets, setup_test_data):
     - schedule has been made
     """
 
-    battery = Asset.query.filter(Asset.name == "Test battery").one_or_none()
+    battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none()
     start = as_server_time(datetime(2015, 1, 2))
     end = as_server_time(datetime(2015, 1, 3))
     resolution = timedelta(minutes=15)
diff --git a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
index 722b69adf..18fce924b 100644
--- a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
+++ b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py
@@ -3,8 +3,9 @@
 import numpy as np
 import pandas as pd
 
-from flexmeasures.data.models.assets import Asset, Power
+from flexmeasures.data.models.assets import Power
 from flexmeasures.data.models.data_sources import DataSource
+from flexmeasures.data.models.time_series import Sensor
 from flexmeasures.data.services.scheduling import create_scheduling_job
 from flexmeasures.data.tests.utils import work_on_rq, exception_reporter
 from flexmeasures.utils.time_utils import as_server_time
@@ -23,8 +24,8 @@ def test_scheduling_a_charging_station(
     target_soc = 5
     duration_until_target = timedelta(hours=2)
 
-    charging_station = Asset.query.filter(
-        Asset.name == "Test charging station"
+    charging_station = Sensor.query.filter(
+        Sensor.name == "Test charging station"
     ).one_or_none()
     start = as_server_time(datetime(2015, 1, 2))
     end = as_server_time(datetime(2015, 1, 3))