Control a water heater and ground source heat pump based on cheap hours of spot priced electricity

Thanks for explaining the splicing, makes things easier to understand.

I was more refering to this:
kuva

Shouldn’t these control signals logically form one block, not separate hours?

Hi,

As explained in the description, there are two different algorithms.

The waterheater.js aims at finding the cheapest consecutive slot (if that is a word, halvin yhtäjaksoinen jakso). Nothing prevents you from using this one with your heat pump if you want to guarantee consequtive periodi. But that’s only within the day, so you might want to tweak the date helper to adjust the period to be like 21-20 and not 00-23.

Nibe.js will find N absolute cheapest individual hours by design. But to ensure that you will have at least some heating also during the day, you can use the slicing to force that.

I personally heat mostly during the nights, and nibe.js has been doing that for me because most of the time the absolute cheapest hours are consecutive hours during the night. But the heating period starts often already at 21, 22 or 23 and then continues over midnight.

Cheers,
Markus

I have had some trouble getting the wifi tasmota relay to obey the code control. After some tinkering around I found out I had not set MQTT qos at all. Setting it to 2 seems to have stopped the strange behavior of the relay, and now it follows control as it should

I don’t quite understand the selection of hours. Here is a graph of last night and this coming day from Influxdb:

I have two slices per day and at least 40% in each. What makes the algorithm select the local minimum at 21:00 rather than the later, much lower local minimum at 00:00? Could this be a bug?

I’ve been thinking about a proper algorithm, too.
The thermal storage capacity of most houses is well large enough that there’ll be no noticeable drop in hours. Plus most HP owners have a water buffer.

What about to simply define a heating time in minutes and have the algorithm select the cheapest m minutes for preferred heating and most expensive n minutes to stop ?
Heat pumps run on their own schedule by default.
If you use SGready to tell your hp to prefer or stop heating, it does not mean it will start or stop heating right away, it will decide based on local parameters such as buffer temperature.
During the rest of the time it would be in normal mode and will only heat when there’s a need to (buffer drop below threshold). If you turn on the fireplace it won’t drop so won’t heat.
So no need to ‘sort’ heating slices, right?
Anyone could adapt n and m to his housing’s needs (or financials, those actually are).
Long-term apply some AI, eventually, based on outer temperature maybe, or observed efficiency.

@timo12357 It’s hard to comment because you did not mention

  • how many hours you had allowed
  • did you use midnight to midnight as the start and stop parameters

The other parameters you did mention, i.e. that you have 2 slices and minimum 40% for each slice.

Another thing to mention is that you need to be very careful on how to interpret the step line visualizations. In Grafana you can choose whether the bar is “in the middle” of the control point or if it the left edge should be aligned to the “start” of the hour of the control point, see the screenshot below from Grafana.

image

I recentely moved from Grafana to openHab Charts, which is effectively Apache ECharts. With Apache ECharts, you can configure the step line to start, middle or end, but this is unfortunately not possible with bar charts, which is always middle. So I use the ‘middle’ also for the step line and the visualization is as follows:

Anyway, if you can’t figure it out with these hints, you should print debug messages to the logs in the nibe.js script that calculates the control points. There are some debug messages there already, but you can of course add more debug lines there to see what’s going on.

Markus

This is what I found in the log:

nibe.js: Number of needed hours: 9.852083333333333

This is what I have in date-helper.js

function getMidnight(type) {
    let date = new Date();
    if (date.getHours() >= 14) {
        date.setDate(date.getDate() + 1);
    }
    // Set time to 00:00:00.000
    date.setHours(0);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);

    if (type == 'stop') {
        date.setDate(date.getDate() + 1);
    }

Are those what you referred to?

Is there a good tutorial on how to use OpenHAB graphs somewhere? I would like to learn that, too.

Timo

That’s exactly what the algorithm does if you set the number of slices to be 1 (read: the whole 24h period is considered as one long slot and it’s not sliced into parts). Except that I’m using hours and not minutes as the unit of measure because a) the spot prices are per hour and b) heat pump compressors do not like being toggled on/off continuously.

My Nibe F-1226 has two external inputs, AUX1 and AUX2 and I can select in the Nibe menus what do I want to do with them. I could configure them to mean “SGReady A” and “SGReady B”, so it would be possible to switch between the four different SGReady modes with the different combinations of these two inputs. However, I have found that the other options give me way better results for cost optimization.

I use AUX1 to mean “Block compressor”. When I release the block, it does not force the compressor on, but it allows it to be on. Whether the compressor will actually start depends on the degree minutes which is a common concept also in many other heat pumps as far as I’ve understood from the Finnish heat pump forums. The degree minute concept is as follows:

  • The heat pump has an outdoor temperature sensor. Using the outdoor temperature and the heat curve, it calculates how warm the outgoing water should be. Let this be 30.0 C in this example.
  • It then compares how warm the outgoing water actually is (which depends on how much it cooled down on its previous cycle). Let this be 28.5 C in this example.
  • This means that every minute, 28.5 - 30.0 = - 1.5 degree minutes would accumulate. In 100 minutes this would be -150 degree minutes. You can consider the degree minutes as “heating debt”.
  • When the degree minutes (“heating debt”) falls below a threshold configured in the Nibe menus, the compressor starts unless it is blocked by AUX1. My threshold is -200 degree minutes.

This means that if the degree minutes is for example -600 and my openHab rule toggles the Switch which unblocks the compressor, the compressor will start immediately from the beginning of the cheap hour. If the allowed period is for example 5 hours and the degree minutes reach 0 after 3,5 hours, the compressor will automatically stop and the remaining 1,5 allowed hours will not be used even though they were allowed.

More on slicing
Winters can be very cold in Finland, even though we were lucky this winter to only have short periods when it was -20 C in Southern Finland where I live. The point with this slicing concept is that if the number of slices is 1 (read: the hours are chosen only based on their price in ascending order), it can easily happen that heating is allowed like 12 hours in a row and then the next 12 hours are blocked. Which means that the house will cool down too much. Of course if I would not use the “block compressor” method for AUX 1 but for example the SG ready modes, then I would not have this issue. But I would not get the cost savings either, because the heating would be occurring during non-optimal hours from the price perspective.

The thing here comes back to what do you want to optimize. The prices for this winter were so absurd that our household decided to optimize the heating cost and we can tolerate a bit of variance in the indoor temperature.

The screenshot below illustrates the temperature data from our house from one week from January when it was a bit colder than right now. The blue line (left axis) is the indoor temperature, the green line (right axis) is the outdoor temperature. The light red area represents the hours when the compressor has been allowed to run (most of the time this is equal to the actual compressor running hours).

(Note for Timo and others with an eagle’s eye: The heating hours have been slightly adjusted manually so it’s not an exact result of the algorithm but that’s irrelevant for the point I’m making, I just mention this so that users of that algorightm are not WTFd if they compare this to the spot prices of those days)

The spot prices are not visible in this chart but the point here is to illustrate that for example on Jan 6th it was not enough to have just one heating period. A second, shorter one was needed to ensure that the temperature doesn’t drop too much before the next night’s heating period. By dividing the day into 3 x 8h periods (slices) and forcing a certain percentage of heating hours for each slice guarantees that the the longest period when heating is not allowed does not exceed certain amount of hours. In 3 x 8h slices there would always be at least 1 heating hour per slice, which means that in the worst case the 1 hour is allocated to the first hour of the 8h slice and 1 hour is allocated to the last hour of the second 8h slice, meaning that there would still be 14hours of blocked hours. If 14 hours is too much, then the user can either split the day into 4 x 6h slices or increase the percentage of how much of the heating hours must be allocated at minimum for each slice.

Cheers,
Markus

1 Like

Ok, so you had:

  • 9.85 hours of heating time (I’ll round this to 10 because I’m too lazy)
  • You had 2 x 12h slices, these are the hours starting at 00:00 - 11:00 and 12:00 - 23:00
  • You are saying that both slices must have at least 40% of the heating hours
    The algorithm works as follows:
  1. It calculates that both slices must have 4 heating hours.
  2. It then analyzes the first slice, which is 00:00-11:00 and allows heating for the 4 cheapest hours in this slice
  3. It then analyzes the second slices, which is 12:00-13:00 and allows heating for the 4 cheapest hours in this slice
  4. There are now heating 2 hours left to be allocated to the cheapest 2 hours that have not yet been allowed.

You can verify if the hours were selected correctly by doing this same selection process on pen and paper and comparing the spot prices of those days.

Cheers,
Markus

1 Like

The general docs are quite short: Chart Pages | openHAB

However, there is a link to the official Apache ECharts docs which are available at Cheat Sheet - Apache ECharts

Here are a couple of examples for your convenience. Before you dive into the wonderful world of the Charts, make sure that you have created an Item with a name “spot_price”, “nibe_control”, “waterheater_control” or whatever are the exact names of the control points that you write to your InfluxDB with the Rules. This way openHab Charts can “see” the data that you have in the database even though you have bypassed openHab persistence layers by saving the data directly from the Rules.

Chart like this:

Can be achieved with a chart configuration like this: (copy-paste this to the “code” tab and then you can go back to the “design” tab to see how it looks like in the click-click UI). Some parts of the ECharts capabilities are not configurable directly with the openHab chart UI but each of the chart components can be edited as YAML and you can find hints in the official ECharts documentation what kind of attributes and values are available. For example a line chart can be configured to be a step-line by adding a “step: middle” attribute and value.

config:
  label: Tuntihinnat ja ohjaukset
  sidebar: true
  chartType: day
  order: "-15"
slots:
  grid:
    - component: oh-chart-grid
      config: {}
  xAxis:
    - component: oh-time-axis
      config:
        gridIndex: 0
  yAxis:
    - component: oh-value-axis
      config:
        gridIndex: 0
    - component: oh-value-axis
      config:
        gridIndex: 0
        splitLine:
          show: false
    - component: oh-value-axis
      config:
        gridIndex: 0
        show: false
        splitLine:
          show: false
        max: "5"
        min: "0"
  series:
    - component: oh-time-series
      config:
        name: Tuntihinnat
        gridIndex: 0
        xAxisIndex: 0
        yAxisIndex: 0
        type: line
        step: middle
        item: spot_price
        areaStyle:
          opacity: "0.4"
        markLine:
          data:
            - name: Avg
              type: average
              label:
                distance: -150
    - component: oh-time-series
      config:
        gridIndex: 0
        xAxisIndex: 0
        type: line
        item: fmi_forecast_temperature
        markLine:
          data:
            - name: Avg
              type: average
              label:
                distance: -100
        yAxisIndex: 1
        name: Sääennuste
    - component: oh-time-series
      config:
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: nibe_control
        yAxisIndex: 2
        name: "Nibe: kompressori"
        service: influxdb
    - component: oh-time-series
      config:
        name: Lämminvesivaraaja
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: waterheater_control
        yAxisIndex: 2
        service: influxdb
    - component: oh-time-series
      config:
        name: "Nibe: Käyttöveden lämmitys"
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: nibe_water_control
        yAxisIndex: 2
        service: influxdb
    - component: oh-time-series
      config:
        name: Auton lataus
        gridIndex: 0
        xAxisIndex: 0
        type: bar
        stack: foo
        item: car_charging_control
        yAxisIndex: 2
        service: influxdb
  tooltip:
    - component: oh-chart-tooltip
      config:
        show: true
        orient: horizontal
  legend:
    - component: oh-chart-legend
      config:
        show: true
        orient: horizontal
1 Like

Thanks a lot for this. Plug and play chart!
How do you extract the hot water generation data from your heatpump?

I don’t. These are the control points for the Nibe hot water heating, which is my aux 2.

I originally had it so that I only had waterheater_control. And then I had an hourly rule that was toggling the Switch Item for the 300 liter waterheater and Nibe heat pumps aux 2 which I have configured to mean “hot water heating blocked”.

Recently I separated these into 2 separate control points so that waterheater_control controls the 300 boiler and nibe_water_control controls the Nube hot water heating.

The rationale for this is that I raised the 300 liter boiler thermostate from 65 to 75 degrees. If nobody goes to shower, the temperature drops only about 3-4 degrees in a day. Which means that I can easily skip one and or two nights if the prices are high also during the night and when they are cheap again, I then allow 5 hours for the 300 liter boiler.

Foreca has this wind power forecast which is an excellent crystal ball for seeing how the spot prices are going to look like beyond the day-ahead spot prices because they forecast wind power production and combine it to the temperature forecast which they are using to derive a forecast on how much electricity is needed for heating in the whole country.

For example this evening we heat the boiler to the max 75 and I’m going to skip the night between 14th and 15th. The early hours on Feb 16th are most probably going to be reasonable priced.

The Nibe heat pump’s hot water tank is only 180 liters so I can’t skip heating of that. I need to allow it enough heating hours every day so that the it doesn’t push completely cold water to the 300 liter boiler. And I don’t want to take risks with legionella bacteria either so I want to be able to control the Nibe hot water heating independently from the 300 liter boiler.

I have a 2866 wifi relay with Tasmota, that reads the pump status from the Nibe 1245 aux output.
You wrote earlier that you have integrated the wind forecast in your private system? Did you mean the manual checking from Foreca, or do you have wind compensation included in the temperature numbers like in the Fissio solution? Could you elaborate on this sometime?

Here’s a chart that displays the 300 liter water heater / boiler.

I have installed 3 DS18B20 temperatures sensors to the boiler tank like this so that the sensor is touching the metal of the tank and then applied an offset to the temperature readings so that the top sensor is showing ~72 degrees when the heating period ends and the water is at its hottest. The hot water is taken from the top of the tank (blue line) and new water is fed to the boiler from the bottom. You can see that when we came back home from the cottage yesterday evening on the 12th somebody went to take a shower because the yellow line (bottom sensor) dropped a bit.

My FMI script fetches the weather forecast from FMI API. I save the following FMI forecast attributes:

  • fmi_forecast_PrecipitationAmount (I don’t currently use this for anything)
  • fmi_forecast_TotalCloudCover (I don’t currently use this for anything)
  • fmi_forecast_WindSpeedMS
  • fmi_forecast_temperature

From the temperature and windspeed I derive a 5th dimension which I save as

  • fmi_forecast_WindChillTemp

This “wind chill compensated temperature” is the same “feels like” temperature that you can see in most of the weather forecasts. This is actually the temperature that I use for calculating how much of heating is needed because when it’s cold AND windy, the house will cool down signficantly faster than when it’s just cold.

The formula for how to calculate the “feels like” temperature can be found from Wikipedia, see Wind chill - Wikipedia

Here’s the javascript method to calculate it. I’ll leave it as an exercise for you to figure out how to call this method :slight_smile:

/**
 * Calculates the wind chill factor for the temperature
 *
 * @param array temperaturePoints
 * @param array windspeedPoints
 *
 * @return array
 *   Array of point objects.
 */
function calculateWindChillTempPoints(temperaturePoints, windspeedPoints) {
    console.log('fmi.js: Calculating wind chill factors from the temperature and wind speed...');
    let points = [];
    if (temperaturePoints.length != windspeedPoints.length) {
        console.log('Different number of temperature and wind speed points!');
        return points;
    }

    for (let i = 0; i < temperaturePoints.length; i++) {
        let datetime = temperaturePoints[i].datetime;
        let temp = temperaturePoints[i].value;
        let wind_ms = windspeedPoints[i].value;
        let wind_kmh = 3.6 * wind_ms;

        // https://en.wikipedia.org/wiki/Wind_chill#North_American_and_United_Kingdom_wind_chill_index
        let windchill = temp;
        if (temp < 10 && wind_kmh > 4.8) {
            windchill = 13.12 + 0.6215 * temp - 11.37 * (wind_kmh ** 0.16) + 0.3965 * temp * (wind_kmh ** 0.16);
        }

        let point = {
            datetime: datetime,
            value: windchill
        }
        points.push(point);
    }

    return points;

}

If that’s true for your setup, it would be a Nibe bug or misconfig of yours I guess.
Actually how SGready is meant to be implemented is right like what you say about your ‘manual’ compressor control.
SGr mode 3 (OFF:ON) means “cheap power” and should leave optimization of what that means in terms of heating to the pump. I’d expect the vendor himself should know best how to optimize for that situation, including applying safety limits, avoiding ON/OFF toggling etc.
4 (ON:ON) means forced heating but isn’t 100% forced. Again ultimate decisions are left to the pump local control.
Many pumps also have config settings what to apply when SGr 3/4 is set so you can override or adapt what they will do with the SGr signal set. Mine for example on SGr mode 4 will increase water temperature by 7K but those 7 are still configurable. And it sets domestic hot water to 80°C (absolute and not configurable). Strange I would have expected it to happen with the other tank the other way round but hey that’s what the vendor defined. For SGr mode 3 the increase is less and relative on DHW.

The idea of SGr3 is not “heat” but “increased heating” so it should increase the buffer temperature in addition to doing ‘live’ heating. In the 12 ‘block’ hours to follow that buffered heat would be deployed, slowing down the rate at which the house is cooling down.
You would not really block heating during ‘block’ hours but apply the ‘normal’ SGr mode 2, i.e. leave the decision to heat to the pump in that time (it would not start heating as long as it has enough buffered heat to use from).

In fact the financial optimum would be to have 24 slices, wouldn’t it?
Pricing changes are dynamic. You don’t know more than one day in ahead. You would not want to select a number of slices that might be optimal for one day’s tariffing curve just to notice that it isn’t optimal for the next day any more.
While dynamic price curve throughout the day usually has only one or two peaks and you can have consecutive hours of cheap heating, the (theoretical) worst case is a toggle-like row A-B-A-B-A- … -B with A being any of the 12 cheaper hours of the day and B being any of the 12 most expensive hours.
So in this worst case scenario, to use less than 24 slices would force you to heat during one or more “B” hours. And you as a user should be able to ‘fire and forget’ and have the algorithm take care of optimum selection.

And why have slices at all ?
While of course you have designed your system to match your conditions/environment, in the generalized use case there is another dimension: PV power.
This is where I’m coming from. PV kWhs are cheaper than the cheapest tariff, and there’s no such thing as slices, PV surplus can rise and drop within seconds hence my suggestion to use minutes rather than hours. With the ON/OFF interval potentially down to some minutes, only limited by pump safety parameters, slicing does not make much sense.
SGready was designed as a PV surplus information transmitter with that in mind and leaves control to the pump so vendors must not care about the price but can focus on ensuring the optimum behavior in terms of thermodynamics, compressor activity cycle length, wearout on mechanical parts and so on.
So those questions are already taken care of by the vendor.

Finally, have you considered to also align heating times with outside temperatures taken from the hourly weather forecast, in addition to cheap hours ?
Efficiency of the pump is better when it’s warmer.
It’s becoming a multi-dimensional optimization task then. To find out the coefficients of how much difference that makes and if/when efficiency savings are more than the savings on price is probably difficult. I have no immediate opinion yet on whether this makes sense, but it’s possibly worth a try.

I inverted my control as per your advice above and to do that I had to choose “block compressor” from the Nibe UI. At the same time I lost production of hot tap water. :-/ How did you solve this problem with your heat pump?

You’re right that toggling between SG Ready modes would most probably yield in very similar results. The manual of Nibe F-1226 says this:

“SG Ready” is a smart form of tariff control where your electricity supplier can affect the indoor and hot water temperatures or simply block the additional heat and/or the compressor in the heat pump at certain times of the day (can be selected in menu 4.1.5 after the function is activated). Activate the function by connecting potential-free switch functions to two inputs selected in menu 5.4 (SG Ready A and SG Ready B).

Closed or open switch means one of the following:
– Blocking (A: Closed, B: Open): “SG Ready” is active. The compressor in the heat pump and additional heat is blocked.

– Normal mode (A: Open, B: Open): “SG Ready” is not active. No effect on the system.

– Low price mode (A: Open, B: Closed): “SG Ready” is active. The system focuses on costs savings and can for example exploit a low tariff from the electricity supplier or over-capacity from any own power source (effect on the system can be adjusted in the menu 4.1.5).

– Overcapacity mode (A: Closed, B: Closed): “SG Ready” is active. The system is permitted to run
at full capacity at over capacity (very low price) with the electricity supplier (effect on the system is set-
table in menu 4.1.5).

And then the documentation of these menu configurations:

Here you set whether room temperature should be affected when activating “SG Ready”.

ROOM TEMPERATURE
Menu 4.1.5

With low price mode on “SG Ready” the parallel offset for the indoor temperature is increased by “+1”. If a room sensor is installed and activated, the desired room temperature is instead increased by 1 °C.

With over capacity mode on “SG Ready” the parallel offset for the indoor temperature is increased by “+2”. If a room sensor is installed and activated, the desired room temperature is instead increased by 2 °C.

HOT WATER
Here you set whether the temperature of the hot water should be affected when activating “SG Ready”.

With low price mode on “SG Ready” the stop temperature of the hot water is set as high as possible at only compressor operation (immersion heater not permitted).

With over capacity mode of “SG Ready” the hot water is set to “luxury” (immersion heater permitted).

So basically switching between the different SG Ready modes would most probably result in very similar behavior than what I’m now toggling in a more brutal way by using Aux 1 as “block compressor” and Aux 2 as “block hot water”.

The thing with hot water is that we have that 300 liter boiler “behind” the Nibe heat pump, in other words Nibe pre-heats the water in the 180 liter tank and this water is then fed into the 300 liter boiler. New cold water is coming to Nibe heat pump and the heated hot water is taken from the 300 liter boiler. With this much of hot water capacity, it’s easily sufficient to force Nibe to heat the hot water on the absolute cheapest hours of the day because there is absolutely no need to heat hot water multiple times per day.

I currently can achieve this very easily by controlling the hot water blocking explicitly. The practical difference to SG Ready is not particularly big, though, because activating the low price or overcapacity mode would cause the hot water heating so most probably in normal mode the hot water heating would not start very soon. Unless somebody takes a shower, and fresh cold water is fed into the Nibe tank. I could of course set the SG Ready to “Blocked” mode for most of the day, but that’s effectively exactly what I’m doing today.

Another factor with this hot water thing is that we have a pump that circulates hot water to the manifold (“jakotukki in Finnish”) and back so that there will be hot water from the taps in a matter of seconds. This pump was originally running all the time for the first 2 years and caused a massive amount of compressor starts before I got interested in this energy optimization and realized it. There were like 20k compressor starts in 2 years (the compressor lifecycle is somewhere around 100k starts as per some discussions on heat pump forums).

I’m currently controlling this pump so that there is one relay controlling the pump and it runs 30 minutes in the morning, 30 minutes in the later afternoon when we typically come home, 30 minutes around the time when the kids do their evening wash and 30 minutes around the time when the parents do theirs. This made a HUGE difference in how the lower temperature sensor of the hot water tank shows. With the “block hot water except on the cheapest hours” the number of compressor starts that we have experienced in the winter months is averaging on 2.5 compressor starts per day. Earlier there were like 25 starts per day.

The advantage of better Nibe models is that they have more than 2 AUX inputs. So with those pumps it would be trivial to use the SG Ready modes for optimizing the heating and then use one more AUX input to explicitly control the hot water block. Unfortunately my Nibe F1226 only has 2 aux inputs. :frowning:

I believe you mean that the cheap hours are utilized regardless when they occur during the day. That’s what slice = 1 does, it then just picks up the number of needed hours in the order of their spot price.

Cheers,
Markus

If you need to heat hot water during the day, you must allow the compressor - there’s simply no way around that.

We have 180 liters of hot water capacity in Nibe heat pump + 300 liters in an external boiler so this is enough for like 2 or 3 days usage (unless we use the bath tub).

Markus

Anyone else experiencing problems with the entso-e script?

For me it doesn’t now run the transformation of received data to json without error.


2023-02-14 18:40:11.162 [INFO ] [nhab.automation.script.ui.d023cfcc23] - entsoe.js: Making an API call to Entso-E API...
2023-02-14 18:40:11.645 [INFO ] [nhab.automation.script.ui.d023cfcc23] - entose.js: transforming XML to JSON and parsing prices...
2023-02-14 18:40:11.703 [ERROR] [t.internal.XsltTransformationService] - transformation throws exception
javax.xml.transform.TransformerException: javax.xml.transform.TransformerException: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: The element type "meta" must be terminated by the matching end-tag "</meta>".
	at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform
……..
………

	... 153 more
2023-02-14 18:40:11.727 [ERROR] [nhab.automation.script.ui.d023cfcc23] - entsoe.js: Exception parsing spot prices: Invalid JSON: <json>:13:0 Expected json literal but found <